Files
CloudOps/docker-compose/mautic-setup/mautic-backup-files/docroot/app/bundles/CampaignBundle/Entity/Campaign.php

751 lines
20 KiB
PHP
Executable File

<?php
namespace Mautic\CampaignBundle\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\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\CampaignBundle\Validator\Constraints\NoOrphanEvents;
use Mautic\CategoryBundle\Entity\Category;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\FormEntity;
use Mautic\CoreBundle\Entity\OptimisticLockInterface;
use Mautic\CoreBundle\Entity\OptimisticLockTrait;
use Mautic\CoreBundle\Entity\PublishStatusIconAttributesInterface;
use Mautic\CoreBundle\Entity\UuidInterface;
use Mautic\CoreBundle\Entity\UuidTrait;
use Mautic\FormBundle\Entity\Form;
use Mautic\LeadBundle\Entity\Lead as Contact;
use Mautic\LeadBundle\Entity\LeadList;
use Mautic\ProjectBundle\Entity\Project;
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('campaign:campaigns:viewown')"),
new Post(security: "is_granted('campaign:campaigns:create')"),
new Get(security: "is_granted('campaign:campaigns:viewown')"),
new Put(security: "is_granted('campaign:campaigns:editown')"),
new Patch(security: "is_granted('campaign:campaigns:editother')"),
new Delete(security: "is_granted('campaign:campaigns:deleteown')"),
],
normalizationContext: [
'groups' => ['campaign:read'],
'swagger_definition_name' => 'Read',
'api_included' => ['category', 'events', 'lists', 'forms', 'fields', 'actions'],
],
denormalizationContext: [
'groups' => ['campaign:write'],
'swagger_definition_name' => 'Write',
]
)]
class Campaign extends FormEntity implements PublishStatusIconAttributesInterface, OptimisticLockInterface, UuidInterface
{
use UuidTrait;
use OptimisticLockTrait;
use ProjectTrait;
public const TABLE_NAME = 'campaigns';
public const ENTITY_NAME = 'campaign';
/**
* @var int
*/
#[Groups(['campaign:read', 'campaign:write'])]
private $id;
/**
* @var string|null
*/
#[Groups(['campaign:read', 'campaign:write'])]
private $name;
/**
* @var string|null
*/
#[Groups(['campaign:read', 'campaign:write'])]
private $description;
/**
* @var \DateTimeInterface|null
*/
#[Groups(['campaign:read', 'campaign:write'])]
private $publishUp;
/**
* @var \DateTimeInterface|null
*/
#[Groups(['campaign:read', 'campaign:write'])]
private $publishDown;
#[Groups(['campaign:read', 'campaign:write'])]
public ?\DateTimeInterface $deleted = null;
/**
* @var Category|null
**/
#[Groups(['campaign:read', 'campaign:write'])]
private $category;
/**
* @var Collection<int, Event>|ArrayCollection<int, Event>
*/
#[Groups(['campaign:read', 'campaign:write'])]
private $events;
/**
* @var ArrayCollection<int, Lead>
*/
#[Groups(['campaign:read', 'campaign:write'])]
private Collection $leads;
/**
* @var Collection<int, LeadList>
*/
#[Groups(['campaign:read', 'campaign:write'])]
private Collection $lists;
/**
* @var Collection<int, Form>
*/
#[Groups(['campaign:read', 'campaign:write'])]
private Collection $forms;
#[Groups(['campaign:read', 'campaign:write'])]
private array $canvasSettings = [];
#[Groups(['campaign:read', 'campaign:write'])]
private bool $allowRestart = false;
public function __construct()
{
$this->events = new ArrayCollection();
$this->leads = new ArrayCollection();
$this->lists = new ArrayCollection();
$this->forms = new ArrayCollection();
$this->initializeProjects();
}
public function __clone()
{
$this->leads = new ArrayCollection();
$this->events = new ArrayCollection();
$this->lists = new ArrayCollection();
$this->forms = new ArrayCollection();
$this->id = null;
parent::__clone();
}
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable(self::TABLE_NAME)
->setCustomRepositoryClass(CampaignRepository::class);
$builder->addIdColumns();
$builder->addPublishDates();
$builder->addCategory();
$builder->createOneToMany('events', Event::class)
->setIndexBy('id')
->setOrderBy(['order' => 'ASC'])
->mappedBy('campaign')
->cascadeAll()
->fetchExtraLazy()
->build();
$builder->createOneToMany('leads', Lead::class)
->mappedBy('campaign')
->fetchExtraLazy()
->build();
$builder->createManyToMany('lists', LeadList::class)
->setJoinTable('campaign_leadlist_xref')
->setIndexBy('id')
->addInverseJoinColumn('leadlist_id', 'id', false, false, 'CASCADE')
->addJoinColumn('campaign_id', 'id', true, false, 'CASCADE')
->build();
$builder->createManyToMany('forms', Form::class)
->setJoinTable('campaign_form_xref')
->setIndexBy('id')
->addInverseJoinColumn('form_id', 'id', false, false, 'CASCADE')
->addJoinColumn('campaign_id', 'id', true, false, 'CASCADE')
->build();
$builder->createField('canvasSettings', 'array')
->columnName('canvas_settings')
->nullable()
->build();
$builder->addNamedField('allowRestart', 'boolean', 'allow_restart');
$builder->addNullableField('deleted', 'datetime');
self::addVersionField($builder);
static::addUuidField($builder);
self::addProjectsField($builder, 'campaign_projects_xref', 'campaign_id');
}
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$metadata->addPropertyConstraint(
'name',
new Assert\NotBlank(
[
'message' => 'mautic.core.name.required',
]
)
);
$metadata->addConstraint(new NoOrphanEvents());
}
/**
* Prepares the metadata for API usage.
*/
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
{
$metadata->setGroupPrefix('campaign')
->addListProperties(
[
'id',
'name',
'category',
'description',
]
)
->addProperties(
[
'allowRestart',
'publishUp',
'publishDown',
'events',
'forms',
'lists', // @deprecated, will be renamed to 'segments' in 3.0.0
'canvasSettings',
]
)
->setGroupPrefix('campaignBasic')
->addListProperties(
[
'id',
'name',
'description',
'allowRestart',
'events',
'publishUp',
'publishDown',
'deleted',
]
)
->build();
self::addProjectsInLoadApiMetadata($metadata, 'campaign');
}
public function convertToArray(): array
{
return get_object_vars($this);
}
/**
* @param string $prop
* @param mixed $val
*/
protected function isChanged($prop, $val)
{
$getter = 'get'.ucfirst($prop);
$current = $this->$getter();
if ('category' == $prop) {
$currentId = ($current) ? $current->getId() : '';
$newId = ($val) ? $val->getId() : null;
if ($currentId != $newId) {
$this->changes[$prop] = [$currentId, $newId];
}
} elseif ('projects' === $prop) {
// Initialize project tracking on first change
if (!isset($this->changes['projects']['old'])) {
$currentProjects = array_map(fn ($project) => $project->getName(), iterator_to_array($current));
$this->changes['projects'] = [
'old' => $currentProjects,
'new' => $currentProjects,
];
}
// Update the new state based on the operation
if ($val instanceof Project) {
// Add project if not already in the list
$projectName = $val->getName();
if (!in_array($projectName, $this->changes['projects']['new'], true)) {
$this->changes['projects']['new'][] = $projectName;
}
} else {
// Remove project from the list
$this->changes['projects']['new'] = array_values(
array_diff($this->changes['projects']['new'], [$val])
);
}
} else {
parent::isChanged($prop, $val);
}
}
public function getId(): ?int
{
return $this->id;
}
/**
* Override to convert projects changes to final format.
*/
public function getChanges($includePast = false)
{
$changes = parent::getChanges($includePast);
// Convert projects format if it exists and is in the intermediate format
if (isset($changes['projects']['old']) && isset($changes['projects']['new'])) {
$changes['projects'] = [
implode(', ', $changes['projects']['old']),
implode(', ', $changes['projects']['new']),
];
}
return $changes;
}
/**
* @param string $description
*
* @return Campaign
*/
public function setDescription($description)
{
$this->isChanged('description', $description);
$this->description = $description;
return $this;
}
/**
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* @return Campaign
*/
public function setName(string $name)
{
$this->isChanged('name', $name);
$this->name = $name;
return $this;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Calls $this->addEvent on every item in the collection.
*
* @return Campaign
*/
public function addEvents(array $events)
{
foreach ($events as $id => $event) {
$this->addEvent($id, $event);
}
return $this;
}
/**
* @return Campaign
*/
public function addEvent($key, Event $event)
{
if ($changes = $event->getChanges()) {
$this->changes['events']['added'][$key] = [$key, $changes];
}
$this->events[$key] = $event;
return $this;
}
public function removeEvent(Event $event): void
{
$this->changes['events']['removed'][$event->getId()] = $event->getName();
$this->events->removeElement($event);
}
/**
* @return ArrayCollection<int, Event>
*/
public function getEvents()
{
return $this->events;
}
public function getRootEvents(): ArrayCollection
{
$criteria = Criteria::create()->where(
Criteria::expr()->andX(
Criteria::expr()->isNull('parent'),
Criteria::expr()->isNull('deleted')
)
);
$events = $this->getEvents()->matching($criteria);
return $this->reindexEventsByIdKey($events);
}
public function getInactionBasedEvents(): ArrayCollection
{
$criteria = Criteria::create()->where(Criteria::expr()->eq('decisionPath', Event::PATH_INACTION));
$events = $this->getEvents()->matching($criteria);
return $this->reindexEventsByIdKey($events);
}
/**
* @param string $type
*
* @return ArrayCollection<int,Event>
*/
public function getEventsByType($type): ArrayCollection
{
$criteria = Criteria::create()->where(Criteria::expr()->eq('eventType', $type));
$events = $this->getEvents()->matching($criteria);
return $this->reindexEventsByIdKey($events);
}
/**
* @return ArrayCollection<int, Event>
*/
public function getEmailSendEvents(): ArrayCollection
{
$criteria = Criteria::create()->where(Criteria::expr()->eq('type', 'email.send'));
$events = $this->getEvents()->matching($criteria);
// Doctrine loses the indexBy mapping definition when using matching so we have to manually reset them.
// @see https://github.com/doctrine/doctrine2/issues/4693
$keyedArrayCollection = new ArrayCollection();
/** @var Event $event */
foreach ($events as $event) {
$keyedArrayCollection->set($event->getId(), $event);
}
return $keyedArrayCollection;
}
public function isEmailCampaign(): bool
{
$criteria = Criteria::create()->where(Criteria::expr()->eq('type', 'email.send'))->setMaxResults(1);
$emailEvent = $this->getEvents()->matching($criteria);
return !$emailEvent->isEmpty();
}
/**
* @param ?\DateTime $publishUp
*
* @return Campaign
*/
public function setPublishUp($publishUp)
{
$this->isChanged('publishUp', $publishUp);
$this->publishUp = $publishUp;
return $this;
}
/**
* @return \DateTimeInterface|null
*/
public function getPublishUp()
{
return $this->publishUp;
}
/**
* @param ?\DateTime $publishDown
*
* @return Campaign
*/
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->isChanged('category', $category);
$this->category = $category;
}
/**
* @return Campaign
*/
public function addLead($key, Lead $lead)
{
$action = ($this->leads->contains($lead)) ? 'updated' : 'added';
$leadEntity = $lead->getLead();
$this->changes['leads'][$action][$leadEntity->getId()] = $leadEntity->getPrimaryIdentifier();
$this->leads[$key] = $lead;
return $this;
}
public function removeLead(Lead $lead): void
{
$leadEntity = $lead->getLead();
$this->changes['leads']['removed'][$leadEntity->getId()] = $leadEntity->getPrimaryIdentifier();
$this->leads->removeElement($lead);
}
/**
* @return Lead[]|Collection
*/
public function getLeads()
{
return $this->leads;
}
/**
* @return ArrayCollection<int, LeadList>
*/
public function getLists()
{
return $this->lists;
}
/**
* @return Campaign
*/
public function addList(LeadList $list)
{
$this->lists[$list->getId()] = $list;
$this->changes['lists']['added'][$list->getId()] = $list->getName();
return $this;
}
public function removeList(LeadList $list): void
{
$this->changes['lists']['removed'][$list->getId()] = $list->getName();
$this->lists->removeElement($list);
}
/**
* @return ArrayCollection<int, Form>
*/
public function getForms()
{
return $this->forms;
}
/**
* @return Campaign
*/
public function addForm(Form $form)
{
$this->forms[$form->getId()] = $form;
$this->changes['forms']['added'][$form->getId()] = $form->getName();
return $this;
}
public function removeForm(Form $form): void
{
$this->changes['forms']['removed'][$form->getId()] = $form->getName();
$this->forms->removeElement($form);
}
/**
* @return mixed
*/
public function getCanvasSettings()
{
return $this->canvasSettings;
}
public function setCanvasSettings(array $canvasSettings): void
{
$this->canvasSettings = $canvasSettings;
}
/**
* Check if there are any orphan events that are not connected to any parent node.
*/
public function hasOrphanEvents(): bool
{
$canvasSettings = $this->getCanvasSettings() ?? [];
if (empty($canvasSettings['nodes'])) {
return false;
}
// Extract event IDs from canvas nodes (excludes 'lists', 'forms' and other non-event nodes)
$eventIds = array_filter(
array_column($canvasSettings['nodes'], 'id'),
fn ($id) => !in_array($id, ['lists', 'forms'])
);
if (empty($eventIds)) {
return false;
}
// Extract connected event IDs from connections
$connectedEventIds = [];
if (!empty($canvasSettings['connections'])) {
$connectedEventIds = array_filter(array_column($canvasSettings['connections'], 'targetId'));
}
return !empty(array_diff($eventIds, $connectedEventIds));
}
public function getAllowRestart(): bool
{
return (bool) $this->allowRestart;
}
public function allowRestart(): bool
{
return $this->getAllowRestart();
}
/**
* @param bool $allowRestart
*
* @return Campaign
*/
public function setAllowRestart($allowRestart)
{
$allowRestart = (bool) $allowRestart;
$this->isChanged('allowRestart', $allowRestart);
$this->allowRestart = $allowRestart;
return $this;
}
public function setDeleted(?\DateTimeInterface $deleted): void
{
$this->isChanged('deleted', $deleted);
$this->deleted = $deleted;
}
public function isDeleted(): bool
{
return !is_null($this->deleted);
}
/**
* Get contact membership.
*/
public function getContactMembership(Contact $contact): Collection
{
return $this->leads->matching(
Criteria::create()
->where(Criteria::expr()->eq('lead', $contact))
->orderBy(['dateAdded' => Order::Descending->value])
);
}
public function getOnclickMethod(): string
{
return 'Mautic.confirmationCampaignPublishStatus(mQuery(this));';
}
public function getDataAttributes(): array
{
return [
'data-toggle' => 'confirmation',
'data-confirm-callback' => 'confirmCallbackCampaignPublishStatus',
'data-cancel-callback' => 'dismissConfirmation',
];
}
public function getTranslationKeysDataAttributes(): array
{
return [
'data-message' => 'mautic.campaign.form.confirmation.message',
'data-confirm-text' => 'mautic.campaign.form.confirmation.confirm_text',
'data-cancel-text' => 'mautic.campaign.form.confirmation.cancel_text',
];
}
/**
* Re-index collection by event ID to work around Doctrine's indexBy mapping issue.
*
* @see https://github.com/doctrine/doctrine2/issues/4693
*/
private function reindexEventsByIdKey(Collection $events): ArrayCollection
{
// Doctrine loses the indexBy mapping definition when using matching so we have to manually reset them.
// @see https://github.com/doctrine/doctrine2/issues/4693
$keyedArrayCollection = new ArrayCollection();
/** @var Event $event */
foreach ($events as $event) {
$keyedArrayCollection->set($event->getId(), $event);
}
unset($events);
return $keyedArrayCollection;
}
}