Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
<?php
namespace Mautic\PointBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\FormEntity;
use Mautic\CoreBundle\Entity\UuidInterface;
use Mautic\CoreBundle\Entity\UuidTrait;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
class Group extends FormEntity implements UuidInterface
{
use UuidTrait;
public const TABLE_NAME = 'point_groups';
public const ENTITY_NAME = 'point_group';
private ?int $id = null;
private ?string $name = '';
private ?string $description = '';
/**
* @param ORM\ClassMetadata<Group> $metadata
*/
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable(self::TABLE_NAME)
->setCustomRepositoryClass(GroupRepository::class);
static::addUuidField($builder);
$builder->addIdColumns();
}
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$metadata->addPropertyConstraint('name', new Assert\NotBlank([
'message' => 'mautic.core.name.required',
]));
}
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
{
$metadata->setGroupPrefix('pointGroup')
->addListProperties(
[
'id',
'name',
'description',
]
)
->addProperties(
[
'id',
'name',
'description',
]
)
->build();
}
public function getId(): ?int
{
return $this->id;
}
public function setDescription(?string $description): self
{
$this->isChanged('description', $description);
$this->description = $description;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setName(?string $name): self
{
$this->isChanged('name', $name);
$this->name = $name;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Entity;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\CommonEntity;
use Mautic\LeadBundle\Entity\Lead;
class GroupContactScore extends CommonEntity
{
public const TABLE_NAME = 'point_group_contact_score';
private Lead $contact;
private Group $group;
private int $score;
public function __construct()
{
$this->contact = new Lead();
$this->group = new Group();
$this->score = 0;
}
/**
* @param ORM\ClassMetadata<GroupContactScore> $metadata
*/
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable(self::TABLE_NAME)
->setCustomRepositoryClass(GroupContactScoreRepository::class);
$builder->addContact(false, 'CASCADE', true, 'groupScores');
$builder->createManyToOne('group', Group::class)
->isPrimaryKey()
->addJoinColumn('group_id', 'id', true, false, 'CASCADE')
->build();
$builder->createField('score', Types::INTEGER)
->build();
}
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
{
$metadata->setGroupPrefix('groupContactScore')
->addListProperties(
[
'score',
'group',
]
)
->addProperties(
[
'score',
'group',
]
)
->build();
}
public function getContact(): Lead
{
return $this->contact;
}
public function setContact(Lead $contact): void
{
$this->contact = $contact;
}
public function getGroup(): Group
{
return $this->group;
}
public function setGroup(Group $group): void
{
$this->group = $group;
}
public function getScore(): int
{
return $this->score;
}
public function setScore(int $score): void
{
$this->score = $score;
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Entity;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<GroupContactScore>
*/
class GroupContactScoreRepository extends CommonRepository
{
public function compareScore(int $leadId, int $groupId, int $score, string $operatorExpr): bool
{
$q = $this->_em->getConnection()->createQueryBuilder();
$q->select('lcs.contact_id')
->from(MAUTIC_TABLE_PREFIX.GroupContactScore::TABLE_NAME, 'lcs');
$expr = $q->expr()->and(
$q->expr()->eq('lcs.contact_id', ':lead'),
$q->expr()->eq('lcs.group_id', ':groupId'),
$q->expr()->$operatorExpr('lcs.score', ':score'),
);
$q->where($expr)
->setParameter('lead', $leadId)
->setParameter('groupId', $groupId)
->setParameter('score', $score);
return false !== $q->executeQuery()->fetchOne();
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Mautic\PointBundle\Entity;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<Group>
*/
class GroupRepository extends CommonRepository
{
public function getTableAlias(): string
{
return 'pl';
}
public function getEntities(array $args = [])
{
// Without qb it returns entities indexed by id instead of array indexes
$args['qb'] = $this->createQueryBuilder($this->getTableAlias());
return parent::getEntities($args);
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Mautic\PointBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\IpAddress;
class LeadPointLog
{
public const TABLE_NAME = 'point_lead_action_log';
/**
* @var Point
**/
private $point;
/**
* @var \Mautic\LeadBundle\Entity\Lead
*/
private $lead;
/**
* @var IpAddress|null
*/
private $ipAddress;
/**
* @var \DateTimeInterface
**/
private $dateFired;
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable(self::TABLE_NAME)
->setCustomRepositoryClass(LeadPointLogRepository::class);
$builder->createManyToOne('point', 'Point')
->isPrimaryKey()
->addJoinColumn('point_id', 'id', true, false, 'CASCADE')
->inversedBy('log')
->build();
$builder->addLead(false, 'CASCADE', true);
$builder->addIpAddress(true);
$builder->createField('dateFired', 'datetime')
->columnName('date_fired')
->build();
}
/**
* @return mixed
*/
public function getDateFired()
{
return $this->dateFired;
}
/**
* @param mixed $dateFired
*/
public function setDateFired($dateFired): void
{
$this->dateFired = $dateFired;
}
/**
* @return IpAddress|null
*/
public function getIpAddress()
{
return $this->ipAddress;
}
/**
* @param IpAddress $ipAddress
*/
public function setIpAddress($ipAddress): void
{
$this->ipAddress = $ipAddress;
}
/**
* @return mixed
*/
public function getLead()
{
return $this->lead;
}
/**
* @param mixed $lead
*/
public function setLead($lead): void
{
$this->lead = $lead;
}
/**
* @return mixed
*/
public function getPoint()
{
return $this->point;
}
/**
* @param mixed $point
*/
public function setPoint($point): void
{
$this->point = $point;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Mautic\PointBundle\Entity;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<LeadPointLog>
*/
class LeadPointLogRepository extends CommonRepository
{
/**
* Updates lead ID (e.g. after a lead merge).
*/
public function updateLead($fromLeadId, $toLeadId): void
{
// First check to ensure the $toLead doesn't already exist
$results = $this->_em->getConnection()->createQueryBuilder()
->select('pl.point_id')
->from(MAUTIC_TABLE_PREFIX.'point_lead_action_log', 'pl')
->where('pl.lead_id = '.$toLeadId)
->executeQuery()
->fetchAllAssociative();
$actions = [];
foreach ($results as $r) {
$actions[] = $r['point_id'];
}
$q = $this->_em->getConnection()->createQueryBuilder();
$q->update(MAUTIC_TABLE_PREFIX.'point_lead_action_log')
->set('lead_id', (int) $toLeadId)
->where('lead_id = '.(int) $fromLeadId);
if (!empty($actions)) {
$q->andWhere(
$q->expr()->notIn('point_id', $actions)
)->executeStatement();
// Delete remaining leads as the new lead already belongs
$this->_em->getConnection()->createQueryBuilder()
->delete(MAUTIC_TABLE_PREFIX.'point_lead_action_log')
->where('lead_id = '.(int) $fromLeadId)
->executeStatement();
} else {
$q->executeStatement();
}
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Mautic\PointBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\IpAddress;
class LeadTriggerLog
{
public const TABLE_NAME = 'point_lead_event_log';
/**
* @var TriggerEvent
**/
private $event;
/**
* @var \Mautic\LeadBundle\Entity\Lead
**/
private $lead;
/**
* @var IpAddress|null
**/
private $ipAddress;
/**
* @var \DateTimeInterface
**/
private $dateFired;
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable(self::TABLE_NAME)
->setCustomRepositoryClass(LeadTriggerLogRepository::class);
$builder->createManyToOne('event', 'TriggerEvent')
->isPrimaryKey()
->addJoinColumn('event_id', 'id', false, false, 'CASCADE')
->inversedBy('log')
->build();
$builder->addLead(false, 'CASCADE', true);
$builder->addIpAddress(true);
$builder->createField('dateFired', 'datetime')
->columnName('date_fired')
->build();
}
/**
* @return mixed
*/
public function getDateFired()
{
return $this->dateFired;
}
/**
* @param mixed $dateFired
*/
public function setDateFired($dateFired): void
{
$this->dateFired = $dateFired;
}
/**
* @return IpAddress|null
*/
public function getIpAddress()
{
return $this->ipAddress;
}
/**
* @param IpAddress $ipAddress
*/
public function setIpAddress($ipAddress): void
{
$this->ipAddress = $ipAddress;
}
/**
* @return mixed
*/
public function getLead()
{
return $this->lead;
}
/**
* @param mixed $lead
*/
public function setLead($lead): void
{
$this->lead = $lead;
}
/**
* @return mixed
*/
public function getEvent()
{
return $this->event;
}
/**
* @param mixed $event
*/
public function setEvent($event): void
{
$this->event = $event;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Mautic\PointBundle\Entity;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<LeadTriggerLog>
*/
class LeadTriggerLogRepository extends CommonRepository
{
/**
* Updates lead ID (e.g. after a lead merge).
*/
public function updateLead($fromLeadId, $toLeadId): void
{
// First check to ensure the $toLead doesn't already exist
$results = $this->_em->getConnection()->createQueryBuilder()
->select('pl.event_id')
->from(MAUTIC_TABLE_PREFIX.'point_lead_event_log', 'pl')
->where('pl.lead_id = '.$toLeadId)
->executeQuery()
->fetchAllAssociative();
$events = [];
foreach ($results as $r) {
$events[] = $r['event_id'];
}
$q = $this->_em->getConnection()->createQueryBuilder();
$q->update(MAUTIC_TABLE_PREFIX.'point_lead_event_log')
->set('lead_id', (int) $toLeadId)
->where('lead_id = '.(int) $fromLeadId);
if (!empty($events)) {
$q->andWhere(
$q->expr()->notIn('event_id', $events)
)->executeStatement();
// Delete remaining leads as the new lead already belongs
$this->_em->getConnection()->createQueryBuilder()
->delete(MAUTIC_TABLE_PREFIX.'point_lead_event_log')
->where('lead_id = '.(int) $fromLeadId)
->executeStatement();
} else {
$q->executeStatement();
}
}
}

View File

@@ -0,0 +1,446 @@
<?php
namespace Mautic\PointBundle\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\CategoryBundle\Entity\Category;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\FormEntity;
use Mautic\CoreBundle\Entity\UuidInterface;
use Mautic\CoreBundle\Entity\UuidTrait;
use Mautic\CoreBundle\Helper\IntHelper;
use Mautic\ProjectBundle\Entity\ProjectTrait;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
#[ApiResource(
operations: [
new GetCollection(security: "is_granted('point:triggers:viewown')"),
new Post(security: "is_granted('point:triggers:create')"),
new Get(security: "is_granted('point:triggers:viewown')"),
new Put(security: "is_granted('point:triggers:editown')"),
new Patch(security: "is_granted('point:triggers:editother')"),
new Delete(security: "is_granted('point:triggers:deleteown')"),
],
normalizationContext: [
'groups' => ['point:read'],
'swagger_definition_name' => 'Read',
'api_included' => ['category'],
],
denormalizationContext: [
'groups' => ['point:write'],
'swagger_definition_name' => 'Write',
]
)]
class Point extends FormEntity implements UuidInterface
{
use UuidTrait;
use ProjectTrait;
public const ENTITY_NAME = 'point';
/**
* @var int
*/
#[Groups(['point:read'])]
private $id;
/**
* @var string
*/
#[Groups(['point:read', 'point:write'])]
private $name;
/**
* @var string|null
*/
#[Groups(['point:read', 'point:write'])]
private $description;
/**
* @var string
*/
#[Groups(['point:read', 'point:write'])]
private $type;
/**
* @var bool
*/
#[Groups(['point:read', 'point:write'])]
private $repeatable = false;
/**
* @var \DateTimeInterface
*/
#[Groups(['point:read', 'point:write'])]
private $publishUp;
/**
* @var \DateTimeInterface
*/
#[Groups(['point:read', 'point:write'])]
private $publishDown;
/**
* @var int
*/
#[Groups(['point:read', 'point:write'])]
private $delta = 0;
/**
* @var array
*/
#[Groups(['point:read', 'point:write'])]
private $properties = [];
/**
* @var ArrayCollection<int,LeadPointLog>
*/
private $log;
/**
* @var Category|null
**/
#[Groups(['point:read', 'point:write'])]
private $category;
#[Groups(['point:read', 'point:write'])]
private ?Group $group = null;
public function __clone()
{
$this->id = null;
parent::__clone();
}
public function __construct()
{
$this->log = new ArrayCollection();
$this->initializeProjects();
}
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('points')
->setCustomRepositoryClass(PointRepository::class)
->addIndex(['type'], 'point_type_search');
$builder->addIdColumns();
$builder->createField('type', 'string')
->length(50)
->build();
$builder->addPublishDates();
$builder->createField('repeatable', 'boolean')
->build();
$builder->addField('delta', 'integer');
$builder->addField('properties', 'array');
$builder->createOneToMany('log', 'LeadPointLog')
->mappedBy('point')
->cascadePersist()
->cascadeRemove()
->fetchExtraLazy()
->build();
$builder->addCategory();
$builder->createManyToOne('group', Group::class)
->addJoinColumn('group_id', 'id', true, false, 'CASCADE')
->build();
static::addUuidField($builder);
self::addProjectsField($builder, 'point_projects_xref', 'point_id');
}
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$metadata->addPropertyConstraint('name', new Assert\NotBlank([
'message' => 'mautic.core.name.required',
]));
$metadata->addPropertyConstraint('type', new Assert\NotBlank([
'message' => 'mautic.point.type.notblank',
]));
$metadata->addPropertyConstraint('delta', new Assert\NotBlank([
'message' => 'mautic.point.delta.notblank',
]));
$metadata->addPropertyConstraint('delta', new Assert\Range([
'min' => IntHelper::MIN_INTEGER_VALUE,
'max' => IntHelper::MAX_INTEGER_VALUE,
]));
}
/**
* Prepares the metadata for API usage.
*/
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
{
$metadata->setGroupPrefix('point')
->addListProperties(
[
'id',
'name',
'category',
'type',
'description',
]
)
->addProperties(
[
'publishUp',
'publishDown',
'delta',
'properties',
'repeatable',
]
)
->build();
self::addProjectsInLoadApiMetadata($metadata, 'point');
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @param array $properties
*
* @return self
*/
public function setProperties($properties)
{
$this->isChanged('properties', $properties);
$this->properties = $properties;
return $this;
}
/**
* @return array
*/
public function getProperties()
{
return $this->properties;
}
/**
* @param string $type
*
* @return self
*/
public function setType($type)
{
$this->isChanged('type', $type);
$this->type = $type;
return $this;
}
/**
* @return string
*/
public function getType()
{
return $this->type;
}
public function convertToArray(): array
{
return get_object_vars($this);
}
/**
* @param string $description
*
* @return self
*/
public function setDescription($description)
{
$this->isChanged('description', $description);
$this->description = $description;
return $this;
}
/**
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* @param string $name
*
* @return self
*/
public function setName($name)
{
$this->isChanged('name', $name);
$this->name = $name;
return $this;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return self
*/
public function addLog(LeadPointLog $log)
{
$this->log[] = $log;
return $this;
}
public function removeLog(LeadPointLog $log): void
{
$this->log->removeElement($log);
}
/**
* @return \Doctrine\Common\Collections\Collection
*/
public function getLog()
{
return $this->log;
}
/**
* @param \DateTime $publishUp
*
* @return Point
*/
public function setPublishUp($publishUp)
{
$this->isChanged('publishUp', $publishUp);
$this->publishUp = $publishUp;
return $this;
}
/**
* @return \DateTimeInterface
*/
public function getPublishUp()
{
return $this->publishUp;
}
/**
* @param \DateTime $publishDown
*
* @return Point
*/
public function setPublishDown($publishDown)
{
$this->isChanged('publishDown', $publishDown);
$this->publishDown = $publishDown;
return $this;
}
/**
* @return \DateTimeInterface
*/
public function getPublishDown()
{
return $this->publishDown;
}
/**
* @return mixed
*/
public function getCategory()
{
return $this->category;
}
/**
* @param mixed $category
*/
public function setCategory($category): void
{
$this->category = $category;
}
/**
* @return mixed
*/
public function getDelta()
{
return $this->delta;
}
/**
* @param mixed $delta
*/
public function setDelta($delta): void
{
$this->delta = (int) $delta;
}
/**
* @param bool $repeatable
*
* @return Point
*/
public function setRepeatable($repeatable)
{
$this->isChanged('repeatable', $repeatable);
$this->repeatable = $repeatable;
return $this;
}
/**
* @return bool
*/
public function getRepeatable()
{
return $this->repeatable;
}
public function getGroup(): ?Group
{
return $this->group;
}
public function setGroup(?Group $group): void
{
$this->group = $group;
}
}

View File

@@ -0,0 +1,145 @@
<?php
namespace Mautic\PointBundle\Entity;
use Mautic\CoreBundle\Entity\CommonRepository;
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
/**
* @extends CommonRepository<Point>
*/
class PointRepository extends CommonRepository
{
use ProjectRepositoryTrait;
public function getEntities(array $args = [])
{
$q = $this->_em
->createQueryBuilder()
->select($this->getTableAlias().', cat')
->from(Point::class, $this->getTableAlias())
->leftJoin($this->getTableAlias().'.category', 'cat')
->leftJoin($this->getTableAlias().'.group', 'pl');
$args['qb'] = $q;
return parent::getEntities($args);
}
public function getTableAlias(): string
{
return 'p';
}
/**
* Get array of published actions based on type.
*
* @param string $type
*
* @return array
*/
public function getPublishedByType($type)
{
$q = $this->createQueryBuilder('p')
->select('partial p.{id, type, name, delta, repeatable, properties}')
->setParameter('type', $type);
// make sure the published up and down dates are good
$expr = $this->getPublishedByDateExpression($q);
$expr->add($q->expr()->eq('p.type', ':type'));
$q->where($expr);
return $q->getQuery()->getResult();
}
/**
* @param string $type
* @param int $leadId
*/
public function getCompletedLeadActions($type, $leadId): array
{
$q = $this->_em->getConnection()->createQueryBuilder()
->select('p.*')
->from(MAUTIC_TABLE_PREFIX.'point_lead_action_log', 'x')
->innerJoin('x', MAUTIC_TABLE_PREFIX.'points', 'p', 'x.point_id = p.id');
// make sure the published up and down dates are good
$q->where(
$q->expr()->and(
$q->expr()->eq('p.type', ':type'),
$q->expr()->eq('x.lead_id', (int) $leadId)
)
)
->setParameter('type', $type);
$results = $q->executeQuery()->fetchAllAssociative();
$return = [];
foreach ($results as $r) {
$return[$r['id']] = $r;
}
return $return;
}
/**
* @param int $leadId
*/
public function getCompletedLeadActionsByLeadId($leadId): array
{
$q = $this->_em->getConnection()->createQueryBuilder()
->select('p.*')
->from(MAUTIC_TABLE_PREFIX.'point_lead_action_log', 'x')
->innerJoin('x', MAUTIC_TABLE_PREFIX.'points', 'p', 'x.point_id = p.id');
// make sure the published up and down dates are good
$q->where(
$q->expr()->and(
$q->expr()->eq('x.lead_id', (int) $leadId)
)
);
$results = $q->executeQuery()->fetchAllAssociative();
$return = [];
foreach ($results as $r) {
$return[$r['id']] = $r;
}
return $return;
}
protected function addCatchAllWhereClause($q, $filter): array
{
return $this->addStandardCatchAllWhereClause($q, $filter, [
'p.name',
'p.description',
]);
}
protected function addSearchCommandWhereClause($q, $filter): array
{
return match ($filter->command) {
$this->translator->trans('mautic.project.searchcommand.name'), $this->translator->trans('mautic.project.searchcommand.name', [], null, 'en_US') => $this->handleProjectFilter(
$this->_em->getConnection()->createQueryBuilder(),
'point_id',
'point_projects_xref',
$this->getTableAlias(),
$filter->string,
$filter->not
),
default => $this->addStandardSearchCommandWhereClause($q, $filter),
};
}
/**
* @return string[]
*/
public function getSearchCommands(): array
{
return array_merge(['mautic.project.searchcommand.name'], $this->getStandardSearchCommands());
}
}

View File

@@ -0,0 +1,432 @@
<?php
namespace Mautic\PointBundle\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\CategoryBundle\Entity\Category;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\FormEntity;
use Mautic\CoreBundle\Entity\UuidInterface;
use Mautic\CoreBundle\Entity\UuidTrait;
use Mautic\ProjectBundle\Entity\ProjectTrait;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
#[ApiResource(
operations: [
new GetCollection(security: "is_granted('point:triggers:viewown')"),
new Post(security: "is_granted('point:triggers:create')"),
new Get(security: "is_granted('point:triggers:viewown')"),
new Put(security: "is_granted('point:triggers:editown')"),
new Patch(security: "is_granted('point:triggers:editother')"),
new Delete(security: "is_granted('point:triggers:deleteown')"),
],
normalizationContext: [
'groups' => ['trigger:read'],
'swagger_definition_name' => 'Read',
'api_included' => ['category', 'events'],
],
denormalizationContext: [
'groups' => ['trigger:write'],
'swagger_definition_name' => 'Write',
]
)]
class Trigger extends FormEntity implements UuidInterface
{
use UuidTrait;
use ProjectTrait;
public const ENTITY_NAME = 'point_trigger';
/**
* @var int
*/
#[Groups(['trigger:read'])]
private $id;
/**
* @var string
*/
#[Groups(['trigger:read', 'trigger:write'])]
private $name;
/**
* @var string|null
*/
#[Groups(['trigger:read', 'trigger:write'])]
private $description;
/**
* @var \DateTimeInterface
*/
#[Groups(['trigger:read', 'trigger:write'])]
private $publishUp;
/**
* @var \DateTimeInterface
*/
#[Groups(['trigger:read', 'trigger:write'])]
private $publishDown;
/**
* @var int
*/
#[Groups(['trigger:read', 'trigger:write'])]
private $points = 0;
/**
* @var string
*/
#[Groups(['trigger:read', 'trigger:write'])]
private $color = 'a0acb8';
/**
* @var bool
*/
#[Groups(['trigger:read', 'trigger:write'])]
private $triggerExistingLeads = false;
/**
* @var Category|null
**/
#[Groups(['trigger:read', 'trigger:write'])]
private $category;
/**
* @var ArrayCollection<int, TriggerEvent>
*/
#[Groups(['trigger:read', 'trigger:write'])]
private $events;
#[Groups(['trigger:read', 'trigger:write'])]
private ?Group $group = null;
public function __clone()
{
$this->id = null;
parent::__clone();
}
public function __construct()
{
$this->events = new ArrayCollection();
$this->initializeProjects();
}
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('point_triggers')
->setCustomRepositoryClass(TriggerRepository::class);
$builder->addIdColumns();
$builder->addPublishDates();
$builder->addField('points', 'integer');
$builder->createField('color', 'string')
->length(7)
->build();
$builder->createField('triggerExistingLeads', 'boolean')
->columnName('trigger_existing_leads')
->build();
$builder->addCategory();
$builder->createOneToMany('events', 'TriggerEvent')
->setIndexBy('id')
->setOrderBy(['order' => 'ASC'])
->mappedBy('trigger')
->cascadeAll()
->fetchExtraLazy()
->build();
$builder->createManyToOne('group', Group::class)
->addJoinColumn('group_id', 'id', true, false, 'CASCADE')
->build();
static::addUuidField($builder);
self::addProjectsField($builder, 'point_trigger_projects_xref', 'point_trigger_id');
}
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$metadata->addPropertyConstraint('name', new Assert\NotBlank([
'message' => 'mautic.core.name.required',
]));
}
/**
* Prepares the metadata for API usage.
*/
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
{
$metadata->setGroupPrefix('trigger')
->addListProperties(
[
'id',
'name',
'category',
'description',
]
)
->addProperties(
[
'publishUp',
'publishDown',
'points',
'color',
'events',
'triggerExistingLeads',
]
)
->build();
self::addProjectsInLoadApiMetadata($metadata, 'trigger');
}
/**
* @param string $prop
* @param mixed $val
*/
protected function isChanged($prop, $val)
{
if ('events' == $prop) {
// changes are already computed so just add them
$this->changes[$prop][$val[0]] = $val[1];
} else {
parent::isChanged($prop, $val);
}
}
/**
* Get id.
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Set description.
*
* @param string $description
*
* @return Trigger
*/
public function setDescription($description)
{
$this->isChanged('description', $description);
$this->description = $description;
return $this;
}
/**
* Get description.
*
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set name.
*
* @param string $name
*
* @return Trigger
*/
public function setName($name)
{
$this->isChanged('name', $name);
$this->name = $name;
return $this;
}
/**
* Get name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Add events.
*
* @return Point
*/
public function addTriggerEvent($key, TriggerEvent $event)
{
if ($changes = $event->getChanges()) {
$this->isChanged('events', [$key, $changes]);
}
$this->events[$key] = $event;
return $this;
}
/**
* Remove events.
*/
public function removeTriggerEvent(TriggerEvent $event): void
{
$this->events->removeElement($event);
}
/**
* Get events.
*
* @return \Doctrine\Common\Collections\Collection
*/
public function getEvents()
{
return $this->events;
}
/**
* Set publishUp.
*
* @param \DateTime $publishUp
*
* @return Point
*/
public function setPublishUp($publishUp)
{
$this->isChanged('publishUp', $publishUp);
$this->publishUp = $publishUp;
return $this;
}
/**
* Get publishUp.
*
* @return \DateTimeInterface
*/
public function getPublishUp()
{
return $this->publishUp;
}
/**
* Set publishDown.
*
* @param \DateTime $publishDown
*
* @return Point
*/
public function setPublishDown($publishDown)
{
$this->isChanged('publishDown', $publishDown);
$this->publishDown = $publishDown;
return $this;
}
/**
* Get publishDown.
*
* @return \DateTimeInterface
*/
public function getPublishDown()
{
return $this->publishDown;
}
/**
* @return mixed
*/
public function getPoints()
{
return $this->points;
}
/**
* @param mixed $points
*/
public function setPoints($points): void
{
$this->isChanged('points', $points);
$this->points = $points;
}
/**
* @return mixed
*/
public function getColor()
{
return $this->color;
}
/**
* @param mixed $color
*/
public function setColor($color): void
{
$this->color = $color;
}
/**
* @return mixed
*/
public function getTriggerExistingLeads()
{
return $this->triggerExistingLeads;
}
/**
* @param mixed $triggerExistingLeads
*/
public function setTriggerExistingLeads($triggerExistingLeads): void
{
$this->triggerExistingLeads = $triggerExistingLeads;
}
/**
* @return mixed
*/
public function getCategory()
{
return $this->category;
}
/**
* @param mixed $category
*/
public function setCategory($category): void
{
$this->category = $category;
}
public function getGroup(): ?Group
{
return $this->group;
}
public function setGroup(Group $group): void
{
$this->group = $group;
}
}

View File

@@ -0,0 +1,333 @@
<?php
namespace Mautic\PointBundle\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\UuidInterface;
use Mautic\CoreBundle\Entity\UuidTrait;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(security: "is_granted('point:triggers:viewown')"),
new Post(security: "is_granted('point:triggers:create')"),
new Get(security: "is_granted('point:triggers:viewown')"),
new Put(security: "is_granted('point:triggers:editown')"),
new Patch(security: "is_granted('point:triggers:editother')"),
new Delete(security: "is_granted('point:triggers:deleteown')"),
],
normalizationContext: [
'groups' => ['trigger_event:read'],
'swagger_definition_name' => 'Read',
],
denormalizationContext: [
'groups' => ['trigger_event:write'],
'swagger_definition_name' => 'Write',
]
)]
class TriggerEvent implements UuidInterface
{
use UuidTrait;
/**
* @var int|null
*/
#[Groups(['trigger_event:read'])]
private $id;
/**
* @var string
*/
#[Groups(['trigger_event:read', 'trigger_event:write'])]
private $name;
/**
* @var string|null
*/
#[Groups(['trigger_event:read', 'trigger_event:write'])]
private $description;
/**
* @var string
*/
#[Groups(['trigger_event:read', 'trigger_event:write'])]
private $type;
/**
* @var int
*/
#[Groups(['trigger_event:read', 'trigger_event:write'])]
private $order = 0;
/**
* @var array
*/
#[Groups(['trigger_event:read', 'trigger_event:write'])]
private $properties = [];
/**
* @var Trigger
*/
#[Groups(['trigger_event:read', 'trigger_event:write'])]
private $trigger;
/**
* @var ArrayCollection<int,LeadTriggerLog>
*/
private $log;
/**
* @var array
*/
private $changes;
public function __clone(): void
{
$this->id = null;
}
public function __construct()
{
$this->log = new ArrayCollection();
}
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('point_trigger_events')
->setCustomRepositoryClass(TriggerEventRepository::class)
->addIndex(['type'], 'trigger_type_search');
$builder->addIdColumns();
$builder->createField('type', 'string')
->length(50)
->build();
$builder->createField('order', 'integer')
->columnName('action_order')
->build();
$builder->addField('properties', 'array');
$builder->createManyToOne('trigger', 'Trigger')
->inversedBy('events')
->addJoinColumn('trigger_id', 'id', false, false, 'CASCADE')
->build();
$builder->createOneToMany('log', 'LeadTriggerLog')
->mappedBy('event')
->cascadePersist()
->cascadeRemove()
->fetchExtraLazy()
->build();
static::addUuidField($builder);
}
/**
* Prepares the metadata for API usage.
*/
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
{
$metadata->setGroupPrefix('trigger')
->addProperties(
[
'id',
'name',
'description',
'type',
'order',
'properties',
]
)
->build();
}
private function isChanged($prop, $val): void
{
if ($this->$prop != $val) {
$this->changes[$prop] = [$this->$prop, $val];
}
}
/**
* @return array
*/
public function getChanges()
{
return $this->changes;
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @param int $order
*
* @return TriggerEvent
*/
public function setOrder($order)
{
$this->isChanged('order', $order);
$this->order = $order;
return $this;
}
/**
* @return int
*/
public function getOrder()
{
return $this->order;
}
/**
* @param array $properties
*
* @return TriggerEvent
*/
public function setProperties($properties)
{
$this->isChanged('properties', $properties);
$this->properties = $properties;
return $this;
}
/**
* @return array
*/
public function getProperties()
{
return $this->properties;
}
/**
* @return self
*/
public function setTrigger(Trigger $trigger)
{
$this->trigger = $trigger;
return $this;
}
/**
* @return Trigger
*/
public function getTrigger()
{
return $this->trigger;
}
/**
* @param string $type
*
* @return TriggerEvent
*/
public function setType($type)
{
$this->isChanged('type', $type);
$this->type = $type;
return $this;
}
/**
* @return string
*/
public function getType()
{
return $this->type;
}
public function convertToArray(): array
{
return get_object_vars($this);
}
/**
* @param string $description
*
* @return TriggerEvent
*/
public function setDescription($description)
{
$this->isChanged('description', $description);
$this->description = $description;
return $this;
}
/**
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* @param string $name
*
* @return TriggerEvent
*/
public function setName($name)
{
$this->isChanged('name', $name);
$this->name = $name;
return $this;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return self
*/
public function addLog(LeadTriggerLog $log)
{
$this->log[] = $log;
return $this;
}
public function removeLog(LeadTriggerLog $log): void
{
$this->log->removeElement($log);
}
/**
* @return \Doctrine\Common\Collections\Collection
*/
public function getLog()
{
return $this->log;
}
}

View File

@@ -0,0 +1,149 @@
<?php
namespace Mautic\PointBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<TriggerEvent>
*/
class TriggerEventRepository extends CommonRepository
{
/**
* Get array of published triggers based on point total.
*
* @param int $points
*
* @return array
*/
public function getPublishedByPointTotal($points)
{
$q = $this->createQueryBuilder('a')
->select('partial a.{id, type, name, properties}, partial r.{id, name, points, color}')
->leftJoin('a.trigger', 'r')
->orderBy('a.order,r.points');
// make sure the published up and down dates are good
$expr = $this->getPublishedByDateExpression($q, 'r');
$expr->add(
$q->expr()->lte('r.points', (int) $points)
);
$q->where($expr);
$q->andWhere('r.group IS NULL');
return $q->getQuery()->getArrayResult();
}
/**
* @param ArrayCollection<int,GroupContactScore> $groupScores
*
* @return mixed[]
*/
public function getPublishedByGroupScore(Collection $groupScores)
{
if ($groupScores->isEmpty()) {
return [];
}
$q = $this->createQueryBuilder('a')
->select('partial a.{id, type, name, properties}, partial r.{id, name, points, color}, partial pl.{id, name}')
->leftJoin('a.trigger', 'r')
->leftJoin('r.group', 'pl')
->orderBy('a.order');
// make sure the published up and down dates are good
$expr = $this->getPublishedByDateExpression($q, 'r');
$groupsExpr = $q->expr()->orX();
/** @var GroupContactScore $score */
foreach ($groupScores as $score) {
$groupsExpr->add(
$q->expr()->andX(
$q->expr()->eq('pl.id', $score->getGroup()->getId()),
$q->expr()->lte('r.points', $score->getScore())
)
);
}
$q->where($expr);
$q->andWhere($groupsExpr);
$q->andWhere('r.group IS NOT NULL');
return $q->getQuery()->getArrayResult();
}
/**
* Get array of published actions based on type.
*
* @param string $type
*
* @return array
*/
public function getPublishedByType($type)
{
$q = $this->createQueryBuilder('e')
->select('partial e.{id, type, name, properties}, partial t.{id, name, points, color}')
->join('e.trigger', 't')
->orderBy('e.order');
// make sure the published up and down dates are good
$expr = $this->getPublishedByDateExpression($q);
$expr->add(
$q->expr()->eq('e.type', ':type')
);
$q->where($expr)
->setParameter('type', $type);
return $q->getQuery()->getResult();
}
/**
* @param int $leadId
*/
public function getLeadTriggeredEvents($leadId): array
{
$q = $this->_em->getConnection()->createQueryBuilder()
->select('e.*')
->from(MAUTIC_TABLE_PREFIX.'point_lead_event_log', 'x')
->innerJoin('x', MAUTIC_TABLE_PREFIX.'point_trigger_events', 'e', 'x.event_id = e.id')
->innerJoin('e', MAUTIC_TABLE_PREFIX.'point_triggers', 't', 'e.trigger_id = t.id');
// make sure the published up and down dates are good
$q->where($q->expr()->eq('x.lead_id', (int) $leadId));
$results = $q->executeQuery()->fetchAllAssociative();
$return = [];
foreach ($results as $r) {
$return[$r['id']] = $r;
}
return $return;
}
/**
* @param int $eventId
*/
public function getLeadsForEvent($eventId): array
{
$results = $this->_em->getConnection()->createQueryBuilder()
->select('e.lead_id')
->from(MAUTIC_TABLE_PREFIX.'point_lead_event_log', 'e')
->where('e.event_id = '.(int) $eventId)
->executeQuery()
->fetchAllAssociative();
$return = [];
foreach ($results as $r) {
$return[] = $r['lead_id'];
}
return $return;
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Mautic\PointBundle\Entity;
use Doctrine\Common\Collections\Order;
use Mautic\CoreBundle\Entity\CommonRepository;
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
/**
* @extends CommonRepository<Trigger>
*/
class TriggerRepository extends CommonRepository
{
use ProjectRepositoryTrait;
public function getEntities(array $args = [])
{
$q = $this->_em
->createQueryBuilder()
->select($this->getTableAlias().', cat')
->from(Trigger::class, $this->getTableAlias())
->leftJoin($this->getTableAlias().'.category', 'cat')
->leftJoin($this->getTableAlias().'.group', 'pl');
$args['qb'] = $q;
return parent::getEntities($args);
}
/**
* Get a list of published triggers with color and points.
*
* @return array
*/
public function getTriggerColors()
{
$q = $this->_em->createQueryBuilder()
->select('partial t.{id, color, points}')
->from(Trigger::class, 't', 't.id');
$q->where($this->getPublishedByDateExpression($q));
$q->orderBy('t.points', Order::Ascending->value);
return $q->getQuery()->getArrayResult();
}
public function getTableAlias(): string
{
return 't';
}
protected function addCatchAllWhereClause($q, $filter): array
{
return $this->addStandardCatchAllWhereClause($q, $filter, [
't.name',
't.description',
]);
}
protected function addSearchCommandWhereClause($q, $filter): array
{
return match ($filter->command) {
$this->translator->trans('mautic.project.searchcommand.name'), $this->translator->trans('mautic.project.searchcommand.name', [], null, 'en_US') => $this->handleProjectFilter(
$this->_em->getConnection()->createQueryBuilder(),
'point_trigger_id',
'point_trigger_projects_xref',
$this->getTableAlias(),
$filter->string,
$filter->not
),
// Handle standard search commands
default => $this->addStandardSearchCommandWhereClause($q, $filter),
};
}
/**
* @return string[]
*/
public function getSearchCommands(): array
{
return array_merge(['mautic.project.searchcommand.name'], $this->getStandardSearchCommands());
}
}