Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,626 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\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\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\LeadBundle\Form\Validator\Constraints\UniqueCustomField;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\ProjectBundle\Entity\ProjectTrait;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Companies',
|
||||
operations: [
|
||||
new GetCollection(uriTemplate: '/companies', security: "is_granted('lead:leads:viewown')"),
|
||||
new Post(uriTemplate: '/companies', security: "is_granted('lead:leads:create')"),
|
||||
new Get(uriTemplate: '/companies/{id}', security: "is_granted('lead:leads:viewown')"),
|
||||
new Put(uriTemplate: '/companies/{id}', security: "is_granted('lead:leads:editown')"),
|
||||
new Patch(uriTemplate: '/companies/{id}', security: "is_granted('lead:leads:editother')"),
|
||||
new Delete(uriTemplate: '/companies/{id}', security: "is_granted('lead:leads:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['company:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['company:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class Company extends FormEntity implements CustomFieldEntityInterface, IdentifierFieldEntityInterface
|
||||
{
|
||||
use CustomFieldEntityTrait;
|
||||
use ProjectTrait;
|
||||
|
||||
public const FIELD_ALIAS = 'company';
|
||||
public const TABLE_NAME = 'companies';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['company:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $score = 0;
|
||||
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private ?User $owner = null;
|
||||
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $socialCache = [];
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $email;
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $address1;
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $address2;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $phone;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $city;
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $state;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $zipcode;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $country;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $website;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $industry;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $description;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->initializeProjects();
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getSocialCache()
|
||||
{
|
||||
return $this->socialCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $cache
|
||||
*/
|
||||
public function setSocialCache($cache): void
|
||||
{
|
||||
$this->socialCache = $cache;
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(CompanyRepository::class);
|
||||
|
||||
$builder->createField('id', 'integer')
|
||||
->makePrimaryKey()
|
||||
->generatedValue()
|
||||
->build();
|
||||
|
||||
$builder->createField('socialCache', 'array')
|
||||
->columnName('social_cache')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('owner', User::class)
|
||||
->cascadeMerge()
|
||||
->addJoinColumn('owner_id', 'id', true, false, 'SET NULL')
|
||||
->build();
|
||||
|
||||
$builder->createField('score', 'integer')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
self::loadFixedFieldMetadata(
|
||||
$builder,
|
||||
[
|
||||
'email',
|
||||
'address1',
|
||||
'address2',
|
||||
'phone',
|
||||
'city',
|
||||
'state',
|
||||
'zipcode',
|
||||
'country',
|
||||
'name',
|
||||
'website',
|
||||
'industry',
|
||||
'description',
|
||||
],
|
||||
FieldModel::$coreCompanyFields
|
||||
);
|
||||
|
||||
self::addProjectsField($builder, 'company_projects_xref', 'company_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('companyBasic')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'name',
|
||||
'email',
|
||||
'address1',
|
||||
'address2',
|
||||
'phone',
|
||||
'city',
|
||||
'state',
|
||||
'zipcode',
|
||||
'country',
|
||||
'website',
|
||||
'industry',
|
||||
'description',
|
||||
'score',
|
||||
]
|
||||
)
|
||||
->setGroupPrefix('company')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'fields',
|
||||
'score',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
|
||||
self::addProjectsInLoadApiMetadata($metadata, 'company');
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addConstraint(new UniqueCustomField(['object' => 'company']));
|
||||
$metadata->addPropertyConstraint('score', new Assert\Range([
|
||||
'min' => 0,
|
||||
'max' => 2147483647,
|
||||
]));
|
||||
}
|
||||
|
||||
public static function getDefaultIdentifierFields(): array
|
||||
{
|
||||
return [
|
||||
'companyname',
|
||||
'companyemail',
|
||||
'companywebsite',
|
||||
'city',
|
||||
'state',
|
||||
'country',
|
||||
];
|
||||
}
|
||||
|
||||
protected function isChanged($prop, $val)
|
||||
{
|
||||
$prefix = 'company';
|
||||
|
||||
if (str_starts_with($prop, $prefix)) {
|
||||
$getter = 'get'.ucfirst(substr($prop, strlen($prefix)));
|
||||
$current = $this->$getter();
|
||||
if ($current !== $val) {
|
||||
$this->addChange($prop, [$current, $val]);
|
||||
}
|
||||
} elseif ('owner' === $prop) {
|
||||
$current = $this->getOwner();
|
||||
if ($current && !$val) {
|
||||
$this->changes['owner'] = [$current->getName().' ('.$current->getId().')', $val];
|
||||
} elseif (!$current && $val) {
|
||||
$this->changes['owner'] = [$current, $val->getName().' ('.$val->getId().')'];
|
||||
} elseif ($current && $current->getId() != $val->getId()) {
|
||||
$this->changes['owner'] = [
|
||||
$current->getName().'('.$current->getId().')',
|
||||
$val->getName().'('.$val->getId().')',
|
||||
];
|
||||
}
|
||||
} else {
|
||||
parent::isChanged($prop, $val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary identifier for the company.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPrimaryIdentifier()
|
||||
{
|
||||
if ($name = $this->getName()) {
|
||||
return $name;
|
||||
} elseif (!empty($this->fields['core']['companyemail']['value'])) {
|
||||
return $this->fields['core']['companyemail']['value'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Company
|
||||
*/
|
||||
public function setOwner(?User $owner = null)
|
||||
{
|
||||
$this->isChanged('owner', $owner);
|
||||
$this->owner = $owner;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOwner(): ?User
|
||||
{
|
||||
return $this->owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user to be used for permissions.
|
||||
*
|
||||
* @return User|int
|
||||
*/
|
||||
public function getPermissionUser()
|
||||
{
|
||||
return $this->getOwner() ?? $this->getCreatedBy();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $score
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setScore($score)
|
||||
{
|
||||
$score = (int) $score;
|
||||
|
||||
$this->isChanged('score', $score);
|
||||
$this->score = $score;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getScore()
|
||||
{
|
||||
return $this->score;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->isChanged('companyname', $name);
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getEmail()
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $email
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setEmail($email)
|
||||
{
|
||||
$this->isChanged('companyemail', $email);
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAddress1()
|
||||
{
|
||||
return $this->address1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $address1
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setAddress1($address1)
|
||||
{
|
||||
$this->isChanged('companyaddress1', $address1);
|
||||
$this->address1 = $address1;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAddress2()
|
||||
{
|
||||
return $this->address2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $address2
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setAddress2($address2)
|
||||
{
|
||||
$this->isChanged('companyaddress2', $address2);
|
||||
$this->address2 = $address2;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPhone()
|
||||
{
|
||||
return $this->phone;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $phone
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setPhone($phone)
|
||||
{
|
||||
$this->isChanged('companyphone', $phone);
|
||||
$this->phone = $phone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCity()
|
||||
{
|
||||
return $this->city;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $city
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setCity($city)
|
||||
{
|
||||
$this->isChanged('companycity', $city);
|
||||
$this->city = $city;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getState()
|
||||
{
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $state
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setState($state)
|
||||
{
|
||||
$this->isChanged('companystate', $state);
|
||||
$this->state = $state;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getZipcode()
|
||||
{
|
||||
return $this->zipcode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $zipcode
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setZipcode($zipcode)
|
||||
{
|
||||
$this->isChanged('companyzipcode', $zipcode);
|
||||
$this->zipcode = $zipcode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCountry()
|
||||
{
|
||||
return $this->country;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $country
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setCountry($country)
|
||||
{
|
||||
$this->isChanged('companycountry', $country);
|
||||
$this->country = $country;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getWebsite()
|
||||
{
|
||||
return $this->website;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $website
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setWebsite($website)
|
||||
{
|
||||
$this->isChanged('companywebsite', $website);
|
||||
$this->website = $website;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getIndustry()
|
||||
{
|
||||
return $this->industry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $industry
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setIndustry($industry)
|
||||
{
|
||||
$this->isChanged('companyindustry', $industry);
|
||||
$this->industry = $industry;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $description
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->isChanged('companydescription', $description);
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class CompanyChangeLog
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $eventName;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $actionName;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $company;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_companies_change_log')
|
||||
->setCustomRepositoryClass(CompanyChangeLogRepository::class)
|
||||
->addIndex(['date_added'], 'company_date_added');
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', false, 'companyChangeLog');
|
||||
|
||||
$builder->createField('type', 'text')
|
||||
->length(50)
|
||||
->build();
|
||||
|
||||
$builder->createField('eventName', 'string')
|
||||
->columnName('event_name')
|
||||
->build();
|
||||
|
||||
$builder->createField('actionName', 'string')
|
||||
->columnName('action_name')
|
||||
->build();
|
||||
|
||||
$builder->createField('company', 'integer')
|
||||
->columnName('company_id')
|
||||
->build();
|
||||
|
||||
$builder->addDateAdded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set type.
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return CompanyChangeLog
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set eventName.
|
||||
*
|
||||
* @param string $eventName
|
||||
*
|
||||
* @return CompanyChangeLog
|
||||
*/
|
||||
public function setEventName($eventName)
|
||||
{
|
||||
$this->eventName = $eventName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get eventName.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEventName()
|
||||
{
|
||||
return $this->eventName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set actionName.
|
||||
*
|
||||
* @param string $actionName
|
||||
*
|
||||
* @return CompanyChangeLog
|
||||
*/
|
||||
public function setActionName($actionName)
|
||||
{
|
||||
$this->actionName = $actionName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actionName.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getActionName()
|
||||
{
|
||||
return $this->actionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set delta.
|
||||
*
|
||||
* @param int $company
|
||||
*
|
||||
* @return CompanyChangeLog
|
||||
*/
|
||||
public function setCompany($company)
|
||||
{
|
||||
$this->company = $company;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get company.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCompany()
|
||||
{
|
||||
return $this->company;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set dateAdded.
|
||||
*
|
||||
* @param \DateTime $dateAdded
|
||||
*
|
||||
* @return CompanyChangeLog
|
||||
*/
|
||||
public function setDateAdded($dateAdded)
|
||||
{
|
||||
$this->dateAdded = $dateAdded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dateAdded.
|
||||
*
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lead.
|
||||
*
|
||||
* @return CompanyChangeLog
|
||||
*/
|
||||
public function setLead(Lead $lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lead.
|
||||
*
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<CompanyChangeLog>
|
||||
*/
|
||||
class CompanyChangeLogRepository extends CommonRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class CompanyLead
|
||||
{
|
||||
/**
|
||||
* @var Company
|
||||
**/
|
||||
private $company;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private $primary = false;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('companies_leads')
|
||||
->setCustomRepositoryClass(CompanyLeadRepository::class);
|
||||
|
||||
$builder->createManyToOne('company', 'Company')
|
||||
->makePrimaryKey()
|
||||
->addJoinColumn('company_id', 'id', false, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', true);
|
||||
|
||||
$builder->addDateAdded();
|
||||
|
||||
$builder->createField('primary', 'boolean')
|
||||
->columnName('is_primary')
|
||||
->nullable()
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $date
|
||||
*/
|
||||
public function setDateAdded($date): void
|
||||
{
|
||||
$this->dateAdded = $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $lead
|
||||
*/
|
||||
public function setLead($lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Company
|
||||
*/
|
||||
public function getCompany()
|
||||
{
|
||||
return $this->company;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Company
|
||||
*/
|
||||
public function getCompanies()
|
||||
{
|
||||
return $this->company;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Company $company
|
||||
*/
|
||||
public function setCompany($company): void
|
||||
{
|
||||
$this->company = $company;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $primary
|
||||
*/
|
||||
public function setPrimary($primary): void
|
||||
{
|
||||
$this->primary = $primary;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getPrimary()
|
||||
{
|
||||
return $this->primary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\LeadBundle\Exception\PrimaryCompanyNotFoundException;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<CompanyLead>
|
||||
*/
|
||||
class CompanyLeadRepository extends CommonRepository
|
||||
{
|
||||
public const DELETE_BATCH_SIZE = 1000;
|
||||
|
||||
/**
|
||||
* @param CompanyLead[] $entities
|
||||
*/
|
||||
public function saveEntities($entities, $new = true): void
|
||||
{
|
||||
// Get a list of contacts and set primary to 0
|
||||
if ($new) {
|
||||
$contacts = [];
|
||||
$contactId = null;
|
||||
foreach ($entities as $entity) {
|
||||
$contactId = $entity->getLead()->getId();
|
||||
if (!isset($contacts[$contactId])) {
|
||||
// Set one company from the batch as as primary
|
||||
$entity->setPrimary(true);
|
||||
}
|
||||
|
||||
$contacts[$contactId] = $contactId;
|
||||
}
|
||||
|
||||
if ($contactId) {
|
||||
// Only one company should be set as primary so reset all in order to let the entity update the one
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->update(MAUTIC_TABLE_PREFIX.'companies_leads')
|
||||
->set('is_primary', 0);
|
||||
|
||||
$qb->where(
|
||||
$qb->expr()->in('lead_id', $contacts)
|
||||
)->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
parent::saveEntities($entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get companies by leadId.
|
||||
*/
|
||||
public function getCompaniesByLeadId($leadId, $companyId = null, ?bool $onlyPrimary = null): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('cl.company_id, cl.date_added as date_associated, cl.is_primary, comp.*')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl')
|
||||
->join('cl', MAUTIC_TABLE_PREFIX.'companies', 'comp', 'comp.id = cl.company_id')
|
||||
->where('cl.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
|
||||
if ($companyId) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('cl.company_id', ':companyId')
|
||||
)->setParameter('companyId', $companyId);
|
||||
}
|
||||
|
||||
if ($onlyPrimary) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('cl.is_primary', true)
|
||||
);
|
||||
}
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*
|
||||
* @throws PrimaryCompanyNotFoundException
|
||||
*/
|
||||
public function getPrimaryCompanyByLeadId(int $leadId): array
|
||||
{
|
||||
$companies = $this->getCompaniesByLeadId($leadId);
|
||||
foreach ($companies as $company) {
|
||||
if ($company['is_primary']) {
|
||||
return $company;
|
||||
}
|
||||
}
|
||||
|
||||
throw new PrimaryCompanyNotFoundException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getCompanyIdsByLeadId(string $leadId): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('cl.company_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl')
|
||||
->where('cl.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
|
||||
return array_map(
|
||||
fn (array $company) => (string) $company['company_id'],
|
||||
$q->executeQuery()->fetchAllAssociative()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $companyId
|
||||
*/
|
||||
public function getCompanyLeads($companyId): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('cl.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl');
|
||||
|
||||
$q->where($q->expr()->eq('cl.company_id', ':company'))
|
||||
->setParameter('company', $companyId);
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getLatestCompanyForLead($leadId)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('cl.company_id, comp.companyname, comp.companycity, comp.companycountry')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl')
|
||||
->join('cl', MAUTIC_TABLE_PREFIX.'companies', 'comp', 'comp.id = cl.company_id')
|
||||
->where('cl.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
$q->orderBy('cl.date_added', 'DESC');
|
||||
|
||||
$result = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
return !empty($result) ? $result[0] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getCompanyLeadEntity($leadId, $companyId): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$qb->select('cl.is_primary, cl.lead_id, cl.company_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl')
|
||||
->where(
|
||||
$qb->expr()->eq('cl.lead_id', ':leadId'),
|
||||
$qb->expr()->eq('cl.company_id', ':companyId')
|
||||
)->setParameter('leadId', $leadId)
|
||||
->setParameter('companyId', $companyId);
|
||||
|
||||
return $qb->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getEntitiesByLead(Lead $lead)
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('cl')
|
||||
->from(CompanyLead::class, 'cl')
|
||||
->where(
|
||||
$qb->expr()->eq('cl.lead', ':lead')
|
||||
)->setParameter('lead', $lead);
|
||||
|
||||
return $qb->getQuery()->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates leads company name If company name changed and company is primary.
|
||||
*/
|
||||
public function updateLeadsPrimaryCompanyName(Company $company): void
|
||||
{
|
||||
if ($company->isNew() || empty($company->getChanges()['fields']['companyname'])) {
|
||||
return;
|
||||
}
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q->select('cl.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl');
|
||||
$q->where($q->expr()->eq('cl.company_id', ':companyId'))
|
||||
->setParameter('companyId', $company->getId())
|
||||
->andWhere('cl.is_primary = 1');
|
||||
$leadIds = $q->executeQuery()->fetchOne();
|
||||
if (!empty($leadIds)) {
|
||||
$this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->update(MAUTIC_TABLE_PREFIX.'leads')
|
||||
->set('company', ':company')
|
||||
->setParameter('company', $company->getName())
|
||||
->where(
|
||||
$q->expr()->in('id', $leadIds)
|
||||
)->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
public function removeContactPrimaryCompany(int $leadId): void
|
||||
{
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->delete(MAUTIC_TABLE_PREFIX.'companies_leads');
|
||||
$qb->where(
|
||||
$qb->expr()->eq('lead_id', $leadId)
|
||||
)->andWhere(
|
||||
$qb->expr()->eq('is_primary', 1)
|
||||
)->executeStatement();
|
||||
}
|
||||
|
||||
public function removeAllSecondaryCompanies(): void
|
||||
{
|
||||
$conn = $this->getEntityManager()->getConnection();
|
||||
do {
|
||||
$sql = 'DELETE FROM '.MAUTIC_TABLE_PREFIX.'companies_leads WHERE is_primary = 0 LIMIT '.self::DELETE_BATCH_SIZE;
|
||||
$row = $conn->executeStatement($sql);
|
||||
} while ($row);
|
||||
}
|
||||
|
||||
public function removeContactSecondaryCompanies(int $leadId): void
|
||||
{
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->delete(MAUTIC_TABLE_PREFIX.'companies_leads');
|
||||
$qb->where(
|
||||
$qb->expr()->eq('lead_id', $leadId)
|
||||
)->andWhere(
|
||||
$qb->expr()->eq('is_primary', 0)
|
||||
)->executeStatement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\LeadBundle\Event\CompanyBuildSearchEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Company>
|
||||
*/
|
||||
class CompanyRepository extends CommonRepository implements CustomFieldRepositoryInterface
|
||||
{
|
||||
use CustomFieldRepositoryTrait;
|
||||
use ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $availableSearchFields = [];
|
||||
|
||||
/**
|
||||
* @var EventDispatcherInterface|null
|
||||
*/
|
||||
private $dispatcher;
|
||||
|
||||
/**
|
||||
* Used by search functions to search using aliases as commands.
|
||||
*/
|
||||
public function setAvailableSearchFields(array $fields): void
|
||||
{
|
||||
$this->availableSearchFields = $fields;
|
||||
}
|
||||
|
||||
public function setDispatcher(EventDispatcherInterface $dispatcher): void
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
*/
|
||||
public function getEntity($id = 0): ?Company
|
||||
{
|
||||
try {
|
||||
$q = $this->createQueryBuilder($this->getTableAlias());
|
||||
if (is_array($id)) {
|
||||
$this->buildSelectClause($q, $id);
|
||||
$companyId = (int) $id['id'];
|
||||
} else {
|
||||
$companyId = $id;
|
||||
}
|
||||
$q->andWhere($this->getTableAlias().'.id = '.(int) $companyId);
|
||||
$entity = $q->getQuery()->getSingleResult();
|
||||
} catch (\Exception) {
|
||||
$entity = null;
|
||||
}
|
||||
|
||||
if (null === $entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($entity->getFields()) {
|
||||
// Pulled from Doctrine memory so don't make unnecessary queries as this has already happened
|
||||
return $entity;
|
||||
}
|
||||
|
||||
$fieldValues = $this->getFieldValues($id, true, 'company');
|
||||
$entity->setFields($fieldValues);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of leads.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
return $this->getEntitiesWithCustomFields('company', $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Doctrine\DBAL\Query\QueryBuilder
|
||||
*/
|
||||
public function getEntitiesDbalQueryBuilder()
|
||||
{
|
||||
return $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies', $this->getTableAlias());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $args
|
||||
*
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function getEntitiesOrmQueryBuilder($order, array $args=[])
|
||||
{
|
||||
$q = $this->getEntityManager()->createQueryBuilder();
|
||||
$q->select($this->getTableAlias().','.$order)
|
||||
->from(Company::class, $this->getTableAlias(), $this->getTableAlias().'.id');
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the groups available for fields.
|
||||
*/
|
||||
public function getFieldGroups(): array
|
||||
{
|
||||
return ['core', 'professional', 'other'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get companies by lead.
|
||||
*/
|
||||
public function getCompaniesByLeadId($leadId, $companyId = null): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('comp.*, cl.is_primary')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies', 'comp')
|
||||
->leftJoin('comp', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 'cl.company_id = comp.id')
|
||||
->where('cl.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId)
|
||||
->orderBy('cl.is_primary', 'DESC');
|
||||
|
||||
if ($companyId) {
|
||||
$q->andWhere('comp.id = :companyId')->setParameter('companyId', $companyId);
|
||||
}
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'comp';
|
||||
}
|
||||
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
$customFields = $this->getSearchableFieldAliases($this->getEntityManager()->getRepository(LeadField::class), 'company');
|
||||
$availableForSearch = array_map(fn ($alias) => 'comp.'.$alias, $customFields);
|
||||
|
||||
$columns = array_merge(
|
||||
[
|
||||
'comp.companyname',
|
||||
'comp.companyemail',
|
||||
],
|
||||
$availableForSearch,
|
||||
);
|
||||
|
||||
return $this->addStandardCatchAllWhereClause(
|
||||
$q,
|
||||
$filter,
|
||||
$columns
|
||||
);
|
||||
}
|
||||
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
[$expr, $parameters] = $this->addStandardSearchCommandWhereClause($q, $filter);
|
||||
$unique = $this->generateRandomParameterName();
|
||||
$returnParameter = true;
|
||||
$command = $filter->command;
|
||||
|
||||
if (in_array($command, [
|
||||
$this->translator->trans('mautic.project.searchcommand.name'),
|
||||
$this->translator->trans('mautic.project.searchcommand.name', [], null, 'en_US'),
|
||||
])) {
|
||||
return $this->handleProjectFilter(
|
||||
$this->_em->getConnection()->createQueryBuilder(),
|
||||
'company_id',
|
||||
'company_projects_xref',
|
||||
$this->getTableAlias(),
|
||||
$filter->string,
|
||||
$filter->not
|
||||
);
|
||||
}
|
||||
|
||||
if (in_array($command, $this->availableSearchFields)) {
|
||||
$expr = $q->expr()->like($this->getTableAlias().".$command", ":$unique");
|
||||
}
|
||||
|
||||
if ($this->dispatcher) {
|
||||
$event = new CompanyBuildSearchEvent($filter->string, $filter->command, $unique, $filter->not, $q);
|
||||
$this->dispatcher->dispatch($event, LeadEvents::COMPANY_BUILD_SEARCH_COMMANDS);
|
||||
if ($event->isSearchDone()) {
|
||||
$returnParameter = $event->getReturnParameters();
|
||||
$filter->strict = $event->getStrict();
|
||||
$expr = $event->getSubQuery();
|
||||
$parameters = array_merge($parameters, $event->getParameters());
|
||||
}
|
||||
}
|
||||
|
||||
if ($returnParameter) {
|
||||
$string = ($filter->strict) ? $filter->string : "%{$filter->string}%";
|
||||
$parameters[$unique] = $string;
|
||||
}
|
||||
|
||||
return [
|
||||
$expr,
|
||||
$parameters,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
$commands = array_merge(['mautic.project.searchcommand.name'], $this->getStandardSearchCommands());
|
||||
if (!empty($this->availableSearchFields)) {
|
||||
$commands = array_merge($commands, $this->availableSearchFields);
|
||||
}
|
||||
|
||||
return $commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $user
|
||||
* @param string $id
|
||||
*
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function getCompanies($user = false, $id = '')
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
static $companies = [];
|
||||
|
||||
if ($user) {
|
||||
$user = $this->currentUser;
|
||||
}
|
||||
|
||||
$key = (int) $id;
|
||||
if (isset($companies[$key])) {
|
||||
return $companies[$key];
|
||||
}
|
||||
|
||||
$q->select('comp.*, cl.is_primary')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies', 'comp')
|
||||
->leftJoin('comp', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 'cl.company_id = comp.id');
|
||||
|
||||
if (!empty($id)) {
|
||||
$q->where(
|
||||
$q->expr()->eq('comp.id', $id)
|
||||
);
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
$q->andWhere('comp.created_by = :user');
|
||||
$q->setParameter('user', $user->getId());
|
||||
}
|
||||
|
||||
$q->orderBy('comp.companyname', 'ASC');
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$companies[$key] = $results;
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a count of leads that belong to the company.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLeadCount($companyIds)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('count(cl.lead_id) as thecount, cl.company_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl');
|
||||
|
||||
$returnArray = is_array($companyIds);
|
||||
|
||||
if (!$returnArray) {
|
||||
$companyIds = [$companyIds];
|
||||
}
|
||||
|
||||
$q->where(
|
||||
$q->expr()->in('cl.company_id', $companyIds)
|
||||
)
|
||||
->groupBy('cl.company_id');
|
||||
|
||||
$result = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$return = [];
|
||||
foreach ($result as $r) {
|
||||
$return[$r['company_id']] = $r['thecount'];
|
||||
}
|
||||
|
||||
// Ensure lists without leads have a value
|
||||
foreach ($companyIds as $l) {
|
||||
if (!isset($return[$l])) {
|
||||
$return[$l] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return ($returnArray) ? $return : $return[$companyIds[0]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of lists.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function identifyCompany($companyName, $city = null, $country = null, $state = null)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
if (empty($companyName)) {
|
||||
return [];
|
||||
}
|
||||
$q->select('comp.id, comp.companyname, comp.companycity, comp.companycountry, comp.companystate')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies', 'comp');
|
||||
|
||||
$q->where(
|
||||
$q->expr()->eq('comp.companyname', ':companyName')
|
||||
)->setParameter('companyName', $companyName);
|
||||
|
||||
if ($city) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('comp.companycity', ':city')
|
||||
)->setParameter('city', $city);
|
||||
}
|
||||
if ($country) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('comp.companycountry', ':country')
|
||||
)->setParameter('country', $country);
|
||||
}
|
||||
if ($state) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('comp.companystate', ':state')
|
||||
)->setParameter('state', $state);
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
return ($results) ? $results[0] : null;
|
||||
}
|
||||
|
||||
public function getCompaniesForContacts(array $contacts): array
|
||||
{
|
||||
if (!$contacts) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$qb->select('c.*, l.lead_id, l.is_primary')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies', 'c')
|
||||
->join('c', MAUTIC_TABLE_PREFIX.'companies_leads', 'l', 'l.company_id = c.id')
|
||||
->where(
|
||||
$qb->expr()->and(
|
||||
$qb->expr()->in('l.lead_id', $contacts)
|
||||
)
|
||||
)
|
||||
->orderBy('l.date_added, l.company_id', 'DESC'); // primary should be [0]
|
||||
|
||||
$companies = $qb->executeQuery()->fetchAllAssociative();
|
||||
|
||||
// Group companies per contact
|
||||
$contactCompanies = [];
|
||||
foreach ($companies as $company) {
|
||||
if (!isset($contactCompanies[$company['lead_id']])) {
|
||||
$contactCompanies[$company['lead_id']] = [];
|
||||
}
|
||||
|
||||
$contactCompanies[$company['lead_id']][] = $company;
|
||||
}
|
||||
|
||||
return $contactCompanies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get companies grouped by column.
|
||||
*
|
||||
* @param \Doctrine\DBAL\Query\QueryBuilder $query
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getCompaniesByGroup($query, $column): array
|
||||
{
|
||||
$query->select('count(comp.id) as companies, '.$column)
|
||||
->addGroupBy($column)
|
||||
->andWhere(
|
||||
$query->expr()->and(
|
||||
$query->expr()->isNotNull($column),
|
||||
$query->expr()->neq($column, $query->expr()->literal(''))
|
||||
)
|
||||
);
|
||||
|
||||
return $query->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $limit
|
||||
* @param int $offset
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMostCompanies($query, $limit = 10, $offset = 0)
|
||||
{
|
||||
$query->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $query->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $valueColumn
|
||||
*/
|
||||
public function getAjaxSimpleList(
|
||||
?CompositeExpression $expr = null,
|
||||
array $parameters = [],
|
||||
$labelColumn = null,
|
||||
$valueColumn = 'id',
|
||||
int $limit = 10,
|
||||
int $start = 0,
|
||||
): array {
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$alias = $prefix = $this->getTableAlias();
|
||||
if (!empty($prefix)) {
|
||||
$prefix .= '.';
|
||||
}
|
||||
|
||||
$tableName = $this->_em->getClassMetadata($this->getEntityName())->getTableName();
|
||||
|
||||
$class = '\\'.$this->getClassName();
|
||||
$reflection = new \ReflectionClass(new $class());
|
||||
|
||||
// Get the label column if necessary
|
||||
if (null == $labelColumn) {
|
||||
if ($reflection->hasMethod('getTitle')) {
|
||||
$labelColumn = 'title';
|
||||
} else {
|
||||
$labelColumn = 'name';
|
||||
}
|
||||
}
|
||||
|
||||
if (!(isset($parameters['onlyNames']) && $parameters['onlyNames'])) {
|
||||
$labelExpression = '
|
||||
case
|
||||
when (comp.companycountry is not null and comp.companycity is not null) then concat(comp.companyname, \' <small>\', companycity,\', \', companycountry, \'</small>\')
|
||||
when (comp.companycountry is not null) then concat(comp.companyname, \' <small>\', comp.companycountry, \'</small>\')
|
||||
when (comp.companycity is not null) then concat(comp.companyname, \' <small>\', comp.companycity, \'</small>\')
|
||||
else comp.companyname
|
||||
end
|
||||
as label';
|
||||
} else {
|
||||
$labelExpression = $prefix.' companyname as label';
|
||||
}
|
||||
|
||||
$q->select($prefix.$valueColumn.' as value, '.$labelExpression)
|
||||
->from($tableName, $alias)
|
||||
->orderBy($prefix.$labelColumn);
|
||||
|
||||
if (null !== $expr && $expr->count()) {
|
||||
$q->where($expr);
|
||||
}
|
||||
|
||||
if (!empty($parameters)) {
|
||||
$q->setParameters($parameters);
|
||||
}
|
||||
|
||||
// Published only
|
||||
if ($reflection->hasMethod('getIsPublished')) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq($prefix.'is_published', ':true')
|
||||
)
|
||||
->setParameter('true', true, 'boolean');
|
||||
}
|
||||
|
||||
if ($limit > 0) {
|
||||
$q->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of company Ids by unique field data.
|
||||
*
|
||||
* @param iterable<mixed> $uniqueFieldsWithData An array of columns & values to filter by
|
||||
* @param int|null $companyId The current company id. Added to query to skip and find other companies
|
||||
* @param int|null $limit Limit count of results to return
|
||||
*
|
||||
* @return array<array{id: string}>
|
||||
*/
|
||||
public function getCompanyIdsByUniqueFields($uniqueFieldsWithData, ?int $companyId = null, ?int $limit = null): array
|
||||
{
|
||||
return $this->getCompanyFieldsByUniqueFields($uniqueFieldsWithData, 'c.id', $companyId, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of company Ids by unique field data.
|
||||
*
|
||||
* @param iterable<mixed> $uniqueFieldsWithData An array of columns & values to filter by
|
||||
* @param int|null $companyId The current company id. Added to query to skip and find other companies
|
||||
* @param int|null $limit Limit count of results to return
|
||||
*
|
||||
* @return array<array{id: string}>
|
||||
*/
|
||||
public function getCompanyFieldsByUniqueFields($uniqueFieldsWithData, string $select, ?int $companyId = null, ?int $limit = null): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select($select)
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies', 'c');
|
||||
|
||||
// loop through the fields and
|
||||
foreach ($uniqueFieldsWithData as $col => $val) {
|
||||
$q->{$this->getUniqueIdentifiersWherePart()}("c.$col = :".$col)
|
||||
->setParameter($col, $val);
|
||||
}
|
||||
|
||||
// if we have a company ID lets use it
|
||||
if ($companyId > 0) {
|
||||
// make sure that it's not the id we already have
|
||||
$q->andWhere('c.id != :companyId')
|
||||
->setParameter('companyId', $companyId);
|
||||
}
|
||||
|
||||
if ($limit > 0) {
|
||||
$q->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Company[]
|
||||
*/
|
||||
public function getCompaniesByUniqueFields(array $uniqueFieldsWithData, ?int $companyId = null, ?int $limit = null): array
|
||||
{
|
||||
$results = $this->getCompanyFieldsByUniqueFields($uniqueFieldsWithData, 'c.*', $companyId, $limit);
|
||||
|
||||
// Collect the IDs
|
||||
$companies = [];
|
||||
foreach ($results as $r) {
|
||||
$companies[(int) $r['id']] = $r;
|
||||
}
|
||||
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->select('c')
|
||||
->from(Company::class, 'c');
|
||||
|
||||
$q->where(
|
||||
$q->expr()->in('c.id', ':ids')
|
||||
)
|
||||
->setParameter('ids', array_keys($companies))
|
||||
->orderBy('c.dateAdded', Order::Descending->value)
|
||||
->addOrderBy('c.id', Order::Descending->value);
|
||||
|
||||
$entities = $q->getQuery()
|
||||
->getResult();
|
||||
|
||||
/** @var Company $company */
|
||||
foreach ($entities as $company) {
|
||||
$company->setFields(
|
||||
$this->formatFieldValues($companies[$company->getId()], true, 'company')
|
||||
);
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string[]>
|
||||
*/
|
||||
public function getCompanyLookupData(string $filterVal): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('id, companyname, companycity, companystate')
|
||||
->from(MAUTIC_TABLE_PREFIX.Company::TABLE_NAME)
|
||||
->where($q->expr()->eq('is_published', true))
|
||||
->andWhere($q->expr()->like('companyname', ':filterVar'))
|
||||
->setParameter('filterVar', '%'.$filterVal.'%')
|
||||
->orderBy('companyname')
|
||||
->setMaxResults(50);
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata as ValidatorClassMetadata;
|
||||
|
||||
class ContactExportScheduler
|
||||
{
|
||||
private ?int $id = null;
|
||||
|
||||
private ?User $user = null; // Created by
|
||||
|
||||
private \DateTimeImmutable $scheduledDateTime;
|
||||
|
||||
/**
|
||||
* @var array<mixed>
|
||||
*/
|
||||
private array $data = [];
|
||||
|
||||
/**
|
||||
* @var array<mixed>
|
||||
*/
|
||||
private array $changes = [];
|
||||
|
||||
/**
|
||||
* @template T of ClassMetadata
|
||||
*
|
||||
* @param T $metadata
|
||||
*/
|
||||
public static function loadMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
$builder->setTable('contact_export_scheduler');
|
||||
$builder->setCustomRepositoryClass(ContactExportSchedulerRepository::class);
|
||||
$builder->addId();
|
||||
$builder->createManyToOne('user', User::class)
|
||||
->addJoinColumn('user_id', 'id', true, false, 'CASCADE')
|
||||
->build();
|
||||
$builder->createField('scheduledDateTime', Types::DATETIME_IMMUTABLE)
|
||||
->columnName('scheduled_datetime')
|
||||
->build();
|
||||
$builder->addNullableField('data', Types::ARRAY);
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ValidatorClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint(
|
||||
'scheduledDate',
|
||||
new Assert\NotBlank(
|
||||
['message' => 'mautic.lead.import.dir.notblank']
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(User $user): self
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->addChange('user', $user->getId());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getScheduledDateTime(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->scheduledDateTime;
|
||||
}
|
||||
|
||||
public function setScheduledDateTime(\DateTimeImmutable $scheduledDateTime): self
|
||||
{
|
||||
$this->scheduledDateTime = $scheduledDateTime;
|
||||
$this->addChange('scheduledDateTime', $scheduledDateTime);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
*/
|
||||
public function setData(array $data): self
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->addChange('data', $data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function getChanges(): array
|
||||
{
|
||||
return $this->changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function addChange(string $property, $value): void
|
||||
{
|
||||
$this->changes[$property] = $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<ContactExportScheduler>
|
||||
*/
|
||||
class ContactExportSchedulerRepository extends CommonRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
interface CustomFieldEntityInterface
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $fields
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function setFields($fields);
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFields();
|
||||
|
||||
/**
|
||||
* @param string $alias
|
||||
* @param mixed $value
|
||||
* @param string $oldValue
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function addUpdatedField($alias, $value, $oldValue = '');
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUpdatedFields();
|
||||
|
||||
/**
|
||||
* Get a field value (should include those in updated fields).
|
||||
*
|
||||
* @param string $field alias
|
||||
* @param string|null $group
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFieldValue($field, $group = null);
|
||||
|
||||
/**
|
||||
* Get field details.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $group
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function getField($key, $group = null);
|
||||
|
||||
/**
|
||||
* Get flat array of profile fields without groups.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getProfileFields();
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\LeadBundle\Field\SchemaDefinition;
|
||||
use Mautic\LeadBundle\Helper\CustomFieldHelper;
|
||||
use Mautic\LeadBundle\Helper\CustomFieldValueHelper;
|
||||
|
||||
trait CustomFieldEntityTrait
|
||||
{
|
||||
/**
|
||||
* Used by Mautic to populate the fields pulled from the DB.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fields = [];
|
||||
|
||||
/**
|
||||
* Just a place to store updated field values so we don't have to loop through them again comparing.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $updatedFields = [];
|
||||
|
||||
/**
|
||||
* A place events can use to pass data around on the object to prevent issues like creating a contact and having it processed to be sent back
|
||||
* to the origin of creation in a webhook.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $eventData = [];
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function __get($name)
|
||||
{
|
||||
return $this->getFieldValue(strtolower($name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function __set($name, $value)
|
||||
{
|
||||
return $this->addUpdatedField(strtolower($name), $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call($name, $arguments)
|
||||
{
|
||||
$isSetter = str_starts_with($name, 'set');
|
||||
$isGetter = str_starts_with($name, 'get');
|
||||
|
||||
if (($isSetter && array_key_exists(0, $arguments)) || $isGetter) {
|
||||
$fieldRequested = mb_strtolower(mb_substr($name, 3));
|
||||
$fields = $this->getProfileFields();
|
||||
|
||||
if (array_key_exists($fieldRequested, $fields)) {
|
||||
return ($isSetter) ? $this->addUpdatedField($fieldRequested, $arguments[0]) : $this->getFieldValue($fieldRequested);
|
||||
}
|
||||
}
|
||||
|
||||
return parent::__call($name, $arguments);
|
||||
}
|
||||
|
||||
public function setFields($fields): void
|
||||
{
|
||||
$this->fields = CustomFieldValueHelper::normalizeValues($fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $ungroup
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFields($ungroup = false)
|
||||
{
|
||||
if ($ungroup && isset($this->fields['core'])) {
|
||||
$return = [];
|
||||
foreach ($this->fields as $fields) {
|
||||
$return += $fields;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an updated field to persist to the DB and to note changes.
|
||||
*
|
||||
* @param string $oldValue
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addUpdatedField($alias, $value, $oldValue = null)
|
||||
{
|
||||
// Don't allow overriding ID
|
||||
if ('id' === $alias) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$property = (defined('self::FIELD_ALIAS')) ? str_replace(self::FIELD_ALIAS, '', $alias) : $alias;
|
||||
$field = $this->getField($alias);
|
||||
$setter = 'set'.ucfirst($property);
|
||||
|
||||
if (null == $oldValue) {
|
||||
$oldValue = $this->getFieldValue($alias);
|
||||
} elseif ($field) {
|
||||
$oldValue = CustomFieldHelper::fixValueType($field['type'], $oldValue);
|
||||
}
|
||||
|
||||
if (property_exists($this, $property) && method_exists($this, $setter)) {
|
||||
// Fixed custom field so use the setter but don't get caught in a loop such as a custom field called "notes"
|
||||
// Set empty value as null
|
||||
if ('' === $value) {
|
||||
$value = null;
|
||||
}
|
||||
$this->$setter($value);
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$value = trim($value);
|
||||
if ('' === $value) {
|
||||
// Ensure value is null for consistency
|
||||
$value = null;
|
||||
|
||||
if ('' === $oldValue) {
|
||||
$oldValue = null;
|
||||
}
|
||||
}
|
||||
} elseif (is_array($value)) {
|
||||
// Flatten the array
|
||||
$value = implode('|', $value);
|
||||
}
|
||||
|
||||
if ($field) {
|
||||
$value = CustomFieldHelper::fixValueType($field['type'], $value);
|
||||
}
|
||||
|
||||
if ($oldValue !== $value && !(('' === $oldValue && null === $value) || (null === $oldValue && '' === $value))) {
|
||||
$this->addChange('fields', [$alias => [$oldValue, $value]]);
|
||||
$this->updatedFields[$alias] = $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array of updated fields.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getUpdatedFields()
|
||||
{
|
||||
return $this->updatedFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @param string|null $group
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFieldValue($field, $group = null)
|
||||
{
|
||||
if (property_exists($this, $field)) {
|
||||
$value = $this->{'get'.ucfirst($field)}();
|
||||
|
||||
if (null !== $value) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists($field, $this->updatedFields)) {
|
||||
return $this->updatedFields[$field];
|
||||
}
|
||||
|
||||
if ($field = $this->getField($field, $group)) {
|
||||
return CustomFieldHelper::fixValueType($field['type'], $field['value']);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field details.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $group
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function getField($key, $group = null)
|
||||
{
|
||||
if ($group && isset($this->fields[$group][$key])) {
|
||||
return $this->fields[$group][$key];
|
||||
}
|
||||
|
||||
foreach ($this->fields as $groupFields) {
|
||||
foreach ($groupFields as $name => $details) {
|
||||
if ($name == $key) {
|
||||
return $details;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getProfileFields()
|
||||
{
|
||||
if (isset($this->fields['core'])) {
|
||||
$fieldValues = [
|
||||
'id' => $this->id,
|
||||
];
|
||||
|
||||
foreach ($this->fields as $group => $fields) {
|
||||
if ('all' === $group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($fields as $alias => $field) {
|
||||
$fieldValues[$alias] = $field['value'];
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge($fieldValues, $this->updatedFields);
|
||||
} else {
|
||||
// The fields are already flattened
|
||||
|
||||
return $this->fields;
|
||||
}
|
||||
}
|
||||
|
||||
public function hasFields(): bool
|
||||
{
|
||||
return !empty($this->fields);
|
||||
}
|
||||
|
||||
public function getEventData($key)
|
||||
{
|
||||
return $this->eventData[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setEventData($key, $value)
|
||||
{
|
||||
$this->eventData[$key] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected static function loadFixedFieldMetadata(ClassMetadataBuilder $builder, array $fields, array $customFieldDefinitions)
|
||||
{
|
||||
foreach ($fields as $fieldProperty) {
|
||||
$field = (defined('self::FIELD_ALIAS')) ? self::FIELD_ALIAS.$fieldProperty : $fieldProperty;
|
||||
|
||||
$type = 'text';
|
||||
if (isset($customFieldDefinitions[$field]) && !empty($customFieldDefinitions[$field]['type'])) {
|
||||
$type = $customFieldDefinitions[$field]['type'];
|
||||
}
|
||||
|
||||
$builder->addNamedField(
|
||||
$fieldProperty,
|
||||
SchemaDefinition::getSchemaDefinition($field, $type, !empty($customFieldDefinitions[$field]['unique']))['type'],
|
||||
$field,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\QueryBuilder as DbalQueryBuilder;
|
||||
use Doctrine\ORM\QueryBuilder as OrmQueryBuilder;
|
||||
|
||||
/**
|
||||
* Interface CustomFieldRepositoryInterface.
|
||||
*/
|
||||
interface CustomFieldRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Return an array of groups supported by the custom fields for this entity.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFieldGroups();
|
||||
|
||||
/**
|
||||
* Get the base DBAL query builder for entities.
|
||||
*
|
||||
* @return DbalQueryBuilder
|
||||
*/
|
||||
public function getEntitiesDbalQueryBuilder();
|
||||
|
||||
/**
|
||||
* Get the base DBAL query builder for entities.
|
||||
*
|
||||
* @return OrmQueryBuilder
|
||||
*/
|
||||
public function getEntitiesOrmQueryBuilder($order);
|
||||
|
||||
/**
|
||||
* Requires table alias.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getTableAlias();
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Mautic\CoreBundle\Cache\ResultCacheHelper;
|
||||
use Mautic\CoreBundle\Cache\ResultCacheOptions;
|
||||
use Mautic\LeadBundle\Controller\ListController;
|
||||
use Mautic\LeadBundle\Helper\CustomFieldHelper;
|
||||
|
||||
trait CustomFieldRepositoryTrait
|
||||
{
|
||||
protected $useDistinctCount = false;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $customFieldList = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $uniqueIdentifiersOperator;
|
||||
|
||||
/**
|
||||
* @param string $object
|
||||
* @param array $args
|
||||
*/
|
||||
public function getEntitiesWithCustomFields($object, $args, $resultsCallback = null)
|
||||
{
|
||||
$skipOrdering = $args['skipOrdering'] ?? false;
|
||||
[$fields, $fixedFields] = $this->getCustomFieldList($object);
|
||||
|
||||
// Fix arguments if necessary
|
||||
$args = $this->convertOrmProperties($this->getClassName(), $args);
|
||||
|
||||
// DBAL
|
||||
/** @var QueryBuilder $dq */
|
||||
$dq = $args['qb'] ?? $this->getEntitiesDbalQueryBuilder();
|
||||
|
||||
// Generate where clause first to know if we need to use distinct on primary ID or not
|
||||
$this->useDistinctCount = false;
|
||||
$this->buildWhereClause($dq, $args);
|
||||
|
||||
if (!empty($args['withTotalCount']) || !isset($args['count'])) {
|
||||
// Distinct is required here to get the correct count when group by is used due to applied filters
|
||||
$countSelect = ($this->useDistinctCount) ? 'COUNT(DISTINCT('.$this->getTableAlias().'.id))' : 'COUNT('.$this->getTableAlias().'.id)';
|
||||
$dq->select($countSelect.' as count');
|
||||
|
||||
// Advanced search filters may have set a group by and if so, let's remove it for the count.
|
||||
if ($groupBy = $dq->getQueryPart('groupBy')) {
|
||||
$dq->resetQueryPart('groupBy');
|
||||
}
|
||||
|
||||
// get a total count
|
||||
if (!empty($args['totalCountTtl'])) {
|
||||
$statement = ResultCacheHelper::executeCachedDbalQuery($this->getEntityManager()->getConnection(), $dq, new ResultCacheOptions($object.'-total-count', $args['totalCountTtl']));
|
||||
} else {
|
||||
$statement = $dq->executeQuery();
|
||||
}
|
||||
|
||||
$result = $statement->fetchAllAssociative();
|
||||
$total = ($result) ? $result[0]['count'] : 0;
|
||||
} else {
|
||||
$total = $args['count'];
|
||||
}
|
||||
|
||||
if (!$total && !empty($args['withTotalCount'])) {
|
||||
$results = [];
|
||||
} else {
|
||||
if (isset($groupBy) && $groupBy) {
|
||||
$dq->groupBy($groupBy);
|
||||
}
|
||||
// now get the actual paginated results
|
||||
|
||||
$this->buildOrderByClause($dq, $args);
|
||||
$this->buildLimiterClauses($dq, $args);
|
||||
|
||||
$dq->resetQueryPart('select');
|
||||
$this->buildSelectClause($dq, $args);
|
||||
|
||||
$results = $dq->executeQuery()->fetchAllAssociative();
|
||||
if (isset($args['route']) && ListController::ROUTE_SEGMENT_CONTACTS == $args['route']) {
|
||||
unset($args['select']); // Our purpose of getting list of ids has already accomplished. We no longer need this.
|
||||
}
|
||||
|
||||
// loop over results to put fields in something that can be assigned to the entities
|
||||
$fieldValues = [];
|
||||
$groups = $this->getFieldGroups();
|
||||
|
||||
foreach ($results as $result) {
|
||||
$id = $result['id'];
|
||||
// unset all the columns that are not fields
|
||||
$this->removeNonFieldColumns($result, $fixedFields);
|
||||
|
||||
foreach ($result as $k => $r) {
|
||||
if (isset($fields[$k])) {
|
||||
$fieldValues[$id][$fields[$k]['group']][$fields[$k]['alias']] = $fields[$k];
|
||||
$fieldValues[$id][$fields[$k]['group']][$fields[$k]['alias']]['value'] = $r;
|
||||
}
|
||||
}
|
||||
|
||||
// make sure each group key is present
|
||||
foreach ($groups as $g) {
|
||||
if (!isset($fieldValues[$id][$g])) {
|
||||
$fieldValues[$id][$g] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unset($results, $fields);
|
||||
|
||||
// get an array of IDs for ORM query
|
||||
$ids = array_keys($fieldValues);
|
||||
|
||||
if (count($ids)) {
|
||||
if ($skipOrdering) {
|
||||
$alias = $this->getTableAlias();
|
||||
$q = $this->getEntityManager()->createQueryBuilder();
|
||||
$q->select($alias)
|
||||
->from(Lead::class, $alias, $alias.'.id')
|
||||
->indexBy($alias, $alias.'.id');
|
||||
} else {
|
||||
// ORM
|
||||
|
||||
// build the order by id since the order was applied above
|
||||
// unfortunately, doctrine does not have a way to natively support this and can't use MySQL's FIELD function
|
||||
// since we have to be cross-platform; it's way ugly
|
||||
|
||||
// We should probably totally ditch orm for leads
|
||||
|
||||
// This "hack" is in place to allow for custom ordering in the API.
|
||||
// See https://github.com/mautic/mautic/pull/7494#issuecomment-600970208
|
||||
$order = '(CASE';
|
||||
foreach ($ids as $count => $id) {
|
||||
$order .= ' WHEN '.$this->getTableAlias().'.id = '.$id.' THEN '.$count;
|
||||
++$count;
|
||||
}
|
||||
$order .= ' ELSE '.$count.' END) AS HIDDEN ORD';
|
||||
|
||||
// ORM - generates lead entities
|
||||
/** @var \Doctrine\ORM\QueryBuilder $q */
|
||||
$q = $this->getEntitiesOrmQueryBuilder($order, $args);
|
||||
$this->buildSelectClause($dq, $args);
|
||||
|
||||
$q->orderBy('ORD', Order::Ascending->value);
|
||||
}
|
||||
|
||||
// only pull the leads as filtered via DBAL
|
||||
$q->where(
|
||||
$q->expr()->in($this->getTableAlias().'.id', ':entityIds')
|
||||
)->setParameter('entityIds', $ids);
|
||||
|
||||
$results = $q->getQuery()
|
||||
->useQueryCache(false) // the query contains ID's, so there is no use in caching it
|
||||
->getResult();
|
||||
|
||||
// assign fields
|
||||
/** @var Lead $r */
|
||||
foreach ($results as $r) {
|
||||
$id = $r->getId();
|
||||
$r->setFields($fieldValues[$id]);
|
||||
|
||||
if (is_callable($resultsCallback)) {
|
||||
$resultsCallback($r);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$results = [];
|
||||
}
|
||||
}
|
||||
|
||||
return (!empty($args['withTotalCount'])) ?
|
||||
[
|
||||
'count' => $total,
|
||||
'results' => $results,
|
||||
] : $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $byGroup
|
||||
* @param string $object
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFieldValues($id, $byGroup = true, $object = 'lead')
|
||||
{
|
||||
// use DBAL to get entity fields
|
||||
$q = $this->getEntitiesDbalQueryBuilder();
|
||||
|
||||
if (is_array($id)) {
|
||||
$this->buildSelectClause($q, $id);
|
||||
$id = $id['id'];
|
||||
} else {
|
||||
$q->select($this->getTableAlias().'.*');
|
||||
}
|
||||
|
||||
$q->where($this->getTableAlias().'.id = '.(int) $id);
|
||||
$values = $q->executeQuery()->fetchAssociative();
|
||||
|
||||
return $this->formatFieldValues($values, $byGroup, $object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of unique values from fields for autocompletes.
|
||||
*
|
||||
* @param string $search
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getValueList($field, $search = '', $limit = 10, $start = 0)
|
||||
{
|
||||
// Includes prefix
|
||||
$table = $this->getEntityManager()->getClassMetadata($this->getClassName())->getTableName();
|
||||
$col = $this->getTableAlias().'.'.$field;
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select("DISTINCT $col")
|
||||
->from($table, 'l');
|
||||
|
||||
$q->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->neq($col, $q->expr()->literal('')),
|
||||
$q->expr()->isNotNull($col)
|
||||
)
|
||||
);
|
||||
|
||||
if (!empty($search)) {
|
||||
$q->andWhere("$col LIKE :search")
|
||||
->setParameter('search', "{$search}%");
|
||||
}
|
||||
|
||||
$q->orderBy($col);
|
||||
|
||||
if (!empty($limit)) {
|
||||
$q->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist an array of entities.
|
||||
*
|
||||
* @param array $entities
|
||||
*/
|
||||
public function saveEntities($entities): void
|
||||
{
|
||||
foreach ($entities as $entity) {
|
||||
// Leads cannot be batched due to requiring the ID to update the fields
|
||||
$this->saveEntity($entity);
|
||||
}
|
||||
}
|
||||
|
||||
public function saveEntity($entity, $flush = true): void
|
||||
{
|
||||
$this->preSaveEntity($entity);
|
||||
|
||||
$this->getEntityManager()->persist($entity);
|
||||
|
||||
if ($flush) {
|
||||
$this->getEntityManager()->flush($entity);
|
||||
}
|
||||
|
||||
// Includes prefix
|
||||
$table = $this->getEntityManager()->getClassMetadata($this->getClassName())->getTableName();
|
||||
$fields = $entity->getUpdatedFields();
|
||||
if (method_exists($entity, 'getChanges')) {
|
||||
$changes = $entity->getChanges();
|
||||
|
||||
// remove the fields that are part of changes as they were already saved via a setter
|
||||
$fields = array_diff_key($fields, $changes);
|
||||
}
|
||||
|
||||
$this->prepareDbalFieldsForSave($fields);
|
||||
|
||||
if (!empty($fields)) {
|
||||
$this->getEntityManager()->getConnection()->update($table, $fields, ['id' => $entity->getId()]);
|
||||
}
|
||||
|
||||
$this->postSaveEntity($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to remove non custom field columns from an arrayed lead row.
|
||||
*
|
||||
* @param array $fixedFields
|
||||
*/
|
||||
protected function removeNonFieldColumns(&$r, $fixedFields = [])
|
||||
{
|
||||
$baseCols = $this->getBaseColumns($this->getClassName(), true);
|
||||
foreach ($baseCols as $c) {
|
||||
if (!isset($fixedFields[$c])) {
|
||||
unset($r[$c]);
|
||||
}
|
||||
}
|
||||
unset($r['owner_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $values
|
||||
* @param bool $byGroup
|
||||
* @param string $object
|
||||
*/
|
||||
protected function formatFieldValues($values, $byGroup = true, $object = 'lead'): array
|
||||
{
|
||||
[$fields, $fixedFields] = $this->getCustomFieldList($object);
|
||||
|
||||
$this->removeNonFieldColumns($values, $fixedFields);
|
||||
|
||||
// Reorder leadValues based on field order
|
||||
$values = array_merge(array_flip(array_keys($fields)), $values);
|
||||
|
||||
$fieldValues = [];
|
||||
|
||||
// loop over results to put fields in something that can be assigned to the entities
|
||||
foreach ($values as $k => $r) {
|
||||
if (isset($fields[$k])) {
|
||||
$r = CustomFieldHelper::fixValueType($fields[$k]['type'], $r);
|
||||
|
||||
if (!is_null($r)) {
|
||||
switch ($fields[$k]['type']) {
|
||||
case 'number':
|
||||
$r = (float) $r;
|
||||
break;
|
||||
case 'boolean':
|
||||
$r = (int) $r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$alias = $fields[$k]['alias'];
|
||||
|
||||
if ($byGroup) {
|
||||
$group = $fields[$k]['group'];
|
||||
$fieldValues[$group][$alias] = $fields[$k];
|
||||
$fieldValues[$group][$alias]['value'] = $r;
|
||||
} else {
|
||||
$fieldValues[$alias] = $fields[$k];
|
||||
$fieldValues[$alias]['value'] = $r;
|
||||
}
|
||||
|
||||
unset($fields[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($byGroup) {
|
||||
// make sure each group key is present
|
||||
$groups = $this->getFieldGroups();
|
||||
foreach ($groups as $g) {
|
||||
if (!isset($fieldValues[$g])) {
|
||||
$fieldValues[$g] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $fieldValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the aliases of searchable fields that are indexed and published.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getSearchableFieldAliases(LeadFieldRepository $leadFieldRepository, string $object): array
|
||||
{
|
||||
return $leadFieldRepository->getSearchableFieldAliases($object);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $object
|
||||
*
|
||||
* @return array [$fields, $fixedFields]
|
||||
*/
|
||||
public function getCustomFieldList($object)
|
||||
{
|
||||
if (empty($this->customFieldList)) {
|
||||
// Get the list of custom fields
|
||||
$results = $this->getFieldList($object);
|
||||
|
||||
$fields = [];
|
||||
$fixedFields = [];
|
||||
foreach ($results as $r) {
|
||||
$fields[$r['alias']] = $r;
|
||||
if ($r['is_fixed']) {
|
||||
$fixedFields[$r['alias']] = $r['alias'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->customFieldList = [$fields, $fixedFields];
|
||||
}
|
||||
|
||||
return $this->customFieldList;
|
||||
}
|
||||
|
||||
protected function prepareDbalFieldsForSave(&$fields)
|
||||
{
|
||||
// Ensure booleans are integers
|
||||
foreach ($fields as $field => &$value) {
|
||||
if (is_bool($value)) {
|
||||
$fields[$field] = (int) $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array<int|string>|int|string>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function getFieldList(?string $object = null): array
|
||||
{
|
||||
// Get the list of custom fields
|
||||
$fq = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$fq->select(
|
||||
'f.id, f.label, f.alias, f.type, f.field_group as "group", f.object, f.is_fixed, f.properties, f.default_value'
|
||||
)
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_fields', 'f')
|
||||
->where('f.is_published = :published')
|
||||
->setParameter('published', true, 'boolean')
|
||||
->addOrderBy('f.field_order', 'asc');
|
||||
|
||||
if (null !== $object) {
|
||||
$fq->andWhere($fq->expr()->eq('object', ':object'))
|
||||
->setParameter('object', $object);
|
||||
}
|
||||
|
||||
return $fq->executeQuery()->fetchAllAssociative() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherit and use in class if required to do something to the entity prior to persisting.
|
||||
*/
|
||||
protected function preSaveEntity($entity)
|
||||
{
|
||||
// Inherit and use if required
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherit and use in class if required to do something with the entity after persisting.
|
||||
*/
|
||||
protected function postSaveEntity($entity)
|
||||
{
|
||||
// Inherit and use if required
|
||||
}
|
||||
|
||||
public function setUniqueIdentifiersOperator(string $uniqueIdentifiersOperator): void
|
||||
{
|
||||
$this->uniqueIdentifiersOperator = $uniqueIdentifiersOperator;
|
||||
}
|
||||
|
||||
public function getUniqueIdentifiersWherePart(): string
|
||||
{
|
||||
if ($this->uniqueIdentifiersOperatorIs(CompositeExpression::TYPE_AND)) {
|
||||
return 'andWhere';
|
||||
}
|
||||
|
||||
return 'orWhere';
|
||||
}
|
||||
|
||||
private function uniqueIdentifiersOperatorIs(string $operator): bool
|
||||
{
|
||||
return $this->uniqueIdentifiersOperator === $operator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
|
||||
class DoNotContact
|
||||
{
|
||||
/**
|
||||
* Lead is contactable.
|
||||
*/
|
||||
public const IS_CONTACTABLE = 0;
|
||||
|
||||
/**
|
||||
* Lead unsubscribed themselves.
|
||||
*/
|
||||
public const UNSUBSCRIBED = 1;
|
||||
|
||||
/**
|
||||
* Lead was unsubscribed due to an unsuccessful send.
|
||||
*/
|
||||
public const BOUNCED = 2;
|
||||
|
||||
/**
|
||||
* Lead was manually unsubscribed by user.
|
||||
*/
|
||||
public const MANUAL = 3;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Lead|null
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $reason = 0;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $comments;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $channel;
|
||||
|
||||
private $channelId;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_donotcontact')
|
||||
->setCustomRepositoryClass(DoNotContactRepository::class)
|
||||
->addIndex(['lead_id', 'channel', 'reason'], 'leadid_reason_channel')
|
||||
->addIndex(['reason'], 'dnc_reason_search')
|
||||
->addIndex(['date_added'], 'dnc_date_added');
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->addLead(true, 'CASCADE', false, 'doNotContact');
|
||||
|
||||
$builder->addDateAdded();
|
||||
|
||||
$builder->createField('reason', 'smallint')
|
||||
->build();
|
||||
|
||||
$builder->createField('channel', 'string')
|
||||
->build();
|
||||
|
||||
$builder->addNamedField('channelId', 'integer', 'channel_id', true);
|
||||
|
||||
$builder->createField('comments', 'text')
|
||||
->nullable()
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('doNotContact')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'dateAdded',
|
||||
'reason',
|
||||
'comments',
|
||||
'channel',
|
||||
'channelId',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'lead',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DoNotContact
|
||||
*/
|
||||
public function setLead(Lead $lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DoNotContact
|
||||
*/
|
||||
public function setDateAdded(\DateTime $dateAdded)
|
||||
{
|
||||
$this->dateAdded = $dateAdded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getReason()
|
||||
{
|
||||
return $this->reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $reason
|
||||
*
|
||||
* @return DoNotContact
|
||||
*/
|
||||
public function setReason($reason)
|
||||
{
|
||||
$this->reason = $reason;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getComments()
|
||||
{
|
||||
return $this->comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $comments
|
||||
*
|
||||
* @return DoNotContact
|
||||
*/
|
||||
public function setComments($comments)
|
||||
{
|
||||
$this->comments = InputHelper::string((string) $comments);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getChannel()
|
||||
{
|
||||
return $this->channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channel
|
||||
*
|
||||
* @return DoNotContact
|
||||
*/
|
||||
public function setChannel($channel)
|
||||
{
|
||||
$this->channel = $channel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getChannelId()
|
||||
{
|
||||
return $this->channelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $channelId
|
||||
*
|
||||
* @return DoNotContact
|
||||
*/
|
||||
public function setChannelId($channelId)
|
||||
{
|
||||
$this->channelId = $channelId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<DoNotContact>
|
||||
*/
|
||||
class DoNotContactRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
|
||||
/**
|
||||
* Get a list of DNC entries based on channel and lead_id.
|
||||
*
|
||||
* @param string $channel
|
||||
*
|
||||
* @return DoNotContact[]
|
||||
*/
|
||||
public function getEntriesByLeadAndChannel(Lead $lead, $channel)
|
||||
{
|
||||
return $this->findBy(['channel' => $channel, 'lead' => $lead]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $channel
|
||||
* @param array<int,int|string>|int|null $ids
|
||||
* @param int|null $reason
|
||||
* @param array<int,int|string>|int|true|null $listId
|
||||
* @param bool $combined
|
||||
*
|
||||
* @return array|int
|
||||
*/
|
||||
public function getCount($channel = null, $ids = null, $reason = null, $listId = null, ?ChartQuery $chartQuery = null, $combined = false)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('count(dnc.id) as dnc_count')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'dnc');
|
||||
|
||||
if ($ids) {
|
||||
if (!is_array($ids)) {
|
||||
$ids = [(int) $ids];
|
||||
}
|
||||
$q->where(
|
||||
$q->expr()->in('dnc.channel_id', $ids)
|
||||
);
|
||||
}
|
||||
|
||||
if ($channel) {
|
||||
$q->andWhere('dnc.channel = :channel')
|
||||
->setParameter('channel', $channel);
|
||||
}
|
||||
|
||||
if ($reason) {
|
||||
$q->andWhere('dnc.reason = :reason')
|
||||
->setParameter('reason', $reason);
|
||||
}
|
||||
|
||||
if ($listId) {
|
||||
if (!$combined) {
|
||||
$q->innerJoin('dnc', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'cs', 'cs.lead_id = dnc.lead_id');
|
||||
|
||||
if (true === $listId && !$combined) {
|
||||
$q->addSelect('cs.leadlist_id')
|
||||
->groupBy('cs.leadlist_id');
|
||||
} elseif (is_array($listId)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->in('cs.leadlist_id', ':segmentIds')
|
||||
);
|
||||
|
||||
$q->setParameter('segmentIds', $listId, ArrayParameterType::INTEGER);
|
||||
|
||||
$q->addSelect('cs.leadlist_id')
|
||||
->groupBy('cs.leadlist_id');
|
||||
} else {
|
||||
$q->andWhere('cs.leadlist_id = :list_id')
|
||||
->setParameter('list_id', $listId);
|
||||
}
|
||||
} else {
|
||||
$subQ = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$subQ->select('distinct(list.lead_id)')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'list')
|
||||
->andWhere(
|
||||
$q->expr()->in('list.leadlist_id', ':segmentIds')
|
||||
);
|
||||
|
||||
$q->setParameter('segmentIds', $listId, ArrayParameterType::INTEGER);
|
||||
|
||||
$q->innerJoin('dnc', sprintf('(%s)', $subQ->getSQL()), 'cs', 'cs.lead_id = dnc.lead_id');
|
||||
}
|
||||
}
|
||||
|
||||
if ($chartQuery) {
|
||||
$chartQuery->applyDateFilters($q, 'date_added', 'dnc');
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
if ((true === $listId || is_array($listId)) && !$combined) {
|
||||
// Return list group of counts
|
||||
$byList = [];
|
||||
foreach ($results as $result) {
|
||||
$byList[$result['leadlist_id']] = $result['dnc_count'];
|
||||
}
|
||||
|
||||
return $byList;
|
||||
}
|
||||
|
||||
return (isset($results[0])) ? $results[0]['dnc_count'] : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getTimelineStats($leadId = null, array $options = [])
|
||||
{
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$query->select('dnc.id, dnc.channel, dnc.channel_id, dnc.date_added, dnc.reason, dnc.comments, dnc.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'dnc');
|
||||
|
||||
if ($leadId) {
|
||||
$query->where($query->expr()->eq('dnc.lead_id', (int) $leadId));
|
||||
}
|
||||
|
||||
if (isset($options['search']) && $options['search']) {
|
||||
$query->andWhere(
|
||||
$query->expr()->like('dnc.channel', $query->expr()->literal('%'.$options['search'].'%'))
|
||||
);
|
||||
}
|
||||
|
||||
return $this->getTimelineResults($query, $options, 'dnc.channel', 'dnc.date_added', [], ['date_added'], null, 'dnc.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $channel
|
||||
* @param string[]|int[] $contacts Array of contact IDs to filter by
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getChannelList($channel, ?array $contacts = null): array
|
||||
{
|
||||
// If no contacts are sent then stop querying for all of the DNC records as it leads to the out of memory error.
|
||||
if (is_array($contacts) && empty($contacts)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'dnc')
|
||||
->leftJoin('dnc', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = dnc.lead_id');
|
||||
|
||||
if (null === $channel) {
|
||||
$q->select('dnc.channel, dnc.reason, l.id as lead_id');
|
||||
} else {
|
||||
$q->select('l.id, dnc.reason')
|
||||
->where('dnc.channel = :channel')
|
||||
->setParameter('channel', $channel);
|
||||
}
|
||||
|
||||
if ($contacts) {
|
||||
$q->andWhere(
|
||||
$q->expr()->in('l.id', $contacts)
|
||||
);
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$dnc = [];
|
||||
foreach ($results as $r) {
|
||||
if (isset($r['lead_id'])) {
|
||||
if (!isset($dnc[$r['lead_id']])) {
|
||||
$dnc[$r['lead_id']] = [];
|
||||
}
|
||||
|
||||
$dnc[$r['lead_id']][$r['channel']] = $r['reason'];
|
||||
} else {
|
||||
$dnc[$r['id']] = $r['reason'];
|
||||
}
|
||||
}
|
||||
|
||||
unset($results);
|
||||
|
||||
return $dnc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unique combinations of reason and channel.
|
||||
*
|
||||
* @return array<int, array{reason: mixed, channel: mixed}> Array of arrays containing 'reason' and 'channel'
|
||||
*/
|
||||
public function getReasonChannelCombinations(): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('dnc')
|
||||
->select('DISTINCT dnc.reason, dnc.channel')
|
||||
->orderBy('dnc.reason', 'ASC')
|
||||
->addOrderBy('dnc.channel', 'ASC');
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
trait ExpressionHelperTrait
|
||||
{
|
||||
/**
|
||||
* @param QueryBuilder|\Doctrine\ORM\QueryBuilder $q
|
||||
* @param $includeIsNull true/false or null to auto determine based on operator
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function generateFilterExpression($q, $column, $operator, $parameter, $includeIsNull, ?CompositeExpression $appendTo = null)
|
||||
{
|
||||
// in/notIn for dbal will use a raw array
|
||||
if (!is_array($parameter) && !str_starts_with($parameter, ':')) {
|
||||
$parameter = ":$parameter";
|
||||
}
|
||||
|
||||
if (null === $includeIsNull) {
|
||||
// Auto determine based on negate operators
|
||||
$includeIsNull = in_array($operator, ['neq', 'notLike', 'notIn']);
|
||||
}
|
||||
|
||||
if ($includeIsNull) {
|
||||
$expr = $q->expr()->or(
|
||||
$q->expr()->$operator($column, $parameter),
|
||||
$q->expr()->isNull($column)
|
||||
);
|
||||
} else {
|
||||
$expr = $q->expr()->$operator($column, $parameter);
|
||||
}
|
||||
|
||||
if ($appendTo) {
|
||||
return $appendTo->with($expr);
|
||||
}
|
||||
|
||||
return $expr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\CommonEntity;
|
||||
|
||||
class FrequencyRule extends CommonEntity
|
||||
{
|
||||
public const TIME_DAY = 'DAY';
|
||||
|
||||
public const TIME_WEEK = 'WEEK';
|
||||
|
||||
public const TIME_MONTH = 'MONTH';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $frequencyNumber;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $frequencyTime;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $channel;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $preferredChannel = 0;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $pauseFromDate;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $pauseToDate;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_frequencyrules')
|
||||
->setCustomRepositoryClass(FrequencyRuleRepository::class)
|
||||
->addIndex(['channel'], 'channel_frequency')
|
||||
->addIndex(['lead_id', 'date_added'], 'idx_frequency_date_added');
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', false, 'frequencyRules');
|
||||
|
||||
$builder->addDateAdded();
|
||||
|
||||
$builder->addNamedField('frequencyNumber', 'smallint', 'frequency_number', true);
|
||||
|
||||
$builder->createField('frequencyTime', 'string')
|
||||
->columnName('frequency_time')
|
||||
->nullable()
|
||||
->length(25)
|
||||
->build();
|
||||
|
||||
$builder->createField('channel', 'string')
|
||||
->build();
|
||||
|
||||
$builder->createField('preferredChannel', 'boolean')
|
||||
->columnName('preferred_channel')
|
||||
->build();
|
||||
|
||||
$builder->createField('pauseFromDate', 'datetime')
|
||||
->columnName('pause_from_date')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('pauseToDate', 'datetime')
|
||||
->columnName('pause_to_date')
|
||||
->nullable()
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('frequencyRules')
|
||||
->addListProperties(
|
||||
[
|
||||
'channel',
|
||||
'frequencyNumber',
|
||||
'frequencyTime',
|
||||
'preferredChannel',
|
||||
'pauseFromDate',
|
||||
'pauseToDate',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'lead',
|
||||
'dateAdded',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead $lead
|
||||
*
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setLead($lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTimeInterface $dateAdded
|
||||
*
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setDateAdded($dateAdded)
|
||||
{
|
||||
$this->isChanged('dateAdded', $dateAdded);
|
||||
|
||||
$this->dateAdded = $dateAdded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getFrequencyNumber()
|
||||
{
|
||||
return $this->frequencyNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $frequencyNumber
|
||||
*
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setFrequencyNumber($frequencyNumber)
|
||||
{
|
||||
$this->isChanged('frequencyNumber', $frequencyNumber);
|
||||
|
||||
$this->frequencyNumber = $frequencyNumber;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFrequencyTime()
|
||||
{
|
||||
return $this->frequencyTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $frequencyTime
|
||||
*
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setFrequencyTime($frequencyTime)
|
||||
{
|
||||
$this->isChanged('frequencyTime', $frequencyTime);
|
||||
|
||||
$this->frequencyTime = $frequencyTime;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getChannel()
|
||||
{
|
||||
return $this->channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channel
|
||||
*
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setChannel($channel)
|
||||
{
|
||||
$this->isChanged('channel', $channel);
|
||||
|
||||
$this->channel = $channel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isPreferredChannel()
|
||||
{
|
||||
return $this->preferredChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getPreferredChannel()
|
||||
{
|
||||
return $this->preferredChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $preferredChannel
|
||||
*
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setPreferredChannel($preferredChannel)
|
||||
{
|
||||
$this->isChanged('preferredChannel', $preferredChannel);
|
||||
|
||||
$this->preferredChannel = $preferredChannel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getPauseFromDate()
|
||||
{
|
||||
return $this->pauseFromDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setPauseFromDate(?\DateTime $pauseFromDate = null)
|
||||
{
|
||||
$this->isChanged('pauseFromDate', $pauseFromDate);
|
||||
|
||||
$this->pauseFromDate = $pauseFromDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getPauseToDate()
|
||||
{
|
||||
return $this->pauseToDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setPauseToDate(?\DateTime $pauseToDate = null)
|
||||
{
|
||||
$this->isChanged('pauseToDate', $pauseToDate);
|
||||
|
||||
$this->pauseToDate = $pauseToDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<FrequencyRule>
|
||||
*/
|
||||
class FrequencyRuleRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* @param string $channel
|
||||
* @param array $leadIds
|
||||
* @param string|null $defaultFrequencyNumber
|
||||
* @param string|null $defaultFrequencyTime
|
||||
* @param string $statTable
|
||||
* @param string $statSentColumn
|
||||
* @param string $statContactColumn
|
||||
*/
|
||||
public function getAppliedFrequencyRules(
|
||||
$channel,
|
||||
$leadIds,
|
||||
$defaultFrequencyNumber,
|
||||
$defaultFrequencyTime,
|
||||
$statTable = 'email_stats',
|
||||
$statContactColumn = 'lead_id',
|
||||
$statSentColumn = 'date_sent',
|
||||
): array {
|
||||
if (empty($leadIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$frequencyRuleViolations = $this->getCustomFrequencyRuleViolations($channel, $leadIds, $statTable, $statContactColumn, $statSentColumn);
|
||||
|
||||
if (!$this->validateDefaultParameters($defaultFrequencyNumber, $defaultFrequencyTime)) {
|
||||
// It makes no sense to calculate default rule violations
|
||||
// if default parameters are not valid
|
||||
return $frequencyRuleViolations;
|
||||
}
|
||||
|
||||
$defaultRuleViolations = $this->getDefaultFrequencyRuleViolations($leadIds, $defaultFrequencyNumber, $defaultFrequencyTime, $statTable, $statContactColumn, $statSentColumn);
|
||||
|
||||
return array_merge($frequencyRuleViolations, $defaultRuleViolations);
|
||||
}
|
||||
|
||||
private function validateDefaultParameters(mixed $number, mixed $time): bool
|
||||
{
|
||||
return $number && $time;
|
||||
}
|
||||
|
||||
public function getFrequencyRules($channel = null, $leadIds = null): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select(
|
||||
'fr.id, fr.frequency_time, fr.frequency_number, fr.channel, fr.preferred_channel, fr.pause_from_date, fr.pause_to_date, fr.lead_id'
|
||||
)
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_frequencyrules', 'fr');
|
||||
|
||||
if ($channel) {
|
||||
$q->andWhere('fr.channel = :channel')
|
||||
->setParameter('channel', $channel);
|
||||
}
|
||||
|
||||
$groupByLeads = is_array($leadIds);
|
||||
if ($leadIds) {
|
||||
if ($groupByLeads) {
|
||||
$q->andWhere(
|
||||
$q->expr()->in('fr.lead_id', $leadIds)
|
||||
);
|
||||
} else {
|
||||
$q->andWhere('fr.lead_id = :leadId')
|
||||
->setParameter('leadId', (int) $leadIds);
|
||||
}
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$frequencyRules = [];
|
||||
|
||||
foreach ($results as $result) {
|
||||
if ($groupByLeads) {
|
||||
if (!isset($frequencyRules[$result['lead_id']])) {
|
||||
$frequencyRules[$result['lead_id']] = [];
|
||||
}
|
||||
|
||||
$frequencyRules[$result['lead_id']][$result['channel']] = $result;
|
||||
} else {
|
||||
$frequencyRules[$result['channel']] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
return $frequencyRules;
|
||||
}
|
||||
|
||||
public function getPreferredChannel($leadId): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('fr.id, fr.frequency_time, fr.frequency_number, fr.channel, fr.pause_from_date, fr.pause_to_date')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_frequencyrules', 'fr');
|
||||
$q->where('fr.preferred_channel = :preferredChannel')
|
||||
->setParameter('preferredChannel', true, 'boolean');
|
||||
if ($leadId) {
|
||||
$q->andWhere('fr.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
}
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channel
|
||||
* @param string $statTable
|
||||
* @param string $statContactColumn
|
||||
* @param string $statSentColumn
|
||||
*/
|
||||
private function getCustomFrequencyRuleViolations($channel, array $leadIds, $statTable, $statContactColumn, $statSentColumn): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select("ch.$statContactColumn, fr.frequency_number, fr.frequency_time")
|
||||
->from(MAUTIC_TABLE_PREFIX.$statTable, 'ch')
|
||||
->join('ch', MAUTIC_TABLE_PREFIX.'lead_frequencyrules', 'fr', "ch.{$statContactColumn} = fr.lead_id");
|
||||
|
||||
if ($channel) {
|
||||
$q->andWhere('fr.channel = :channel')
|
||||
->setParameter('channel', $channel);
|
||||
}
|
||||
|
||||
// Preferred channel is stored in this table so they may not have a frequency rule defined but just a preference so exclude them
|
||||
$q->andWhere('fr.frequency_time IS NOT NULL AND fr.frequency_number IS NOT NULL');
|
||||
|
||||
// Calculate the rule timeframe
|
||||
$q->andWhere(
|
||||
'(ch.'.$statSentColumn.' >= case fr.frequency_time
|
||||
when \'MONTH\' then DATE_SUB(NOW(),INTERVAL 1 MONTH)
|
||||
when \'DAY\' then DATE_SUB(NOW(),INTERVAL 1 DAY)
|
||||
when \'WEEK\' then DATE_SUB(NOW(),INTERVAL 1 WEEK)
|
||||
end)'
|
||||
);
|
||||
|
||||
$q->andWhere(
|
||||
$q->expr()->in("ch.$statContactColumn", $leadIds)
|
||||
);
|
||||
|
||||
$q->groupBy("ch.$statContactColumn, fr.frequency_time, fr.frequency_number");
|
||||
|
||||
$q->having("count(ch.$statContactColumn) >= fr.frequency_number");
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $defaultFrequencyNumber
|
||||
* @param string $defaultFrequencyTime
|
||||
* @param string $statTable
|
||||
* @param string $statContactColumn
|
||||
* @param string $statSentColumn
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getDefaultFrequencyRuleViolations(
|
||||
array $leadIds,
|
||||
$defaultFrequencyNumber,
|
||||
$defaultFrequencyTime,
|
||||
$statTable,
|
||||
$statContactColumn,
|
||||
$statSentColumn,
|
||||
) {
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$query->select("ch.$statContactColumn")
|
||||
->from(MAUTIC_TABLE_PREFIX.$statTable, 'ch');
|
||||
|
||||
switch ($defaultFrequencyTime) {
|
||||
case 'MONTH':
|
||||
$since = new \DateTime('-1 month', new \DateTimeZone('UTC'));
|
||||
break;
|
||||
case 'WEEK':
|
||||
$since = new \DateTime('-1 week', new \DateTimeZone('UTC'));
|
||||
break;
|
||||
case 'DAY':
|
||||
$since = new \DateTime('-1 day', new \DateTimeZone('UTC'));
|
||||
break;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
||||
$query->andWhere('ch.'.$statSentColumn.' >= :frequencyTime')
|
||||
->setParameter('frequencyTime', $since->format('Y-m-d H:i:s'));
|
||||
|
||||
$query->andWhere(
|
||||
$query->expr()->in("ch.$statContactColumn", $leadIds)
|
||||
);
|
||||
|
||||
$hasCustomRules = $this->tableHasRows(MAUTIC_TABLE_PREFIX.'lead_frequencyrules');
|
||||
// We don't need to check if users have custom rules if there are no records inside that table
|
||||
if ($hasCustomRules) {
|
||||
// Exclude contacts with custom rules defined
|
||||
$subQuery = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$subQuery->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_frequencyrules', 'fr')
|
||||
->where("fr.lead_id = ch.{$statContactColumn}")
|
||||
->andWhere('fr.frequency_time IS NOT NULL AND fr.frequency_number IS NOT NULL');
|
||||
$query->andWhere(
|
||||
sprintf('NOT EXISTS (%s)', $subQuery->getSQL())
|
||||
);
|
||||
}
|
||||
|
||||
$query->groupBy("ch.$statContactColumn");
|
||||
|
||||
$query->having("count(ch.$statContactColumn) >= :defaultNumber")
|
||||
->setParameter('defaultNumber', $defaultFrequencyNumber);
|
||||
|
||||
$results = $query->executeQuery()->fetchAllAssociative();
|
||||
|
||||
foreach ($results as $key => $result) {
|
||||
$results[$key]['frequency_number'] = $defaultFrequencyNumber;
|
||||
$results[$key]['frequency_time'] = $defaultFrequencyTime;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
interface IdentifierFieldEntityInterface
|
||||
{
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getDefaultIdentifierFields(): array;
|
||||
}
|
||||
@@ -0,0 +1,835 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\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\FormEntity;
|
||||
use Mautic\CoreBundle\Helper\Chart\PieChart;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
class Import extends FormEntity
|
||||
{
|
||||
/** ===== Statuses: ===== */
|
||||
/**
|
||||
* When the import entity is created for background processing.
|
||||
*/
|
||||
public const QUEUED = 1;
|
||||
|
||||
/**
|
||||
* When the background process started the import.
|
||||
*/
|
||||
public const IN_PROGRESS = 2;
|
||||
|
||||
/**
|
||||
* When the import is finished.
|
||||
*/
|
||||
public const IMPORTED = 3;
|
||||
|
||||
/**
|
||||
* When the import process failed.
|
||||
*/
|
||||
public const FAILED = 4;
|
||||
|
||||
/**
|
||||
* When the import has been stopped by a user.
|
||||
*/
|
||||
public const STOPPED = 5;
|
||||
|
||||
/**
|
||||
* When the import happens in the browser.
|
||||
*/
|
||||
public const MANUAL = 6;
|
||||
|
||||
/**
|
||||
* When the import is scheduled for later processing.
|
||||
*/
|
||||
public const DELAYED = 7;
|
||||
|
||||
/**
|
||||
* ===== Priorities: =====.
|
||||
*/
|
||||
public const LOW = 512;
|
||||
|
||||
public const NORMAL = 64;
|
||||
|
||||
public const HIGH = 1;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* Base directory of the import.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $dir;
|
||||
|
||||
/**
|
||||
* File name of the CSV file which is in the $dir.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $file = 'import.csv';
|
||||
|
||||
/**
|
||||
* Name of the original uploaded file.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $originalFile;
|
||||
|
||||
/**
|
||||
* Tolal line count of the CSV file.
|
||||
*/
|
||||
private int $lineCount = 0;
|
||||
|
||||
/**
|
||||
* Count of entities which were newly created.
|
||||
*/
|
||||
private int $insertedCount = 0;
|
||||
|
||||
/**
|
||||
* Count of entities which were updated.
|
||||
*/
|
||||
private int $updatedCount = 0;
|
||||
|
||||
/**
|
||||
* Count of ignored items.
|
||||
*/
|
||||
private int $ignoredCount = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $priority;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $status;
|
||||
|
||||
private ?\DateTimeInterface $dateStarted = null;
|
||||
|
||||
private ?\DateTimeInterface $dateEnded = null;
|
||||
|
||||
private string $object = 'lead';
|
||||
|
||||
/**
|
||||
* @var array<mixed>|null
|
||||
*/
|
||||
private $properties = [];
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->status = self::QUEUED;
|
||||
$this->priority = self::LOW;
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
$builder->setTable('imports')
|
||||
->setCustomRepositoryClass(ImportRepository::class)
|
||||
->addIndex(['object'], 'import_object')
|
||||
->addIndex(['status'], 'import_status')
|
||||
->addIndex(['priority'], 'import_priority')
|
||||
->addId()
|
||||
->addField('dir', Types::STRING)
|
||||
->addField('file', Types::STRING)
|
||||
->addNullableField('originalFile', Types::STRING, 'original_file')
|
||||
->addNamedField('lineCount', Types::INTEGER, 'line_count')
|
||||
->addNamedField('insertedCount', Types::INTEGER, 'inserted_count')
|
||||
->addNamedField('updatedCount', Types::INTEGER, 'updated_count')
|
||||
->addNamedField('ignoredCount', Types::INTEGER, 'ignored_count')
|
||||
->addField('priority', Types::INTEGER)
|
||||
->addField('status', Types::INTEGER)
|
||||
->addNullableField('dateStarted', Types::DATETIME_MUTABLE, 'date_started')
|
||||
->addNullableField('dateEnded', Types::DATETIME_MUTABLE, 'date_ended')
|
||||
->addField('object', Types::STRING)
|
||||
->addNullableField('properties', Types::JSON);
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('dir', new Assert\NotBlank(
|
||||
['message' => 'mautic.lead.import.dir.notblank']
|
||||
));
|
||||
|
||||
$metadata->addPropertyConstraint('file', new Assert\NotBlank(
|
||||
['message' => 'mautic.lead.import.file.notblank']
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('import')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'dir',
|
||||
'file',
|
||||
'originalFile',
|
||||
'lineCount',
|
||||
'insertedCount',
|
||||
'updatedCount',
|
||||
'ignoredCount',
|
||||
'priority',
|
||||
'status',
|
||||
'dateStarted',
|
||||
'dateEnded',
|
||||
'object',
|
||||
'properties',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the import has everything needed to proceed.
|
||||
*/
|
||||
public function canProceed(): bool
|
||||
{
|
||||
if (!in_array($this->getStatus(), [self::QUEUED, self::DELAYED])) {
|
||||
$this->setStatusInfo('Import could not be triggered since it is not queued nor delayed');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (false === file_exists($this->getFilePath()) || false === is_readable($this->getFilePath())) {
|
||||
$this->setStatus(self::FAILED);
|
||||
$this->setStatusInfo($this->getFile().' not found');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides if this import entity is triggered as the background
|
||||
* job or as UI process.
|
||||
*/
|
||||
public function isBackgroundProcess(): bool
|
||||
{
|
||||
return !(self::MANUAL === $this->getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dir
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setDir($dir)
|
||||
{
|
||||
$this->isChanged('dir', $dir);
|
||||
$this->dir = $dir;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDir()
|
||||
{
|
||||
return $this->dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $file
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setFile($file)
|
||||
{
|
||||
$this->isChanged('file', $file);
|
||||
$this->file = $file;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFile()
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get import file path.
|
||||
*/
|
||||
public function getFilePath(): string
|
||||
{
|
||||
return $this->getDir().'/'.$this->getFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set import file path.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setFilePath($path)
|
||||
{
|
||||
$fileName = basename($path);
|
||||
$dir = substr($path, 0, -1 * (strlen($fileName) + 1));
|
||||
|
||||
$this->setDir($dir);
|
||||
$this->setFile($fileName);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the file if exists.
|
||||
* It won't throw any exception if the file is not readable.
|
||||
* Not removing the CSV file is not considered a big trouble.
|
||||
* It will be removed on the next cache:clear.
|
||||
*/
|
||||
public function removeFile(): void
|
||||
{
|
||||
$file = $this->getFilePath();
|
||||
|
||||
if (file_exists($file) && is_writable($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $originalFile
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setOriginalFile($originalFile)
|
||||
{
|
||||
$this->isChanged('originalFile', $originalFile);
|
||||
$this->originalFile = $originalFile;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getOriginalFile()
|
||||
{
|
||||
return $this->originalFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* getName method is used by standard templates so there it is for this entity.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->getOriginalFile() ?: $this->getId();
|
||||
}
|
||||
|
||||
public function setLineCount(int $lineCount): self
|
||||
{
|
||||
$this->isChanged('lineCount', $lineCount);
|
||||
$this->lineCount = $lineCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLineCount(): int
|
||||
{
|
||||
return $this->lineCount;
|
||||
}
|
||||
|
||||
public function setInsertedCount(int $insertedCount): self
|
||||
{
|
||||
$this->isChanged('insertedCount', $insertedCount);
|
||||
$this->insertedCount = $insertedCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function increaseInsertedCount(): self
|
||||
{
|
||||
return $this->setInsertedCount($this->insertedCount + 1);
|
||||
}
|
||||
|
||||
public function getInsertedCount(): int
|
||||
{
|
||||
return $this->insertedCount;
|
||||
}
|
||||
|
||||
public function setUpdatedCount(int $updatedCount): self
|
||||
{
|
||||
$this->isChanged('updatedCount', $updatedCount);
|
||||
$this->updatedCount = $updatedCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function increaseUpdatedCount(): self
|
||||
{
|
||||
return $this->setUpdatedCount($this->updatedCount + 1);
|
||||
}
|
||||
|
||||
public function getUpdatedCount(): int
|
||||
{
|
||||
return $this->updatedCount;
|
||||
}
|
||||
|
||||
public function setIgnoredCount(int $ignoredCount): self
|
||||
{
|
||||
$this->isChanged('ignoredCount', $ignoredCount);
|
||||
$this->ignoredCount = $ignoredCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function increaseIgnoredCount(): self
|
||||
{
|
||||
return $this->setIgnoredCount($this->ignoredCount + 1);
|
||||
}
|
||||
|
||||
public function getIgnoredCount(): int
|
||||
{
|
||||
return $this->ignoredCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts how many rows have been processed so far.
|
||||
*/
|
||||
public function getProcessedRows(): int
|
||||
{
|
||||
return $this->getInsertedCount() + $this->getUpdatedCount() + $this->getIgnoredCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts current progress percentage.
|
||||
*/
|
||||
public function getProgressPercentage(): float|int
|
||||
{
|
||||
$processed = $this->getProcessedRows();
|
||||
|
||||
if ($processed && $total = $this->getLineCount()) {
|
||||
return round(($processed / $total) * 100, 2);
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $priority
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setPriority($priority)
|
||||
{
|
||||
$this->isChanged('priority', $priority);
|
||||
$this->priority = $priority;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPriority()
|
||||
{
|
||||
return $this->priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $status
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setStatus($status)
|
||||
{
|
||||
$this->isChanged('status', $status);
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getStatus()
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Twitter Bootstrap label class based on current status.
|
||||
*/
|
||||
public function getSatusLabelClass(): string
|
||||
{
|
||||
return match ($this->status) {
|
||||
self::QUEUED => 'info',
|
||||
self::IN_PROGRESS, self::MANUAL => 'primary',
|
||||
self::IMPORTED => 'success',
|
||||
self::FAILED => 'danger',
|
||||
self::STOPPED, self::DELAYED => 'warning',
|
||||
default => 'default',
|
||||
};
|
||||
}
|
||||
|
||||
public function setDateStarted(?\DateTimeInterface $dateStarted): self
|
||||
{
|
||||
$this->isChanged('dateStarted', $dateStarted);
|
||||
$this->dateStarted = $dateStarted;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDateStarted(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->dateStarted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the entity for the start of import.
|
||||
*/
|
||||
public function start(): self
|
||||
{
|
||||
if (empty($this->getDateStarted())) {
|
||||
$this->setDateStarted(new \DateTime());
|
||||
}
|
||||
|
||||
$this->setStatus(self::IN_PROGRESS);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the entity for the end of import.
|
||||
*/
|
||||
public function end($removeFile = true): self
|
||||
{
|
||||
$this->setDateEnded(new \DateTime());
|
||||
|
||||
if (self::IN_PROGRESS === $this->getStatus()) {
|
||||
$this->setStatus(self::IMPORTED);
|
||||
|
||||
if ($removeFile) {
|
||||
$this->removeFile();
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDateEnded(?\DateTimeInterface $dateEnded): self
|
||||
{
|
||||
$this->isChanged('dateEnded', $dateEnded);
|
||||
$this->dateEnded = $dateEnded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDateEnded(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->dateEnded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts how long the import has run so far.
|
||||
*
|
||||
* @return \DateInterval|null
|
||||
*/
|
||||
public function getRunTime()
|
||||
{
|
||||
$startTime = $this->getDateStarted();
|
||||
$endTime = $this->getDateEnded();
|
||||
|
||||
if (!$endTime && self::IN_PROGRESS === $this->getStatus()) {
|
||||
$endTime = $this->getDateModified();
|
||||
}
|
||||
|
||||
if ($startTime instanceof \DateTimeInterface && $endTime instanceof \DateTimeInterface) {
|
||||
return $endTime->diff($startTime);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns run time in seconds.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getRunTimeSeconds()
|
||||
{
|
||||
$startTime = $this->getDateStarted();
|
||||
$endTime = $this->getDateEnded();
|
||||
|
||||
if (!$endTime && self::IN_PROGRESS === $this->getStatus()) {
|
||||
$endTime = $this->getDateModified();
|
||||
}
|
||||
|
||||
if ($startTime instanceof \DateTime && $endTime instanceof \DateTime) {
|
||||
return $endTime->format('U') - $startTime->format('U');
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts speed in items per second.
|
||||
*/
|
||||
public function getSpeed(): float
|
||||
{
|
||||
$runtime = $this->getRunTimeSeconds();
|
||||
$processedRows = $this->getProcessedRows();
|
||||
|
||||
if ($runtime && $processedRows) {
|
||||
return round($processedRows / $runtime, 2);
|
||||
}
|
||||
|
||||
return (float) $processedRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $object
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setObject($object)
|
||||
{
|
||||
$this->isChanged('object', $object);
|
||||
$this->object = $object;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getObject()
|
||||
{
|
||||
return $this->object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Import
|
||||
*/
|
||||
public function setMatchedFields(array $fields)
|
||||
{
|
||||
$properties = $this->properties;
|
||||
$properties['fields'] = $fields;
|
||||
|
||||
return $this->setProperties($properties);
|
||||
}
|
||||
|
||||
public function setLastLineImported($line): void
|
||||
{
|
||||
$this->properties['line'] = (int) $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLastLineImported()
|
||||
{
|
||||
return $this->properties['line'] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getMatchedFields()
|
||||
{
|
||||
return empty($this->properties['fields']) ? [] : $this->properties['fields'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $properties
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setProperties($properties)
|
||||
{
|
||||
$this->isChanged('properties', $properties);
|
||||
$this->properties = $properties;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $properties
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function mergeToProperties($properties)
|
||||
{
|
||||
return $this->setProperties(array_merge($this->properties, $properties));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of default values.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDefaults()
|
||||
{
|
||||
return $this->properties['defaults'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a default value to the defaults array.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setDefault($key, $value)
|
||||
{
|
||||
return $this->mergeToProperties([
|
||||
'defaults' => array_merge($this->getDefaults(), [$key => $value]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDefault($key)
|
||||
{
|
||||
return empty($this->properties['defaults'][$key]) ? null : $this->properties['defaults'][$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set headers array to the properties.
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setHeaders(array $headers)
|
||||
{
|
||||
$properties = $this->properties;
|
||||
$properties['headers'] = $headers;
|
||||
|
||||
return $this->setProperties($properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getHeaders()
|
||||
{
|
||||
return empty($this->properties['headers']) ? [] : $this->properties['headers'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set parser config array to the properties.
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setParserConfig(array $parser)
|
||||
{
|
||||
$properties = $this->properties;
|
||||
$properties['parser'] = $parser;
|
||||
|
||||
return $this->setProperties($properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getParserConfig()
|
||||
{
|
||||
return empty($this->properties['parser']) ? [] : $this->properties['parser'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getProperties()
|
||||
{
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getStatusInfo()
|
||||
{
|
||||
return empty($this->properties['status_info']) ? 'unknown' : $this->properties['status_info'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $info
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setStatusInfo($info)
|
||||
{
|
||||
$properties = $this->properties;
|
||||
$properties['status_info'] = $info;
|
||||
|
||||
return $this->setProperties($properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite this method so we could change import status based on it.
|
||||
*
|
||||
* @param bool $isPublished
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setIsPublished($isPublished)
|
||||
{
|
||||
if ($isPublished && self::STOPPED === $this->getStatus()) {
|
||||
$this->setStatus(self::QUEUED);
|
||||
}
|
||||
|
||||
if (!$isPublished && (self::IN_PROGRESS === $this->getStatus() || self::QUEUED === $this->getStatus())) {
|
||||
$this->setStatus(self::STOPPED);
|
||||
}
|
||||
|
||||
return parent::setIsPublished($isPublished);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pie graph data for row status counts.
|
||||
*
|
||||
* @return array{labels: mixed[], datasets: mixed[]}
|
||||
*/
|
||||
public function getRowStatusesPieChart(Translator $translator): array
|
||||
{
|
||||
$chart = new PieChart();
|
||||
$chart->setDataset($translator->trans('mautic.lead.import.inserted.count'), $this->getInsertedCount());
|
||||
$chart->setDataset($translator->trans('mautic.lead.import.updated.count'), $this->getUpdatedCount());
|
||||
$chart->setDataset($translator->trans('mautic.lead.import.ignored.count'), $this->getIgnoredCount());
|
||||
|
||||
return $chart->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Import>
|
||||
*/
|
||||
class ImportRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* Count how many imports with the status is there.
|
||||
*
|
||||
* @param float $ghostDelay when is the import ghost? In hours
|
||||
* @param int $limit
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getGhostImports($ghostDelay = 2, $limit = null)
|
||||
{
|
||||
$q = $this->getQueryForStatuses([Import::IN_PROGRESS]);
|
||||
$q->select($this->getTableAlias())
|
||||
->andWhere($q->expr()->lt($this->getTableAlias().'.dateModified', ':delay'))
|
||||
->setParameter('delay', (new \DateTime())->modify('-'.$ghostDelay.' hours'));
|
||||
|
||||
if (null !== $limit) {
|
||||
$q->setFirstResult(0)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count how many imports with the status is there.
|
||||
*
|
||||
* @param int $limit
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getImportsWithStatuses(array $statuses, $limit = null)
|
||||
{
|
||||
$q = $this->getQueryForStatuses($statuses);
|
||||
$q->select($this->getTableAlias())
|
||||
->orderBy($this->getTableAlias().'.priority', 'ASC')
|
||||
->addOrderBy($this->getTableAlias().'.dateAdded', 'DESC');
|
||||
|
||||
if (null !== $limit) {
|
||||
$q->setFirstResult(0)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count how many imports with the status is there.
|
||||
*/
|
||||
public function countImportsWithStatuses(array $statuses): int
|
||||
{
|
||||
$q = $this->getQueryForStatuses($statuses);
|
||||
$q->select('COUNT(DISTINCT '.$this->getTableAlias().'.id) as theCount');
|
||||
|
||||
$results = $q->getQuery()->getSingleResult();
|
||||
|
||||
if (isset($results['theCount'])) {
|
||||
return (int) $results['theCount'];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function countImportsInProgress(): int
|
||||
{
|
||||
return $this->countImportsWithStatuses([Import::IN_PROGRESS]);
|
||||
}
|
||||
|
||||
public function getQueryForStatuses($statuses)
|
||||
{
|
||||
$q = $this->createQueryBuilder($this->getTableAlias());
|
||||
|
||||
return $q->where($q->expr()->in($this->getTableAlias().'.status', $statuses));
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'i';
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\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\ORM\Mapping as ORM;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Contact Category',
|
||||
operations: [
|
||||
new GetCollection(uriTemplate: '/contactcategories', security: "is_granted('lead:leads:viewown')"),
|
||||
new Post(uriTemplate: '/contactcategories', security: "is_granted('lead:leads:create')"),
|
||||
new Get(uriTemplate: '/contactcategories/{id}', security: "is_granted('lead:leads:viewown')"),
|
||||
new Put(uriTemplate: '/contactcategories/{id}', security: "is_granted('lead:leads:editown')"),
|
||||
new Patch(uriTemplate: '/contactcategories/{id}', security: "is_granted('lead:leads:editother')"),
|
||||
new Delete(uriTemplate: '/contactcategories/{id}', security: "is_granted('lead:leads:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['leadcategory:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
'api_included' => ['category'],
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['leadcategory:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class LeadCategory
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['leadcategory:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Category
|
||||
**/
|
||||
#[Groups(['leadcategory:read', 'leadcategory:write'])]
|
||||
private $category;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
#[Groups(['leadcategory:read', 'leadcategory:write'])]
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
#[Groups(['leadcategory:read', 'leadcategory:write'])]
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadcategory:read', 'leadcategory:write'])]
|
||||
private $manuallyRemoved = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadcategory:read', 'leadcategory:write'])]
|
||||
private $manuallyAdded = false;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_categories')
|
||||
->setCustomRepositoryClass(LeadCategoryRepository::class);
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->createManyToOne('category', Category::class)
|
||||
->addJoinColumn('category_id', 'id', false, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', false);
|
||||
|
||||
$builder->addDateAdded();
|
||||
|
||||
$builder->createField('manuallyRemoved', 'boolean')
|
||||
->columnName('manually_removed')
|
||||
->build();
|
||||
|
||||
$builder->createField('manuallyAdded', 'boolean')
|
||||
->columnName('manually_added')
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $date
|
||||
*/
|
||||
public function setDateAdded($date): void
|
||||
{
|
||||
$this->dateAdded = $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $lead
|
||||
*/
|
||||
public function setLead($lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Category
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Category $category
|
||||
*/
|
||||
public function setCategory($category): void
|
||||
{
|
||||
$this->category = $category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getManuallyRemoved()
|
||||
{
|
||||
return $this->manuallyRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $manuallyRemoved
|
||||
*/
|
||||
public function setManuallyRemoved($manuallyRemoved): void
|
||||
{
|
||||
$this->manuallyRemoved = $manuallyRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function wasManuallyRemoved()
|
||||
{
|
||||
return $this->manuallyRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getManuallyAdded()
|
||||
{
|
||||
return $this->manuallyAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $manuallyAdded
|
||||
*/
|
||||
public function setManuallyAdded($manuallyAdded): void
|
||||
{
|
||||
$this->manuallyAdded = $manuallyAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function wasManuallyAdded()
|
||||
{
|
||||
return $this->manuallyAdded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<LeadCategory>
|
||||
*/
|
||||
class LeadCategoryRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* @return array<mixed, array<string, mixed>>
|
||||
*/
|
||||
public function getLeadCategories(Lead $lead): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('lc.id, lc.category_id, lc.date_added, lc.manually_added, lc.manually_removed, c.alias, c.title')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_categories', 'lc')
|
||||
->join('lc', MAUTIC_TABLE_PREFIX.'categories', 'c', 'c.id = lc.category_id')
|
||||
->where('lc.lead_id = :lead')
|
||||
->andWhere('lc.manually_removed = 0')
|
||||
->setParameter('lead', $lead->getId());
|
||||
|
||||
$results = $q->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$categories = [];
|
||||
foreach ($results as $category) {
|
||||
$categories[$category['category_id']] = $category;
|
||||
}
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getUnsubscribedLeadCategories(Lead $lead): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('lc.id, lc.category_id, lc.date_added, lc.manually_added, lc.manually_removed, c.alias, c.title')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_categories', 'lc')
|
||||
->join('lc', MAUTIC_TABLE_PREFIX.'categories', 'c', 'c.id = lc.category_id')
|
||||
->where('lc.lead_id = :lead')
|
||||
->andWhere('lc.manually_removed = 1')
|
||||
->setParameter('lead', $lead->getId());
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$categories = [];
|
||||
foreach ($results as $category) {
|
||||
$categories[$category['category_id']] = $category;
|
||||
}
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $types
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getSubscribedAndNewCategoryIds(Lead $lead, array $types): array
|
||||
{
|
||||
$criteria = Criteria::create()
|
||||
->andWhere(Criteria::expr()->eq('manuallyRemoved', 1));
|
||||
|
||||
return $this->getLeadCategoriesMapping($lead, $types, $criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $types
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getNonAssociatedCategoryIdsForAContact(Lead $lead, array $types): array
|
||||
{
|
||||
return $this->getLeadCategoriesMapping($lead, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $types
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
private function getLeadCategoriesMapping(Lead $lead, array $types, ?Criteria $criteria = null): array
|
||||
{
|
||||
$parentQ = $this->getEntityManager()->getRepository(Category::class)->createQueryBuilder('c');
|
||||
$parentQ->select('c.id');
|
||||
$parentQ->where('c.isPublished = :isPublished');
|
||||
$parentQ->setParameter('isPublished', 1);
|
||||
$parentQ->andWhere($parentQ->expr()->in('c.bundle', ':bundles'));
|
||||
$parentQ->setParameter('bundles', $types, ArrayParameterType::STRING);
|
||||
|
||||
// Get the category ids for particular lead
|
||||
$subQ = $this->getEntityManager()->getRepository(LeadCategory::class)->createQueryBuilder('lc');
|
||||
$subQ->select('IDENTITY(lc.category)');
|
||||
$subQ->where($subQ->expr()->eq('lc.lead', ':leadId'));
|
||||
$subQ->setParameter('leadId', $lead->getId());
|
||||
|
||||
if ($criteria) {
|
||||
$subQ->addCriteria($criteria);
|
||||
}
|
||||
|
||||
// Add sub-query
|
||||
$parentQ->andWhere($parentQ->expr()->notIn('c.id', $subQ->getDQL()));
|
||||
|
||||
// Add sub-query parameter.
|
||||
foreach ($subQ->getParameters() as $parameter) {
|
||||
$parentQ->setParameter($parameter->getName(), $parameter->getValue(), $parameter->getType());
|
||||
}
|
||||
|
||||
$leadCategories = $parentQ->getQuery()->getResult();
|
||||
|
||||
$leadCategoryList = [];
|
||||
foreach ($leadCategories as $category) {
|
||||
$id = (int) $category['id'];
|
||||
|
||||
$leadCategoryList[$id] = $id;
|
||||
}
|
||||
|
||||
return $leadCategoryList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class LeadDevice
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $clientInfo = [];
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $device;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $deviceOsName;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $deviceOsShortName;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $deviceOsVersion;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $deviceOsPlatform;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $deviceBrand;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $deviceModel;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $trackingId;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_devices')
|
||||
->setCustomRepositoryClass(LeadDeviceRepository::class)
|
||||
->addIndex(['date_added'], 'date_added_search')
|
||||
->addIndex(['device'], 'device_search')
|
||||
->addIndex(['device_os_name'], 'device_os_name_search')
|
||||
->addIndex(['device_os_shortname'], 'device_os_shortname_search')
|
||||
->addIndex(['device_os_version'], 'device_os_version_search')
|
||||
->addIndex(['device_os_platform'], 'device_os_platform_search')
|
||||
->addIndex(['device_brand'], 'device_brand_search')
|
||||
->addIndex(['device_model'], 'device_model_search');
|
||||
|
||||
$builder->addBigIntIdField();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', false);
|
||||
|
||||
$builder->addDateAdded();
|
||||
|
||||
$builder->createField('clientInfo', 'array')
|
||||
->columnName('client_info')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->addNullableField('device', 'string');
|
||||
|
||||
$builder->createField('deviceOsName', 'string')
|
||||
->columnName('device_os_name')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('deviceOsShortName', 'string')
|
||||
->columnName('device_os_shortname')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('deviceOsVersion', 'string')
|
||||
->columnName('device_os_version')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('deviceOsPlatform', 'string')
|
||||
->columnName('device_os_platform')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('deviceBrand', 'string')
|
||||
->columnName('device_brand')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('deviceModel', 'string')
|
||||
->columnName('device_model')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('trackingId', 'string')
|
||||
->columnName('tracking_id')
|
||||
->unique()
|
||||
->nullable()
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('leadDevice')
|
||||
->addProperties(
|
||||
[
|
||||
'id',
|
||||
'lead',
|
||||
'clientInfo',
|
||||
'device',
|
||||
'deviceBrand',
|
||||
'deviceModel',
|
||||
'deviceOsName',
|
||||
'deviceOsShortName',
|
||||
'deviceOsVersion',
|
||||
'deviceOsPlatform',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return (int) $this->id;
|
||||
}
|
||||
|
||||
public function getSignature(): string
|
||||
{
|
||||
return md5(json_encode($this->clientInfo).$this->device.$this->deviceOsName.$this->deviceOsPlatform.$this->deviceBrand.$this->deviceModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getClientInfo()
|
||||
{
|
||||
return $this->clientInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $clientInfo
|
||||
*/
|
||||
public function setClientInfo($clientInfo): void
|
||||
{
|
||||
$this->clientInfo = $clientInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDevice()
|
||||
{
|
||||
return $this->device;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $device
|
||||
*/
|
||||
public function setDevice($device): void
|
||||
{
|
||||
$this->device = $device;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDeviceBrand()
|
||||
{
|
||||
return $this->deviceBrand;
|
||||
}
|
||||
|
||||
public function setDeviceBrand($brand): void
|
||||
{
|
||||
$this->deviceBrand = $brand;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDeviceModel()
|
||||
{
|
||||
return $this->deviceModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $deviceModel
|
||||
*/
|
||||
public function setDeviceModel($deviceModel): void
|
||||
{
|
||||
$this->deviceModel = $deviceModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDeviceOsName()
|
||||
{
|
||||
return $this->deviceOsName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $deviceOsName
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDeviceOsName($deviceOsName)
|
||||
{
|
||||
$this->deviceOsName = $deviceOsName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDeviceOsShortName()
|
||||
{
|
||||
return $this->deviceOsShortName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $deviceOsShortName
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDeviceOsShortName($deviceOsShortName)
|
||||
{
|
||||
$this->deviceOsShortName = $deviceOsShortName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDeviceOsVersion()
|
||||
{
|
||||
return $this->deviceOsVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $deviceOsVersion
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDeviceOsVersion($deviceOsVersion)
|
||||
{
|
||||
$this->deviceOsVersion = $deviceOsVersion;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDeviceOsPlatform()
|
||||
{
|
||||
return $this->deviceOsPlatform;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $deviceOsPlatform
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDeviceOsPlatform($deviceOsPlatform)
|
||||
{
|
||||
$this->deviceOsPlatform = $deviceOsPlatform;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDeviceOs()
|
||||
{
|
||||
return $this->deviceOsName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $deviceOs
|
||||
*/
|
||||
public function setDeviceOs($deviceOs): void
|
||||
{
|
||||
if (isset($deviceOs['name'])) {
|
||||
$this->deviceOsName = $deviceOs['name'];
|
||||
}
|
||||
if (isset($deviceOs['short_name'])) {
|
||||
$this->deviceOsShortName = $deviceOs['short_name'];
|
||||
}
|
||||
if (isset($deviceOs['version'])) {
|
||||
$this->deviceOsVersion = $deviceOs['version'];
|
||||
}
|
||||
if (isset($deviceOs['platform'])) {
|
||||
$this->deviceOsPlatform = $deviceOs['platform'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTrackingId()
|
||||
{
|
||||
return $this->trackingId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $trackingId
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setTrackingId($trackingId)
|
||||
{
|
||||
$this->trackingId = $trackingId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLead(Lead $lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $dateAdded
|
||||
*/
|
||||
public function setDateAdded($dateAdded): void
|
||||
{
|
||||
$this->dateAdded = $dateAdded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<LeadDevice>
|
||||
*/
|
||||
class LeadDeviceRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* {@inhertidoc}.
|
||||
*
|
||||
* @return Paginator
|
||||
*/
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$q = $this
|
||||
->createQueryBuilder($this->getTableAlias())
|
||||
->select($this->getTableAlias());
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'd';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDevice($lead, $deviceNames = null, $deviceBrands = null, $deviceModels = null, $deviceOss = null, $deviceId = null)
|
||||
{
|
||||
$selectQuery = $this->_em->getConnection()->createQueryBuilder();
|
||||
$selectQuery->select('es.id as id, es.device as device')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_devices', 'es');
|
||||
|
||||
if (null !== $deviceNames) {
|
||||
if (!is_array($deviceNames)) {
|
||||
$deviceNames = [$deviceNames];
|
||||
}
|
||||
|
||||
$or = $selectQuery->expr()->or(
|
||||
...array_map(fn ($key, $deviceName) => $selectQuery->expr()->eq('es.device', ':device'.$key), array_keys($deviceNames), $deviceNames)
|
||||
);
|
||||
$selectQuery->andWhere($or);
|
||||
foreach ($deviceNames as $key => $deviceName) {
|
||||
$selectQuery->setParameter('device'.$key, $deviceName);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $deviceBrands) {
|
||||
if (!is_array($deviceBrands)) {
|
||||
$deviceBrands = [$deviceBrands];
|
||||
}
|
||||
|
||||
$or = $selectQuery->expr()->or(
|
||||
...array_map(fn ($key, $deviceBrand) => $selectQuery->expr()->eq('es.device_brand', ':deviceBrand'.$key), array_keys($deviceBrands), $deviceBrands)
|
||||
);
|
||||
$selectQuery->andWhere($or);
|
||||
foreach ($deviceBrands as $key => $deviceBrand) {
|
||||
$selectQuery->setParameter('deviceBrand'.$key, $deviceBrand);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $deviceModels) {
|
||||
if (!is_array($deviceModels)) {
|
||||
$deviceModels = [$deviceModels];
|
||||
}
|
||||
|
||||
$or = $selectQuery->expr()->or(
|
||||
...array_map(fn ($key, $deviceModel) => $selectQuery->expr()->eq('es.device_model', ':deviceModel'.$key), array_keys($deviceModels), $deviceModels)
|
||||
);
|
||||
$selectQuery->andWhere($or);
|
||||
foreach ($deviceModels as $key => $deviceModel) {
|
||||
$selectQuery->setParameter('deviceModel'.$key, $deviceModel);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $deviceOss) {
|
||||
if (!is_array($deviceOss)) {
|
||||
$deviceOss = [$deviceOss];
|
||||
}
|
||||
|
||||
$or = $selectQuery->expr()->or(
|
||||
...array_map(fn ($key, $deviceOs) => $selectQuery->expr()->eq('es.device_os_name', ':deviceOs'.$key), array_keys($deviceOss), $deviceOss)
|
||||
);
|
||||
$selectQuery->andWhere($or);
|
||||
foreach ($deviceOss as $key => $deviceOs) {
|
||||
$selectQuery->setParameter('deviceOs'.$key, $deviceOs);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $deviceId) {
|
||||
$selectQuery->andWhere(
|
||||
$selectQuery->expr()->eq('es.id', $deviceId)
|
||||
);
|
||||
} elseif (null !== $lead) {
|
||||
$selectQuery->andWhere(
|
||||
$selectQuery->expr()->eq('es.lead_id', $lead->getId())
|
||||
);
|
||||
}
|
||||
|
||||
// get totals
|
||||
$device = $selectQuery->executeQuery()->fetchAllAssociative();
|
||||
|
||||
return (!empty($device)) ? $device[0] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $trackingId
|
||||
*
|
||||
* @return LeadDevice|null
|
||||
*/
|
||||
public function getByTrackingId($trackingId)
|
||||
{
|
||||
/** @var LeadDevice $leadDevice */
|
||||
$leadDevice = $this->findOneBy([
|
||||
'trackingId' => $trackingId,
|
||||
]);
|
||||
|
||||
return $leadDevice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is at least one device with filled tracking code assigned to Lead.
|
||||
*/
|
||||
public function isAnyLeadDeviceTracked(Lead $lead): bool
|
||||
{
|
||||
$alias = $this->getTableAlias();
|
||||
$qb = $this->createQueryBuilder($alias);
|
||||
$qb->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq($alias.'.lead', ':lead'),
|
||||
$qb->expr()->isNotNull($alias.'.trackingId')
|
||||
)
|
||||
)
|
||||
->setParameter('lead', $lead);
|
||||
|
||||
$devices = $qb->getQuery()->getResult();
|
||||
|
||||
return !empty($devices);
|
||||
}
|
||||
|
||||
public function getLeadDevices(Lead $lead): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
return $qb->select('*')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_devices', 'es')
|
||||
->where('lead_id = :leadId')
|
||||
->setParameter('leadId', (int) $lead->getId())
|
||||
->orderBy('date_added', 'desc')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'lead_devices')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
public function findExistingDevice(LeadDevice $device): ?LeadDevice
|
||||
{
|
||||
return $this->findOneBy(
|
||||
[
|
||||
'lead' => $device->getLead(),
|
||||
'device' => $device->getDevice(),
|
||||
'deviceBrand' => $device->getDeviceBrand(),
|
||||
'deviceModel' => $device->getDeviceModel(),
|
||||
'deviceOsName' => $device->getDeviceOsName(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
/**
|
||||
* Store here contact events.
|
||||
*/
|
||||
class LeadEventLog
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const INDEX_SEARCH = 'IDX_SEARCH';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @var Lead|null
|
||||
*/
|
||||
protected $lead;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
protected $userId;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $userName;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $bundle;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $object;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
protected $objectId;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $action;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
protected $dateAdded;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
private $properties = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setDateAdded(new \DateTime());
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
$builder->setTable('lead_event_log')
|
||||
->setCustomRepositoryClass(LeadEventLogRepository::class)
|
||||
->addIndex(['lead_id'], 'lead_id_index')
|
||||
->addIndex(['object', 'object_id'], 'lead_object_index')
|
||||
->addIndex(['bundle', 'object', 'action', 'object_id'], 'lead_timeline_index')
|
||||
->addIndex(['bundle', 'object', 'action', 'object_id', 'date_added'], self::INDEX_SEARCH)
|
||||
->addIndex(['action'], 'lead_timeline_action_index')
|
||||
->addIndex(['date_added'], 'lead_date_added_index')
|
||||
->addBigIntIdField()
|
||||
->addNullableField('userId', Types::INTEGER, 'user_id')
|
||||
->addNullableField('userName', Types::STRING, 'user_name')
|
||||
->addNullableField('bundle', Types::STRING)
|
||||
->addNullableField('object', Types::STRING)
|
||||
->addNullableField('action', Types::STRING)
|
||||
->addNullableField('objectId', Types::INTEGER, 'object_id')
|
||||
->addNamedField('dateAdded', Types::DATETIME_MUTABLE, 'date_added')
|
||||
->addNullableField('properties', Types::JSON);
|
||||
|
||||
$builder->createManyToOne('lead', Lead::class)
|
||||
->addJoinColumn('lead_id', 'id', true, false, 'CASCADE')
|
||||
->inversedBy('eventLog')
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('import')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'leadId',
|
||||
'userId',
|
||||
'userName',
|
||||
'bundle',
|
||||
'object',
|
||||
'action',
|
||||
'objectId',
|
||||
'dateAdded',
|
||||
'properties',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*/
|
||||
public function getId(): int
|
||||
{
|
||||
return (int) $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lead.
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setLead(Lead $lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lead.
|
||||
*
|
||||
* @return Lead|null
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set userId.
|
||||
*
|
||||
* @param int $userId
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setUserId($userId)
|
||||
{
|
||||
$this->userId = $userId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get userId.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getUserId()
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set object.
|
||||
*
|
||||
* @param string $object
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setObject($object)
|
||||
{
|
||||
$this->object = $object;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getObject()
|
||||
{
|
||||
return $this->object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set objectId.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setObjectId($objectId)
|
||||
{
|
||||
$this->objectId = $objectId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get objectId.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getObjectId()
|
||||
{
|
||||
return $this->objectId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set action.
|
||||
*
|
||||
* @param string $action
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setAction($action)
|
||||
{
|
||||
$this->action = $action;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAction()
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set properties.
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setProperties(array $properties)
|
||||
{
|
||||
$this->properties = $properties;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set one property into the properties array.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function addProperty($key, $value)
|
||||
{
|
||||
$this->properties[$key] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get properties.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getProperties()
|
||||
{
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set dateAdded.
|
||||
*
|
||||
* @param \DateTime $dateAdded
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setDateAdded($dateAdded)
|
||||
{
|
||||
$this->dateAdded = $dateAdded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dateAdded.
|
||||
*
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set bundle.
|
||||
*
|
||||
* @param string $bundle
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setBundle($bundle)
|
||||
{
|
||||
$this->bundle = $bundle;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bundle.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBundle()
|
||||
{
|
||||
return $this->bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set userName.
|
||||
*
|
||||
* @param string $userName
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setUserName($userName)
|
||||
{
|
||||
$this->userName = $userName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get userName.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserName()
|
||||
{
|
||||
return $this->userName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<LeadEventLog>
|
||||
*/
|
||||
class LeadEventLogRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
|
||||
/**
|
||||
* Returns paginator with failed rows.
|
||||
*
|
||||
* @param string $bundle
|
||||
* @param string $object
|
||||
*
|
||||
* @return Paginator
|
||||
*/
|
||||
public function getFailedRows($importId, array $args = [], $bundle = 'lead', $object = 'import')
|
||||
{
|
||||
return $this->getSpecificRows($importId, 'failed', $args, $bundle, $object);
|
||||
}
|
||||
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$entities = parent::getEntities($args);
|
||||
$entities = iterator_to_array($entities);
|
||||
|
||||
foreach ($entities as $key => $row) {
|
||||
if (
|
||||
isset($row['properties']['error'])
|
||||
&& preg_match('/SQLSTATE\[\w+\]: (.*)/', $row['properties']['error'], $matches)
|
||||
) {
|
||||
$entities[$key]['properties']['error'] = $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns paginator with specific type of rows.
|
||||
*
|
||||
* @param string $bundle
|
||||
* @param string $object
|
||||
*
|
||||
* @return Paginator
|
||||
*/
|
||||
public function getSpecificRows($objectId, $action, array $args = [], $bundle = 'lead', $object = 'import')
|
||||
{
|
||||
return $this->getEntities(
|
||||
array_merge(
|
||||
[
|
||||
'start' => 0,
|
||||
'limit' => 100,
|
||||
'orderBy' => $this->getTableAlias().'.dateAdded',
|
||||
'orderByDir' => 'ASC',
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => $this->getTableAlias().'.bundle',
|
||||
'expr' => 'eq',
|
||||
'value' => $bundle,
|
||||
],
|
||||
[
|
||||
'column' => $this->getTableAlias().'.object',
|
||||
'expr' => 'eq',
|
||||
'value' => $object,
|
||||
],
|
||||
[
|
||||
'column' => $this->getTableAlias().'.action',
|
||||
'expr' => 'eq',
|
||||
'value' => $action,
|
||||
],
|
||||
[
|
||||
'column' => $this->getTableAlias().'.objectId',
|
||||
'expr' => 'eq',
|
||||
'value' => $objectId,
|
||||
],
|
||||
],
|
||||
],
|
||||
'hydration_mode' => 'HYDRATE_ARRAY',
|
||||
],
|
||||
$args
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string $bundle
|
||||
* @param ?string $object
|
||||
* @param array|string|null $actions
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getEvents(?Lead $contact = null, $bundle = null, $object = null, $actions = null, array $options = [])
|
||||
{
|
||||
$alias = $this->getTableAlias();
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('*')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_event_log', $alias);
|
||||
|
||||
if ($contact) {
|
||||
$qb->andWhere($alias.'.lead_id = :lead')
|
||||
->setParameter('lead', $contact->getId());
|
||||
}
|
||||
|
||||
if ($bundle) {
|
||||
$qb->andWhere($alias.'.bundle = :bundle')
|
||||
->setParameter('bundle', $bundle);
|
||||
}
|
||||
|
||||
if ($object) {
|
||||
$qb->andWhere($alias.'.object = :object')
|
||||
->setParameter('object', $object);
|
||||
}
|
||||
|
||||
if ($actions) {
|
||||
if (is_array($actions)) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->in($alias.'.action', ':actions')
|
||||
)
|
||||
->setParameter('actions', $actions, ArrayParameterType::STRING);
|
||||
} else {
|
||||
$qb->andWhere($alias.'.action = :action')
|
||||
->setParameter('action', $actions);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($options['search'])) {
|
||||
$qb->andWhere($qb->expr()->like('LOWER('.$alias.'.properties)', $qb->expr()->literal('%'.strtolower($options['search']).'%')));
|
||||
}
|
||||
|
||||
return $this->getTimelineResults($qb, $options, $alias.'.action', $alias.'.date_added', [], ['date_added'], null, $alias.'.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*
|
||||
* @param int $fromLeadId
|
||||
* @param int $toLeadId
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'lead_event_log')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines default table alias for lead_event_log table.
|
||||
*/
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'lel';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,934 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\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\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\CacheInvalidateInterface;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Mautic\LeadBundle\Field\DTO\CustomFieldObject;
|
||||
use Mautic\LeadBundle\Form\Validator\Constraints\FieldAliasKeyword;
|
||||
use Mautic\LeadBundle\Validator\LeadFieldMinimumLength;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(security: "is_granted('lead:leads:viewown')"),
|
||||
new Post(security: "is_granted('lead:leads:create')"),
|
||||
new Get(security: "is_granted('lead:leads:viewown')"),
|
||||
new Put(security: "is_granted('lead:leads:editown')"),
|
||||
new Patch(security: "is_granted('lead:leads:editother')"),
|
||||
new Delete(security: "is_granted('lead:leads:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['leadfield:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['leadfield:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class LeadField extends FormEntity implements CacheInvalidateInterface, UuidInterface
|
||||
{
|
||||
use UuidTrait;
|
||||
|
||||
public const MAX_VARCHAR_LENGTH = 191;
|
||||
public const CACHE_NAMESPACE = 'LeadField';
|
||||
public const TYPES_SUPPORTING_LENGTH = [
|
||||
'text',
|
||||
'select',
|
||||
'phone',
|
||||
'url',
|
||||
'email',
|
||||
];
|
||||
public const ENTITY_NAME = 'lead_field';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['leadfield:read'])]
|
||||
private $id;
|
||||
|
||||
private bool $isCloned = false;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $label;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $alias;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $type = 'text';
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $group = 'core';
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $defaultValue;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isRequired = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isFixed = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isVisible = true;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isShortVisible = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isListable = true;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isPubliclyUpdatable = false;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isUniqueIdentifer = false;
|
||||
|
||||
/**
|
||||
* Workaround for incorrectly spelled $isUniqueIdentifer.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isUniqueIdentifier = false;
|
||||
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private ?int $charLengthLimit = 64;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $order = 1;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $object = 'lead';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $properties = [];
|
||||
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private bool $isIndex = false;
|
||||
|
||||
/**
|
||||
* The column in lead_fields table was not created yet if this property is true.
|
||||
* Entity cannot be published and we cannot work with it until column is created.
|
||||
*/
|
||||
#[Groups(['leadfield:read'])]
|
||||
private bool $columnIsNotCreated = false;
|
||||
|
||||
/**
|
||||
* The column in lead_fields table was not removed yet if this property is true.
|
||||
*/
|
||||
#[Groups(['leadfield:read'])]
|
||||
private bool $columnIsNotRemoved = false;
|
||||
|
||||
/**
|
||||
* This property contains an original value for $isPublished.
|
||||
* $isPublished is always set on false if $columnIsNotCreated is true.
|
||||
*/
|
||||
#[Groups(['leadfield:read'])]
|
||||
private bool $originalIsPublishedValue = false;
|
||||
|
||||
/**
|
||||
* @var CustomFieldObject
|
||||
*/
|
||||
private $customFieldObject;
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
$this->isCloned = true;
|
||||
$this->order = 0;
|
||||
$this->isFixed = false;
|
||||
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
$builder->addLifecycleEvent('identifierWorkaround', 'postLoad');
|
||||
|
||||
$builder->setTable('lead_fields')
|
||||
->setCustomRepositoryClass(LeadFieldRepository::class)
|
||||
->addIndex(['object', 'field_order', 'is_published'], 'idx_object_field_order_is_published');
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->addField('label', 'string');
|
||||
|
||||
$builder->addField('alias', 'string');
|
||||
|
||||
$builder->createField('type', 'string')
|
||||
->length(50)
|
||||
->build();
|
||||
|
||||
$builder->createField('group', 'string')
|
||||
->columnName('field_group')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('defaultValue', 'string')
|
||||
->columnName('default_value')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('isRequired', 'boolean')
|
||||
->columnName('is_required')
|
||||
->build();
|
||||
|
||||
$builder->createField('isFixed', 'boolean')
|
||||
->columnName('is_fixed')
|
||||
->build();
|
||||
|
||||
$builder->createField('isVisible', 'boolean')
|
||||
->columnName('is_visible')
|
||||
->build();
|
||||
|
||||
$builder->createField('isShortVisible', 'boolean')
|
||||
->columnName('is_short_visible')
|
||||
->nullable(false)
|
||||
->option('default', false)
|
||||
->build();
|
||||
|
||||
$builder->createField('isListable', 'boolean')
|
||||
->columnName('is_listable')
|
||||
->build();
|
||||
|
||||
$builder->createField('isPubliclyUpdatable', 'boolean')
|
||||
->columnName('is_publicly_updatable')
|
||||
->build();
|
||||
|
||||
$builder->addNullableField('isUniqueIdentifer', 'boolean', 'is_unique_identifer');
|
||||
|
||||
$builder->createField('isIndex', 'boolean')
|
||||
->columnName('is_index')
|
||||
->option('default', false)
|
||||
->nullable(false)
|
||||
->build();
|
||||
|
||||
$builder->createField('charLengthLimit', 'integer')
|
||||
->columnName('char_length_limit')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('order', 'integer')
|
||||
->columnName('field_order')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('object', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('properties', 'array')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('columnIsNotCreated', 'boolean')
|
||||
->columnName('column_is_not_created')
|
||||
->option('default', false)
|
||||
->build();
|
||||
|
||||
$builder->createField('columnIsNotRemoved', Types::BOOLEAN)
|
||||
->columnName('column_is_not_removed')
|
||||
->option('default', false)
|
||||
->build();
|
||||
|
||||
$builder->createField('originalIsPublishedValue', 'boolean')
|
||||
->columnName('original_is_published_value')
|
||||
->option('default', false)
|
||||
->build();
|
||||
|
||||
static::addUuidField($builder);
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('label', new Assert\NotBlank(
|
||||
['message' => 'mautic.lead.field.label.notblank']
|
||||
));
|
||||
|
||||
$metadata->addPropertyConstraint('label', new Assert\Length([
|
||||
'max' => 191,
|
||||
'maxMessage' => 'mautic.lead.field.label.maxlength',
|
||||
]));
|
||||
|
||||
$metadata->addConstraint(new UniqueEntity([
|
||||
'fields' => ['alias'],
|
||||
'message' => 'mautic.lead.field.alias.unique',
|
||||
]));
|
||||
|
||||
$metadata->addConstraint(new Assert\Callback(
|
||||
function (LeadField $field, ExecutionContextInterface $context): void {
|
||||
$violations = $context->getValidator()->validate($field, [new FieldAliasKeyword()]);
|
||||
|
||||
if ($violations->count() > 0) {
|
||||
$context->buildViolation($violations->get(0)->getMessage())
|
||||
->atPath('alias')
|
||||
->addViolation();
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
$metadata->addConstraint(new LeadFieldMinimumLength());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('leadField')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'label',
|
||||
'alias',
|
||||
'type',
|
||||
'group',
|
||||
'order',
|
||||
'object',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'defaultValue',
|
||||
'isRequired',
|
||||
'isFixed',
|
||||
'isListable',
|
||||
'isVisible',
|
||||
'isVisible',
|
||||
'isShortVisible',
|
||||
'isUniqueIdentifier',
|
||||
'isPubliclyUpdatable',
|
||||
'properties',
|
||||
'isIndex',
|
||||
'charLengthLimit',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
public function setId(?int $id = null): void
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getIsCloned(): bool
|
||||
{
|
||||
return $this->isCloned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set label.
|
||||
*
|
||||
* @param string $label
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setLabel($label)
|
||||
{
|
||||
$this->isChanged('label', $label);
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get label.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLabel()
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy function to setLabel().
|
||||
*
|
||||
* @param string $label
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setName($label)
|
||||
{
|
||||
$this->isChanged('label', $label);
|
||||
|
||||
return $this->setLabel($label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy function for getLabel().
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->getLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set type.
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->isChanged('type', $type);
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set defaultValue.
|
||||
*
|
||||
* @param string|array<string> $defaultValue
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setDefaultValue($defaultValue)
|
||||
{
|
||||
$defaultValue = is_array($defaultValue) ? implode('|', $defaultValue) : $defaultValue;
|
||||
$this->isChanged('defaultValue', $defaultValue);
|
||||
$this->defaultValue = $defaultValue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get defaultValue.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDefaultValue()
|
||||
{
|
||||
return $this->defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set isRequired.
|
||||
*
|
||||
* @param bool $isRequired
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setIsRequired($isRequired)
|
||||
{
|
||||
$this->isChanged('isRequired', $isRequired);
|
||||
$this->isRequired = $isRequired;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isRequired.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsRequired()
|
||||
{
|
||||
return $this->isRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy to getIsRequired().
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isRequired()
|
||||
{
|
||||
return $this->getIsRequired();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set isFixed.
|
||||
*
|
||||
* @param bool $isFixed
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setIsFixed($isFixed)
|
||||
{
|
||||
$this->isFixed = $isFixed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isFixed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsFixed()
|
||||
{
|
||||
return $this->isFixed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy to getIsFixed().
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFixed()
|
||||
{
|
||||
return $this->getIsFixed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $properties
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setProperties($properties)
|
||||
{
|
||||
$this->isChanged('properties', $properties);
|
||||
$this->properties = $properties;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getProperties()
|
||||
{
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set order.
|
||||
*
|
||||
* @param int $order
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setOrder($order)
|
||||
{
|
||||
$this->isChanged('order', $order);
|
||||
$this->order = $order;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getObject()
|
||||
{
|
||||
return $this->object;
|
||||
}
|
||||
|
||||
public function setCharLengthLimit(?int $charLengthLimit): LeadField
|
||||
{
|
||||
$this->isChanged('charLengthLimit', $charLengthLimit);
|
||||
$this->charLengthLimit = $charLengthLimit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCharLengthLimit(): ?int
|
||||
{
|
||||
return $this->charLengthLimit;
|
||||
}
|
||||
|
||||
public function getCustomFieldObject(): string
|
||||
{
|
||||
if (!$this->customFieldObject) {
|
||||
$this->customFieldObject = new CustomFieldObject($this);
|
||||
}
|
||||
|
||||
return $this->customFieldObject->getObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set object.
|
||||
*
|
||||
* @param string $object
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setObject($object)
|
||||
{
|
||||
$this->isChanged('object', $object);
|
||||
$this->object = $object;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getOrder()
|
||||
{
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set isVisible.
|
||||
*
|
||||
* @param bool $isVisible
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setIsVisible($isVisible)
|
||||
{
|
||||
$this->isChanged('isVisible', $isVisible);
|
||||
$this->isVisible = $isVisible;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isVisible.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsVisible()
|
||||
{
|
||||
return $this->isVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy to getIsVisible().
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isVisible()
|
||||
{
|
||||
return $this->getIsVisible();
|
||||
}
|
||||
|
||||
public function setIsShortVisible(?bool $isShortVisible): self
|
||||
{
|
||||
$isShortVisible = $isShortVisible ?? false;
|
||||
$this->isChanged('isShortVisible', $isShortVisible);
|
||||
$this->isShortVisible = $isShortVisible;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isShortVisible.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsShortVisible()
|
||||
{
|
||||
return $this->isShortVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy to getIsShortVisible().
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isShortVisible()
|
||||
{
|
||||
return $this->getIsShortVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique identifer state of the field.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsUniqueIdentifer()
|
||||
{
|
||||
return $this->isUniqueIdentifer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the unique identifer state of the field.
|
||||
*
|
||||
* @param mixed $isUniqueIdentifer
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setIsUniqueIdentifer($isUniqueIdentifer)
|
||||
{
|
||||
if ($isUniqueIdentifer) {
|
||||
$this->isIndex = true;
|
||||
}
|
||||
|
||||
$this->isUniqueIdentifer = $this->isUniqueIdentifier = $isUniqueIdentifer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for incorrectly spelled setIsUniqueIdentifer.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsUniqueIdentifier()
|
||||
{
|
||||
return $this->getIsUniqueIdentifer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for incorrectly spelled setIsUniqueIdentifer.
|
||||
*
|
||||
* @param mixed $isUniqueIdentifier
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setIsUniqueIdentifier($isUniqueIdentifier)
|
||||
{
|
||||
return $this->setIsUniqueIdentifer($isUniqueIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set alias.
|
||||
*
|
||||
* @param string $alias
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setAlias($alias)
|
||||
{
|
||||
$this->isChanged('alias', $alias);
|
||||
$this->alias = $alias;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get alias.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAlias()
|
||||
{
|
||||
return $this->alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set isListable.
|
||||
*
|
||||
* @param bool $isListable
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setIsListable($isListable)
|
||||
{
|
||||
$this->isChanged('isListable', $isListable);
|
||||
$this->isListable = $isListable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isListable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsListable()
|
||||
{
|
||||
return $this->isListable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy to getIsListable().
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isListable()
|
||||
{
|
||||
return $this->getIsListable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getGroup()
|
||||
{
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $group
|
||||
*/
|
||||
public function setGroup($group): void
|
||||
{
|
||||
$this->group = $group;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getIsPubliclyUpdatable()
|
||||
{
|
||||
return $this->isPubliclyUpdatable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $isPubliclyUpdatable
|
||||
*/
|
||||
public function setIsPubliclyUpdatable($isPubliclyUpdatable): void
|
||||
{
|
||||
$this->isPubliclyUpdatable = (bool) $isPubliclyUpdatable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for mispelled isUniqueIdentifer.
|
||||
*/
|
||||
public function identifierWorkaround(): void
|
||||
{
|
||||
$this->isUniqueIdentifier = $this->isUniqueIdentifer;
|
||||
}
|
||||
|
||||
public function isNew(): bool
|
||||
{
|
||||
return $this->getId() ? false : true;
|
||||
}
|
||||
|
||||
public function getColumnIsNotCreated(): bool
|
||||
{
|
||||
return $this->columnIsNotCreated;
|
||||
}
|
||||
|
||||
public function getColumnIsNotRemoved(): bool
|
||||
{
|
||||
return $this->columnIsNotRemoved;
|
||||
}
|
||||
|
||||
public function setColumnIsNotCreated(): void
|
||||
{
|
||||
$this->columnIsNotCreated = true;
|
||||
$this->originalIsPublishedValue = $this->getIsPublished();
|
||||
$this->setIsPublished(false);
|
||||
}
|
||||
|
||||
public function setColumnIsNotRemoved(): void
|
||||
{
|
||||
$this->columnIsNotRemoved = true;
|
||||
$this->setIsPublished(false);
|
||||
}
|
||||
|
||||
public function setColumnWasCreated(): void
|
||||
{
|
||||
$this->columnIsNotCreated = false;
|
||||
$this->setIsPublished($this->getOriginalIsPublishedValue());
|
||||
}
|
||||
|
||||
public function disablePublishChange(): bool
|
||||
{
|
||||
return 'email' === $this->getAlias() || $this->getColumnIsNotCreated() || $this->getColumnIsNotRemoved();
|
||||
}
|
||||
|
||||
public function getOriginalIsPublishedValue(): bool
|
||||
{
|
||||
return (bool) $this->originalIsPublishedValue;
|
||||
}
|
||||
|
||||
public function getCacheNamespacesToDelete(): array
|
||||
{
|
||||
return [self::CACHE_NAMESPACE];
|
||||
}
|
||||
|
||||
public function isIsIndex(): bool
|
||||
{
|
||||
return $this->isIndex;
|
||||
}
|
||||
|
||||
public function setIsIndex(?bool $indexable): void
|
||||
{
|
||||
$this->isIndex = $indexable ?? false;
|
||||
}
|
||||
|
||||
public function supportsLength(): bool
|
||||
{
|
||||
return in_array($this->type, self::TYPES_SUPPORTING_LENGTH);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<LeadField>
|
||||
*/
|
||||
class LeadFieldRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* @var array<int|string, array<string,mixed>>|null
|
||||
*/
|
||||
private ?array $fields = null;
|
||||
|
||||
/**
|
||||
* Retrieves array of aliases used to ensure unique alias for new fields.
|
||||
*
|
||||
* @param int $exludingId
|
||||
* @param bool $publishedOnly
|
||||
* @param bool $includeEntityFields
|
||||
* @param string $object name of object using the custom fields
|
||||
*/
|
||||
public function getAliases($exludingId, $publishedOnly = false, $includeEntityFields = true, $object = 'lead'): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('l.alias')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_fields', 'l');
|
||||
|
||||
if (!empty($exludingId)) {
|
||||
$q->where('l.id != :id')
|
||||
->setParameter('id', $exludingId);
|
||||
}
|
||||
|
||||
if ($publishedOnly) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('is_published', ':true')
|
||||
)
|
||||
->setParameter('true', true, 'boolean');
|
||||
}
|
||||
|
||||
if ($object) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('l.object', ':object')
|
||||
)->setParameter('object', $object);
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$aliases = [];
|
||||
foreach ($results as $item) {
|
||||
$aliases[] = $item['alias'];
|
||||
}
|
||||
|
||||
if ($includeEntityFields) {
|
||||
// add lead main column names to prevent attempt to create a field with the same name
|
||||
$leadRepo = $this->_em->getRepository(Lead::class)->getBaseColumns(Lead::class, true);
|
||||
$aliases = array_merge($aliases, $leadRepo);
|
||||
}
|
||||
|
||||
return $aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int|string, array<string, mixed>>
|
||||
*/
|
||||
public function getFields(): array
|
||||
{
|
||||
if (!isset($this->fields)) {
|
||||
$fq = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$fq->select('f.id, f.label, f.alias, f.type, f.field_group as "group", f.object, f.is_fixed, f.properties, f.default_value')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_fields', 'f')
|
||||
->where('f.is_published = :published')
|
||||
->setParameter('published', true, 'boolean')
|
||||
->addOrderBy('f.field_order', 'asc');
|
||||
$results = $fq->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$this->fields = array_column($results, null, 'alias');
|
||||
}
|
||||
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadField[]
|
||||
*/
|
||||
public function getFieldsForObject(string $object): array
|
||||
{
|
||||
$queryBuilder = $this->_em->createQueryBuilder();
|
||||
$queryBuilder->select($this->getTableAlias());
|
||||
$queryBuilder->from($this->getEntityName(), $this->getTableAlias(), "{$this->getTableAlias()}.id");
|
||||
$queryBuilder->where("{$this->getTableAlias()}.object = :object");
|
||||
$queryBuilder->andWhere("{$this->getTableAlias()}.isPublished = 1");
|
||||
$queryBuilder->orderBy("{$this->getTableAlias()}.label");
|
||||
$queryBuilder->setParameter('object', $object);
|
||||
|
||||
return $queryBuilder->getQuery()->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the aliases of searchable fields that are indexed and published.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchableFieldAliases(?string $object = null): array
|
||||
{
|
||||
$fq = $this->createQueryBuilder($this->getTableAlias());
|
||||
$fq->select($this->getTableAlias().'.alias')
|
||||
->andWhere($fq->expr()->eq($this->getTableAlias().'.isIndex', true))
|
||||
->andWhere($fq->expr()->eq($this->getTableAlias().'.isPublished', true));
|
||||
|
||||
if (!empty($object)) {
|
||||
$fq->andWhere($fq->expr()->eq($this->getTableAlias().'.object', ':object'))
|
||||
->setParameter('object', $object, ParameterType::STRING);
|
||||
}
|
||||
|
||||
$results = $fq->getQuery()->getResult();
|
||||
|
||||
return array_column($results, 'alias');
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'f';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
* @param object $filter
|
||||
*/
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardCatchAllWhereClause(
|
||||
$q,
|
||||
$filter,
|
||||
[
|
||||
'f.label',
|
||||
'f.alias',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[][]
|
||||
*/
|
||||
protected function getDefaultOrder(): array
|
||||
{
|
||||
return [
|
||||
['f.order', 'ASC'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field aliases for lead table columns.
|
||||
*
|
||||
* @param string $object name of object using the custom fields
|
||||
*/
|
||||
public function getFieldAliases($object = 'lead'): array
|
||||
{
|
||||
$qb = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
return $qb->select('f.alias, f.is_unique_identifer as is_unique, f.type, f.object')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_fields', 'f')
|
||||
->where($qb->expr()->eq('object', ':object'))
|
||||
->setParameter('object', $object)
|
||||
->orderBy('f.field_order', 'ASC')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection<int,LeadField>
|
||||
*/
|
||||
public function getListablePublishedFields(): ArrayCollection
|
||||
{
|
||||
$queryBuilder = $this->_em->createQueryBuilder();
|
||||
$queryBuilder->select($this->getTableAlias());
|
||||
$queryBuilder->from($this->getEntityName(), $this->getTableAlias(), "{$this->getTableAlias()}.id");
|
||||
$queryBuilder->where("{$this->getTableAlias()}.isListable = 1");
|
||||
$queryBuilder->andWhere("{$this->getTableAlias()}.isPublished = 1");
|
||||
$queryBuilder->orderBy("{$this->getTableAlias()}.object");
|
||||
|
||||
return new ArrayCollection($queryBuilder->getQuery()->execute());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add company left join.
|
||||
*
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
*/
|
||||
private function addCompanyLeftJoin($q): void
|
||||
{
|
||||
$q->leftJoin('l', MAUTIC_TABLE_PREFIX.'companies_leads', 'companies_lead', 'l.id = companies_lead.lead_id');
|
||||
$q->leftJoin('companies_lead', MAUTIC_TABLE_PREFIX.'companies', 'company', 'companies_lead.company_id = company.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return property by field alias and join tables.
|
||||
*
|
||||
* @param string $field
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
*/
|
||||
public function getPropertyByField($field, $q): string
|
||||
{
|
||||
$columnAlias = 'l.';
|
||||
// Join company tables If we're trying search by company fields
|
||||
if (in_array($field, array_column($this->getFieldAliases('company'), 'alias'))) {
|
||||
$this->addCompanyLeftJoin($q);
|
||||
$columnAlias = 'company.';
|
||||
} elseif (in_array($field, ['utm_campaign', 'utm_content', 'utm_medium', 'utm_source', 'utm_term'])) {
|
||||
$q->join('l', MAUTIC_TABLE_PREFIX.'lead_utmtags', 'u', 'l.id = u.lead_id');
|
||||
$columnAlias = 'u.';
|
||||
}
|
||||
|
||||
return $columnAlias.$field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare a form result value with defined value for defined lead.
|
||||
*
|
||||
* @param int $lead ID
|
||||
* @param string $field alias
|
||||
* @param mixed $value to compare with
|
||||
* @param string $operatorExpr for WHERE clause
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function compareValue($lead, $field, $value, $operatorExpr, ?string $fieldType = null)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('l.id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'leads', 'l');
|
||||
|
||||
if ('tags' === $field) {
|
||||
// Special reserved tags field
|
||||
$q->join('l', MAUTIC_TABLE_PREFIX.'lead_tags_xref', 'x', 'l.id = x.lead_id')
|
||||
->join('x', MAUTIC_TABLE_PREFIX.'lead_tags', 't', 'x.tag_id = t.id')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead'),
|
||||
$q->expr()->eq('t.tag', ':value')
|
||||
)
|
||||
)
|
||||
->setParameter('lead', (int) $lead)
|
||||
->setParameter('value', $value);
|
||||
|
||||
$result = $q->executeQuery()->fetchAssociative();
|
||||
|
||||
if (('eq' === $operatorExpr) || ('like' === $operatorExpr)) {
|
||||
return !empty($result['id']);
|
||||
} elseif (('neq' === $operatorExpr) || ('notLike' === $operatorExpr)) {
|
||||
return empty($result['id']);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$property = $this->getPropertyByField($field, $q);
|
||||
if ('empty' === $operatorExpr || 'notEmpty' === $operatorExpr) {
|
||||
$doesSupportEmptyValue = !in_array($fieldType, ['date', 'datetime'], true);
|
||||
$compositeExpression = ('empty' === $operatorExpr) ?
|
||||
$q->expr()->or(
|
||||
$q->expr()->isNull($property),
|
||||
$doesSupportEmptyValue ? $q->expr()->eq($property, $q->expr()->literal('')) : null
|
||||
)
|
||||
:
|
||||
$q->expr()->and(
|
||||
$q->expr()->isNotNull($property),
|
||||
$doesSupportEmptyValue ? $q->expr()->neq($property, $q->expr()->literal('')) : null
|
||||
);
|
||||
$q->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead'),
|
||||
$compositeExpression
|
||||
)
|
||||
)
|
||||
->setParameter('lead', (int) $lead);
|
||||
} elseif ('regexp' === $operatorExpr || 'notRegexp' === $operatorExpr) {
|
||||
if ('regexp' === $operatorExpr) {
|
||||
$where = $property.' REGEXP :value';
|
||||
} else {
|
||||
$where = $property.' NOT REGEXP :value';
|
||||
}
|
||||
|
||||
$q->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead'),
|
||||
$q->expr()->and($where)
|
||||
)
|
||||
)
|
||||
->setParameter('lead', (int) $lead)
|
||||
->setParameter('value', $value);
|
||||
} elseif ('in' === $operatorExpr || 'notIn' === $operatorExpr) {
|
||||
$values = (!is_array($value)) ? [$value] : $value;
|
||||
$operator = str_starts_with($operatorExpr, 'not') ? 'NOT REGEXP' : 'REGEXP';
|
||||
$expr = $q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead')
|
||||
);
|
||||
|
||||
$innerExpr = [];
|
||||
foreach ($values as $v) {
|
||||
$v = $q->expr()->literal(
|
||||
InputHelper::clean($v)
|
||||
);
|
||||
|
||||
$v = trim($v, "'");
|
||||
$innerExpr[] = $property." $operator '\\\\|?$v\\\\|?'";
|
||||
}
|
||||
|
||||
if (str_starts_with($operatorExpr, 'not')) {
|
||||
$expr = $expr->with($q->expr()->or(
|
||||
$q->expr()->isNull($property),
|
||||
$q->expr()->and(...$innerExpr)
|
||||
));
|
||||
} else {
|
||||
$expr = $expr->with($q->expr()->or(...$innerExpr));
|
||||
}
|
||||
|
||||
$q->where($expr)
|
||||
->setParameter('lead', (int) $lead)
|
||||
->setParameter('values', $values, ArrayParameterType::STRING);
|
||||
} else {
|
||||
$expr = $q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead')
|
||||
);
|
||||
|
||||
if ('neq' === $operatorExpr) {
|
||||
// include null
|
||||
$expr = $expr->with(
|
||||
$q->expr()->or(
|
||||
$q->expr()->$operatorExpr($property, ':value'),
|
||||
$q->expr()->isNull($property)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
switch ($operatorExpr) {
|
||||
case 'startsWith':
|
||||
$operatorExpr = 'like';
|
||||
$value = $value.'%';
|
||||
break;
|
||||
case 'endsWith':
|
||||
$operatorExpr = 'like';
|
||||
$value = '%'.$value;
|
||||
break;
|
||||
case 'contains':
|
||||
$operatorExpr = 'like';
|
||||
$value = '%'.$value.'%';
|
||||
break;
|
||||
}
|
||||
|
||||
$expr = $expr->with(
|
||||
$q->expr()->$operatorExpr($property, ':value')
|
||||
);
|
||||
}
|
||||
|
||||
$q->where($expr)
|
||||
->setParameter('lead', (int) $lead)
|
||||
->setParameter('value', $value);
|
||||
}
|
||||
if (str_starts_with($property, 'u.')) {
|
||||
// Match only against the latest UTM properties.
|
||||
$q->orderBy('u.date_added', 'DESC');
|
||||
$q->setMaxResults(1);
|
||||
}
|
||||
$result = $q->executeQuery()->fetchAssociative();
|
||||
|
||||
return !empty($result['id']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare a form result value with empty value for defined lead.
|
||||
*/
|
||||
public function compareEmptyDateValue(int $lead, string $field, string $operatorExpr): bool
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$property = $this->getPropertyByField($field, $q);
|
||||
$q->select('l.id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead'),
|
||||
('empty' === $operatorExpr) ?
|
||||
$q->expr()->isNull($property)
|
||||
:
|
||||
$q->expr()->isNotNull($property)
|
||||
)
|
||||
)
|
||||
->setParameter('lead', $lead, \PDO::PARAM_INT);
|
||||
$result = $q->executeQuery()->fetchAssociative();
|
||||
|
||||
return !empty($result['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare a form result value with defined date value for defined lead.
|
||||
*
|
||||
* @param int $lead ID
|
||||
* @param int $field alias
|
||||
* @param string $value to compare with
|
||||
*/
|
||||
public function compareDateValue($lead, $field, $value): bool
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$property = $this->getPropertyByField($field, $q);
|
||||
$q->select('l.id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead'),
|
||||
$q->expr()->eq($property, ':value')
|
||||
)
|
||||
)
|
||||
->setParameter('lead', (int) $lead)
|
||||
->setParameter('value', $value);
|
||||
|
||||
$result = $q->executeQuery()->fetchAssociative();
|
||||
|
||||
return !empty($result['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare a form result value with defined date value ( only day and month compare for
|
||||
* events such as anniversary) for defined lead.
|
||||
*
|
||||
* @param int $lead ID
|
||||
* @param int $field alias
|
||||
* @param object $value Date object to compare with
|
||||
*/
|
||||
public function compareDateMonthValue($lead, $field, $value): bool
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('l.id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead'),
|
||||
$q->expr()->eq("MONTH(l. $field)", ':month'),
|
||||
$q->expr()->eq("DAY(l. $field)", ':day')
|
||||
)
|
||||
)
|
||||
->setParameter('lead', (int) $lead)
|
||||
->setParameter('month', $value->format('m'))
|
||||
->setParameter('day', $value->format('d'));
|
||||
|
||||
$result = $q->executeQuery()->fetchAssociative();
|
||||
|
||||
return !empty($result['id']);
|
||||
}
|
||||
|
||||
public function getFieldThatIsMissingColumn(): ?LeadField
|
||||
{
|
||||
$qb = $this->createQueryBuilder($this->getTableAlias());
|
||||
$qb->where($qb->expr()->eq("{$this->getTableAlias()}.columnIsNotCreated", 1));
|
||||
$qb->orderBy("{$this->getTableAlias()}.dateAdded", Order::Ascending->value);
|
||||
$qb->setMaxResults(1);
|
||||
|
||||
return $qb->getQuery()->getOneOrNullResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadField[]
|
||||
*/
|
||||
public function getFieldsByType($type)
|
||||
{
|
||||
return $this->findBy(['type' => $type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
$commands = [
|
||||
'mautic.core.searchcommand.ispublished',
|
||||
'mautic.core.searchcommand.isunpublished',
|
||||
'mautic.core.searchcommand.ismine',
|
||||
'mautic.lead.field.searchcommand.isindexed',
|
||||
'mautic.lead.field.searchcommand.isunique',
|
||||
'mautic.lead.field.searchcommand.type',
|
||||
'mautic.lead.field.searchcommand.group',
|
||||
];
|
||||
|
||||
return array_merge($commands, parent::getSearchCommands());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getFieldSchemaData(string $object): array
|
||||
{
|
||||
return $this->_em->createQueryBuilder()
|
||||
->select('f.alias, f.label, f.type, f.isUniqueIdentifer, f.charLengthLimit')
|
||||
->from($this->getEntityName(), 'f', 'f.alias')
|
||||
->where('f.object = :object')
|
||||
->setParameter('object', $object)
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
* @param \StdClass $filter
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
list($expr, $parameters) = $this->addStandardSearchCommandWhereClause($q, $filter);
|
||||
if ($expr) {
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
$command = $filter->command;
|
||||
$unique = $this->generateRandomParameterName();
|
||||
$returnParameter = false; // returning a parameter that is not used will lead to a Doctrine error
|
||||
$prefix = $this->getTableAlias();
|
||||
|
||||
switch ($command) {
|
||||
case $this->translator->trans('mautic.lead.field.searchcommand.isindexed'):
|
||||
$expr = $q->expr()->eq($prefix.'.isIndex', ":$unique");
|
||||
$forceParameters = [$unique => true];
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.lead.field.searchcommand.isunique'):
|
||||
$expr = $q->expr()->eq($prefix.'.isUniqueIdentifer', ":$unique");
|
||||
$forceParameters = [$unique => true];
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.lead.field.searchcommand.type'):
|
||||
$forceParameters = [
|
||||
$unique => $filter->string,
|
||||
];
|
||||
$expr = $q->expr()->like($prefix.'.type', ":$unique");
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.lead.field.searchcommand.group'):
|
||||
$forceParameters = [
|
||||
$unique => $filter->string,
|
||||
];
|
||||
$expr = $q->expr()->like($prefix.'.group', ":$unique");
|
||||
$returnParameter = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($expr && $filter->not) {
|
||||
$expr = $q->expr()->not($expr);
|
||||
}
|
||||
|
||||
if (!empty($forceParameters)) {
|
||||
$parameters = $forceParameters;
|
||||
} elseif ($returnParameter) {
|
||||
$string = ($filter->strict) ? $filter->string : "%{$filter->string}%";
|
||||
$parameters = ["$unique" => $string];
|
||||
}
|
||||
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\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\DateTimeHelper;
|
||||
use Mautic\LeadBundle\Form\Validator\Constraints\SegmentInUse;
|
||||
use Mautic\LeadBundle\Form\Validator\Constraints\UniqueUserAlias;
|
||||
use Mautic\LeadBundle\Validator\Constraints\SegmentUsedInCampaigns;
|
||||
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(
|
||||
shortName: 'Segments',
|
||||
operations: [
|
||||
new GetCollection(uriTemplate: '/segments', security: "is_granted('lead:lists:viewown')"),
|
||||
new Post(uriTemplate: '/segments', security: "is_granted('lead:lists:create')"),
|
||||
new Get(uriTemplate: '/segments/{id}', security: "is_granted('lead:lists:viewown')"),
|
||||
new Put(uriTemplate: '/segments/{id}', security: "is_granted('lead:lists:editown')"),
|
||||
new Patch(uriTemplate: '/segments/{id}', security: "is_granted('lead:lists:editother')"),
|
||||
new Delete(uriTemplate: '/segments/{id}', security: "is_granted('lead:lists:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['segment:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
'api_included' => ['category'],
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['segment:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class LeadList extends FormEntity implements UuidInterface
|
||||
{
|
||||
use UuidTrait;
|
||||
|
||||
use ProjectTrait;
|
||||
|
||||
public const TABLE_NAME = 'lead_lists';
|
||||
public const ENTITY_NAME = 'lists';
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
#[Groups(['segment:read', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $publicName;
|
||||
|
||||
/**
|
||||
* @var Category|null
|
||||
**/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $category;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $alias;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $filters = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $isGlobal = true;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $isPreferenceCenter = false;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<ListLead>
|
||||
*/
|
||||
private $leads;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface|null
|
||||
*/
|
||||
#[Groups(['segment:read', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $lastBuiltDate;
|
||||
|
||||
/**
|
||||
* @var float|null
|
||||
*/
|
||||
#[Groups(['segment:read', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $lastBuiltTime;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->leads = new ArrayCollection();
|
||||
$this->initializeProjects();
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(LeadListRepository::class)
|
||||
->addIndex(['alias'], 'lead_list_alias');
|
||||
|
||||
$builder->addIdColumns();
|
||||
|
||||
$builder->addField('alias', 'string');
|
||||
|
||||
$builder->createField('publicName', 'string')
|
||||
->columnName('public_name')
|
||||
->build();
|
||||
|
||||
$builder->addCategory();
|
||||
|
||||
$builder->addField('filters', 'array');
|
||||
|
||||
$builder->createField('isGlobal', 'boolean')
|
||||
->columnName('is_global')
|
||||
->build();
|
||||
|
||||
$builder->createField('isPreferenceCenter', 'boolean')
|
||||
->columnName('is_preference_center')
|
||||
->build();
|
||||
|
||||
$builder->createOneToMany('leads', 'ListLead')
|
||||
->mappedBy('list')
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
$builder->createField('lastBuiltDate', 'datetime')
|
||||
->columnName('last_built_date')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('lastBuiltTime', 'float')
|
||||
->columnName('last_built_time')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
self::addProjectsField($builder, 'lead_list_projects_xref', 'leadlist_id');
|
||||
static::addUuidField($builder);
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('name', new Assert\NotBlank(
|
||||
['message' => 'mautic.core.name.required']
|
||||
));
|
||||
|
||||
$metadata->addConstraint(new UniqueUserAlias([
|
||||
'field' => 'alias',
|
||||
'message' => 'mautic.lead.list.alias.unique',
|
||||
]));
|
||||
|
||||
$metadata->addConstraint(new SegmentUsedInCampaigns());
|
||||
$metadata->addConstraint(new SegmentInUse());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('leadList')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'name',
|
||||
'publicName',
|
||||
'alias',
|
||||
'description',
|
||||
'category',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'filters',
|
||||
'isGlobal',
|
||||
'isPreferenceCenter',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
|
||||
self::addProjectsInLoadApiMetadata($metadata, 'leadList');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
*
|
||||
* @return LeadList
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->isChanged('name', $name);
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $description
|
||||
*
|
||||
* @return LeadList
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->isChanged('description', $description);
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setCategory(?Category $category = null): LeadList
|
||||
{
|
||||
$this->isChanged('category', $category);
|
||||
$this->category = $category;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCategory(): ?Category
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get publicName.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPublicName()
|
||||
{
|
||||
return $this->publicName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $publicName
|
||||
*
|
||||
* @return LeadList
|
||||
*/
|
||||
public function setPublicName($publicName)
|
||||
{
|
||||
$this->isChanged('publicName', $publicName);
|
||||
$this->publicName = $publicName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadList
|
||||
*/
|
||||
public function setFilters(array $filters)
|
||||
{
|
||||
$this->isChanged('filters', $filters);
|
||||
$this->filters = $filters;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getFilters()
|
||||
{
|
||||
if (is_array($this->filters)) {
|
||||
return $this->setFirstFilterGlueToAnd($this->addLegacyParams($this->filters)); // @phpstan-ignore method.deprecated
|
||||
}
|
||||
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
public function needsRebuild(): bool
|
||||
{
|
||||
// Manual or unpublished segments never require rebuild
|
||||
if (empty($this->getFilters()) || !$this->isPublished()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// A segment with filters requires rebuild if it was changed since the last build date, or was never built
|
||||
if (null === $this->getLastBuiltDate()) {
|
||||
return true;
|
||||
}
|
||||
if (null !== $this->getDateModified() && $this->getDateModified()->getTimestamp() >= $this->getLastBuiltDate()->getTimestamp()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function hasFilterTypeOf(string $type): bool
|
||||
{
|
||||
foreach ($this->getFilters() as $filter) {
|
||||
if ($filter['type'] === $type) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isGlobal
|
||||
*
|
||||
* @return LeadList
|
||||
*/
|
||||
public function setIsGlobal($isGlobal)
|
||||
{
|
||||
$this->isChanged('isGlobal', (bool) $isGlobal);
|
||||
$this->isGlobal = (bool) $isGlobal;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsGlobal()
|
||||
{
|
||||
return $this->isGlobal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy function to getIsGlobal().
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isGlobal()
|
||||
{
|
||||
return $this->getIsGlobal();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $alias
|
||||
*
|
||||
* @return LeadList
|
||||
*/
|
||||
public function setAlias($alias)
|
||||
{
|
||||
$this->isChanged('alias', $alias);
|
||||
$this->alias = $alias;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAlias()
|
||||
{
|
||||
return $this->alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Doctrine\Common\Collections\Collection
|
||||
*/
|
||||
public function getLeads()
|
||||
{
|
||||
return $this->leads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone entity with empty contact list.
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
parent::__clone();
|
||||
|
||||
$this->id = null;
|
||||
$this->leads = new ArrayCollection();
|
||||
$this->setIsPublished(false);
|
||||
$this->setAlias('');
|
||||
$this->lastBuiltDate = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsPreferenceCenter()
|
||||
{
|
||||
return $this->isPreferenceCenter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isPreferenceCenter
|
||||
*/
|
||||
public function setIsPreferenceCenter($isPreferenceCenter): void
|
||||
{
|
||||
$this->isChanged('isPreferenceCenter', (bool) $isPreferenceCenter);
|
||||
$this->isPreferenceCenter = (bool) $isPreferenceCenter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated remove after several of years.
|
||||
*
|
||||
* This is needed go keep BC after we moved 'filter' and 'display' params
|
||||
* to the 'properties' array.
|
||||
*/
|
||||
private function addLegacyParams(array $filters): array
|
||||
{
|
||||
return array_map(
|
||||
function (array $filter): array {
|
||||
if (isset($filter['properties']) && $filter['properties'] && array_key_exists('filter', $filter['properties'])) {
|
||||
$filter['filter'] = $filter['properties']['filter'];
|
||||
} else {
|
||||
$filter['filter'] = $filter['filter'] ?? null;
|
||||
}
|
||||
|
||||
if (isset($filter['properties']) && $filter['properties'] && array_key_exists('display', $filter['properties'])) {
|
||||
$filter['display'] = $filter['properties']['display'];
|
||||
} else {
|
||||
$filter['display'] = $filter['display'] ?? null;
|
||||
}
|
||||
|
||||
return $filter;
|
||||
},
|
||||
$filters
|
||||
);
|
||||
}
|
||||
|
||||
public function getLastBuiltDate(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->lastBuiltDate;
|
||||
}
|
||||
|
||||
public function setLastBuiltDate(?\DateTime $lastBuiltDate): void
|
||||
{
|
||||
$this->lastBuiltDate = $lastBuiltDate;
|
||||
}
|
||||
|
||||
public function setLastBuiltDateToCurrentDatetime(): void
|
||||
{
|
||||
$now = (new DateTimeHelper())->getUtcDateTime();
|
||||
$this->setLastBuiltDate($now);
|
||||
}
|
||||
|
||||
public function getLastBuiltTime(): ?float
|
||||
{
|
||||
return $this->lastBuiltTime;
|
||||
}
|
||||
|
||||
public function setLastBuiltTime(?float $lastBuiltTime): void
|
||||
{
|
||||
$this->lastBuiltTime = $lastBuiltTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $filters
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function setFirstFilterGlueToAnd(array $filters): array
|
||||
{
|
||||
foreach ($filters as &$filter) {
|
||||
$filter['glue'] = 'and';
|
||||
break;
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,867 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<LeadList>
|
||||
*/
|
||||
class LeadListRepository extends CommonRepository
|
||||
{
|
||||
use OperatorListTrait; // @deprecated to be removed in Mautic 3. Not used inside this class.
|
||||
|
||||
use ExpressionHelperTrait;
|
||||
use RegexTrait;
|
||||
use ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $listFiltersInnerJoinCompany = false;
|
||||
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
|
||||
/**
|
||||
* Flag to check if some segment filter on a company field exists.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasCompanyFilter = false;
|
||||
|
||||
/**
|
||||
* @var \Doctrine\DBAL\Schema\Column[]
|
||||
*/
|
||||
protected $leadTableSchema;
|
||||
|
||||
/**
|
||||
* @var \Doctrine\DBAL\Schema\Column[]
|
||||
*/
|
||||
protected $companyTableSchema;
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
*/
|
||||
public function getEntity($id = 0): ?LeadList
|
||||
{
|
||||
try {
|
||||
return $this
|
||||
->createQueryBuilder('l')
|
||||
->where('l.id = :listId')
|
||||
->setParameter('listId', $id)
|
||||
->getQuery()
|
||||
->getSingleResult();
|
||||
} catch (\Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of lists.
|
||||
*
|
||||
* @param string $alias
|
||||
* @param string $id
|
||||
* @param bool $justPublished if false, returns all published and unpublished segments
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLists(?User $user = null, $alias = '', $id = '', bool $justPublished = true)
|
||||
{
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->from(LeadList::class, 'l', 'l.id');
|
||||
|
||||
$q->select('partial l.{id, name, alias}');
|
||||
|
||||
if ($justPublished) {
|
||||
$q->andWhere($q->expr()->eq('l.isPublished', ':true'))
|
||||
->setParameter('true', true, 'boolean');
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
$q->andWhere($q->expr()->eq('l.isGlobal', ':true'));
|
||||
$q->orWhere('l.createdBy = :user');
|
||||
$q->setParameter('user', $user->getId());
|
||||
}
|
||||
|
||||
if (!empty($alias)) {
|
||||
$q->andWhere('l.alias = :alias');
|
||||
$q->setParameter('alias', $alias);
|
||||
}
|
||||
|
||||
if (!empty($id)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->neq('l.id', $id)
|
||||
);
|
||||
}
|
||||
|
||||
$q->orderBy('l.name');
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lists for a specific lead.
|
||||
*
|
||||
* @param int|Lead[] $lead Lead ID or array of Leads
|
||||
* @param bool $forList
|
||||
* @param bool $singleArrayHydration
|
||||
* @param bool $isPublic
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLeadLists($lead, $forList = false, $singleArrayHydration = false, $isPublic = false, $isPreferenceCenter = false)
|
||||
{
|
||||
if (is_array($lead)) {
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->from(LeadList::class, 'l', 'l.id');
|
||||
|
||||
if ($forList) {
|
||||
$q->select('partial l.{id, alias, name}, partial il.{lead, list, dateAdded, manuallyAdded, manuallyRemoved}');
|
||||
} else {
|
||||
$q->select('l, partial lead.{id}');
|
||||
}
|
||||
|
||||
$q->leftJoin('l.leads', 'il')
|
||||
->leftJoin('il.lead', 'lead');
|
||||
|
||||
$q->where(
|
||||
$q->expr()->andX(
|
||||
$q->expr()->in('lead.id', ':leads'),
|
||||
$q->expr()->in('il.manuallyRemoved', ':false')
|
||||
)
|
||||
)
|
||||
->setParameter('leads', $lead)
|
||||
->setParameter('false', false, 'boolean');
|
||||
|
||||
if ($isPublic) {
|
||||
$q->andWhere($q->expr()->eq('l.isGlobal', ':isPublic'))
|
||||
->setParameter('isPublic', true, 'boolean');
|
||||
}
|
||||
if ($isPreferenceCenter) {
|
||||
$q->andWhere($q->expr()->eq('l.isPreferenceCenter', ':isPreferenceCenter'))
|
||||
->setParameter('isPreferenceCenter', true, 'boolean');
|
||||
}
|
||||
$result = $q->getQuery()->getArrayResult();
|
||||
$return = [];
|
||||
foreach ($result as $r) {
|
||||
foreach ($r['leads'] as $l) {
|
||||
$return[$l['lead_id']][$r['id']] = $r;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
} else {
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->from(LeadList::class, 'l', 'l.id');
|
||||
|
||||
if ($forList) {
|
||||
$q->select('partial l.{id, alias, name}, partial il.{lead, list, dateAdded, manuallyAdded, manuallyRemoved}');
|
||||
} else {
|
||||
$q->select('l');
|
||||
}
|
||||
|
||||
$q->leftJoin('l.leads', 'il');
|
||||
|
||||
$q->where(
|
||||
$q->expr()->andX(
|
||||
$q->expr()->eq('IDENTITY(il.lead)', (int) $lead),
|
||||
$q->expr()->in('il.manuallyRemoved', ':false')
|
||||
)
|
||||
)
|
||||
->setParameter('false', false, 'boolean');
|
||||
|
||||
if ($isPublic) {
|
||||
$q->andWhere($q->expr()->eq('l.isGlobal', ':isPublic'))
|
||||
->setParameter('isPublic', true, 'boolean');
|
||||
}
|
||||
|
||||
if ($isPreferenceCenter) {
|
||||
$q->andWhere($q->expr()->eq('l.isPreferenceCenter', ':isPreferenceCenter'))
|
||||
->setParameter('isPreferenceCenter', true, 'boolean');
|
||||
}
|
||||
|
||||
return ($singleArrayHydration) ? $q->getQuery()->getArrayResult() : $q->getQuery()->getResult();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Lead segments by ids.
|
||||
*/
|
||||
public function checkLeadSegmentsByIds(Lead $lead, $ids): bool
|
||||
{
|
||||
if (empty($ids)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$qb->select('ll.leadlist_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'll')
|
||||
->where(
|
||||
$qb->expr()->and(
|
||||
$qb->expr()->in('ll.leadlist_id', ':ids'),
|
||||
$qb->expr()->eq('ll.lead_id', ':leadId'),
|
||||
$qb->expr()->eq('ll.manually_removed', 0)
|
||||
)
|
||||
)
|
||||
->setParameter('leadId', $lead->getId())
|
||||
->setParameter('ids', $ids, ArrayParameterType::INTEGER);
|
||||
|
||||
return (bool) $qb->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of global lists.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getGlobalLists()
|
||||
{
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->from(LeadList::class, 'l', 'l.id');
|
||||
|
||||
$q->select('partial l.{id, name, alias}')
|
||||
->where($q->expr()->eq('l.isPublished', 'true'))
|
||||
->setParameter('true', true, 'boolean')
|
||||
->andWhere($q->expr()->eq('l.isGlobal', ':true'))
|
||||
->orderBy('l.name');
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of global lists.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPreferenceCenterList()
|
||||
{
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->from(LeadList::class, 'l', 'l.id');
|
||||
|
||||
$q->select('partial l.{id, name, publicName, alias}')
|
||||
->where($q->expr()->eq('l.isPublished', 'true'))
|
||||
->setParameter('true', true, 'boolean')
|
||||
->andWhere($q->expr()->eq('l.isPreferenceCenter', ':true'))
|
||||
->orderBy('l.name');
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a count of leads that belong to the list.
|
||||
*
|
||||
* @param int|int[] $listIds
|
||||
*
|
||||
* @return array|int
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getLeadCount($listIds)
|
||||
{
|
||||
if (!is_array($listIds)) {
|
||||
$listIds = [$listIds];
|
||||
}
|
||||
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q->select('count(l.lead_id) as thecount, l.leadlist_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'l');
|
||||
|
||||
$countListIds = count($listIds);
|
||||
|
||||
if (1 === $countListIds) {
|
||||
$q = $this->forceUseIndex($q, MAUTIC_TABLE_PREFIX.'manually_removed');
|
||||
$expression = $q->expr()->eq('l.leadlist_id', $listIds[0]);
|
||||
} else {
|
||||
$expression = $q->expr()->in('l.leadlist_id', $listIds);
|
||||
}
|
||||
|
||||
$q->where(
|
||||
$expression,
|
||||
$q->expr()->eq('l.manually_removed', ':false')
|
||||
)
|
||||
->setParameter('false', false, 'boolean')
|
||||
->groupBy('l.leadlist_id');
|
||||
|
||||
$result = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$return = [];
|
||||
foreach ($result as $r) {
|
||||
$return[$r['leadlist_id']] = (int) $r['thecount'];
|
||||
}
|
||||
|
||||
// Ensure lists without leads have a value
|
||||
foreach ($listIds as $l) {
|
||||
if (!isset($return[$l])) {
|
||||
$return[$l] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return (1 === $countListIds) ? $return[$listIds[0]] : $return;
|
||||
}
|
||||
|
||||
private function forceUseIndex(QueryBuilder $qb, string $indexName): QueryBuilder
|
||||
{
|
||||
$fromPart = $qb->getQueryPart('from');
|
||||
$fromPart[0]['alias'] = sprintf('%s USE INDEX (%s)', $fromPart[0]['alias'], $indexName);
|
||||
$qb->resetQueryPart('from');
|
||||
$qb->from($fromPart[0]['table'], $fromPart[0]['alias']);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function arrangeFilters($filters): array
|
||||
{
|
||||
$objectFilters = [];
|
||||
if (empty($filters)) {
|
||||
$objectFilters['lead'][] = $filters;
|
||||
}
|
||||
foreach ($filters as $filter) {
|
||||
$object = $filter['object'] ?? 'lead';
|
||||
switch ($object) {
|
||||
case 'company':
|
||||
$objectFilters['company'][] = $filter;
|
||||
break;
|
||||
default:
|
||||
$objectFilters['lead'][] = $filter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $objectFilters;
|
||||
}
|
||||
|
||||
public function setDispatcher(EventDispatcherInterface $dispatcher): void
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
protected function createFilterExpressionSubQuery($table, $alias, $column, $value, array &$parameters, $leadId = null, array $subQueryFilters = [])
|
||||
{
|
||||
$subQb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$subExpr = [];
|
||||
|
||||
foreach ($subQueryFilters as $subColumn => $subParameter) {
|
||||
$subExpr[] = $subQb->expr()->eq($subColumn, ":$subParameter");
|
||||
}
|
||||
|
||||
if ('leads' !== $table) {
|
||||
$subExpr[] = $subQb->expr()->eq($alias.'.lead_id', 'l.id');
|
||||
}
|
||||
|
||||
// Specific lead
|
||||
if (!empty($leadId)) {
|
||||
$columnName = ('leads' === $table) ? 'id' : 'lead_id';
|
||||
$subExpr[] = $subQb->expr()->eq($alias.'.'.$columnName, $leadId);
|
||||
}
|
||||
|
||||
if (null !== $value && !empty($column)) {
|
||||
$subFilterParamter = $this->generateRandomParameterName();
|
||||
$subFunc = 'eq';
|
||||
if (is_array($value)) {
|
||||
$subFunc = 'in';
|
||||
$subExpr[] = $subQb->expr()->in(sprintf('%s.%s', $alias, $column), ":$subFilterParamter");
|
||||
$parameters[$subFilterParamter] = ['value' => $value, 'type' => ArrayParameterType::STRING];
|
||||
} else {
|
||||
$parameters[$subFilterParamter] = $value;
|
||||
}
|
||||
|
||||
$subExpr = $subQb->expr()->$subFunc(sprintf('%s.%s', $alias, $column), ":$subFilterParamter");
|
||||
}
|
||||
|
||||
$subQb->expr()->and(...$subExpr);
|
||||
|
||||
$subQb->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.$table, $alias)
|
||||
->where($subExpr);
|
||||
|
||||
return $subQb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|QueryBuilder $q
|
||||
*/
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardCatchAllWhereClause(
|
||||
$q,
|
||||
$filter,
|
||||
[
|
||||
'l.name',
|
||||
'l.alias',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|QueryBuilder $q
|
||||
*/
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
[$expr, $parameters] = parent::addStandardSearchCommandWhereClause($q, $filter);
|
||||
if ($expr) {
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
$command = $filter->command;
|
||||
$unique = $this->generateRandomParameterName();
|
||||
$returnParameter = false; // returning a parameter that is not used will lead to a Doctrine error
|
||||
|
||||
switch ($command) {
|
||||
case $this->translator->trans('mautic.lead.list.searchcommand.isglobal'):
|
||||
case $this->translator->trans('mautic.lead.list.searchcommand.isglobal', [], null, 'en_US'):
|
||||
$expr = $q->expr()->eq('l.isGlobal', ":$unique");
|
||||
$forceParameters = [$unique => true];
|
||||
break;
|
||||
case $this->translator->trans('mautic.core.searchcommand.name'):
|
||||
case $this->translator->trans('mautic.core.searchcommand.name', [], null, 'en_US'):
|
||||
$expr = $q->expr()->like('l.name', ':'.$unique);
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.project.searchcommand.name'):
|
||||
case $this->translator->trans('mautic.project.searchcommand.name', [], null, 'en_US'):
|
||||
return $this->handleProjectFilter(
|
||||
$this->_em->getConnection()->createQueryBuilder(),
|
||||
'leadlist_id',
|
||||
'lead_list_projects_xref',
|
||||
'l',
|
||||
$filter->string,
|
||||
$filter->not
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($forceParameters)) {
|
||||
$parameters = $forceParameters;
|
||||
} elseif ($returnParameter) {
|
||||
$string = ($filter->strict) ? $filter->string : "%{$filter->string}%";
|
||||
$parameters = ["$unique" => $string];
|
||||
}
|
||||
|
||||
return [
|
||||
$expr,
|
||||
$parameters,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
$commands = [
|
||||
'mautic.lead.list.searchcommand.isglobal',
|
||||
'mautic.core.searchcommand.ispublished',
|
||||
'mautic.core.searchcommand.isunpublished',
|
||||
'mautic.core.searchcommand.name',
|
||||
'mautic.core.searchcommand.ismine',
|
||||
'mautic.core.searchcommand.category',
|
||||
'mautic.project.searchcommand.name',
|
||||
];
|
||||
|
||||
return array_merge($commands, parent::getSearchCommands());
|
||||
}
|
||||
|
||||
public function getRelativeDateStrings(): array
|
||||
{
|
||||
$keys = self::getRelativeDateTranslationKeys();
|
||||
|
||||
$strings = [];
|
||||
foreach ($keys as $key) {
|
||||
$strings[$key] = $this->translator->trans($key);
|
||||
}
|
||||
|
||||
return $strings;
|
||||
}
|
||||
|
||||
public static function getRelativeDateTranslationKeys(): array
|
||||
{
|
||||
return [
|
||||
'mautic.lead.list.month_last',
|
||||
'mautic.lead.list.month_next',
|
||||
'mautic.lead.list.month_this',
|
||||
'mautic.lead.list.today',
|
||||
'mautic.lead.list.tomorrow',
|
||||
'mautic.lead.list.yesterday',
|
||||
'mautic.lead.list.week_last',
|
||||
'mautic.lead.list.week_next',
|
||||
'mautic.lead.list.week_this',
|
||||
'mautic.lead.list.year_last',
|
||||
'mautic.lead.list.year_next',
|
||||
'mautic.lead.list.year_this',
|
||||
'mautic.lead.list.anniversary',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array<string>>
|
||||
*/
|
||||
protected function getDefaultOrder(): array
|
||||
{
|
||||
return [
|
||||
['l.name', 'ASC'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'l';
|
||||
}
|
||||
|
||||
public function leadListExists(int $id): bool
|
||||
{
|
||||
$tableName = MAUTIC_TABLE_PREFIX.'lead_lists';
|
||||
$result = (int) $this->getEntityManager()->getConnection()
|
||||
->executeQuery("SELECT EXISTS(SELECT 1 FROM {$tableName} WHERE id = {$id})")
|
||||
->fetchOne();
|
||||
|
||||
return 1 === $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array of campaigns related to this segment.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getSegmentCampaigns(int $segmentId): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('clx.campaign_id, c.name')
|
||||
->distinct()
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leadlist_xref', 'clx')
|
||||
->join('clx', MAUTIC_TABLE_PREFIX.'campaigns', 'c', 'c.id = clx.campaign_id');
|
||||
$q->where(
|
||||
$q->expr()->eq('clx.leadlist_id', $segmentId)
|
||||
);
|
||||
|
||||
$lists = [];
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
foreach ($results as $row) {
|
||||
$lists[$row['campaign_id']] = $row['name'];
|
||||
}
|
||||
|
||||
return $lists;
|
||||
}
|
||||
|
||||
public function isContactInAnySegment(int $contactId): bool
|
||||
{
|
||||
$tableName = MAUTIC_TABLE_PREFIX.'lead_lists_leads';
|
||||
|
||||
$sql = <<<SQL
|
||||
SELECT leadlist_id
|
||||
FROM $tableName
|
||||
WHERE lead_id = ?
|
||||
AND manually_removed = 0
|
||||
LIMIT 1
|
||||
SQL;
|
||||
|
||||
$segmentIds = $this->getEntityManager()->getConnection()
|
||||
->executeQuery(
|
||||
$sql,
|
||||
[$contactId],
|
||||
[\PDO::PARAM_INT]
|
||||
)
|
||||
->fetchFirstColumn();
|
||||
|
||||
return !empty($segmentIds);
|
||||
}
|
||||
|
||||
public function isNotContactInAnySegment(int $contactId): bool
|
||||
{
|
||||
return !$this->isContactInAnySegment($contactId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $expectedSegmentIds
|
||||
*/
|
||||
public function isContactInSegments(int $contactId, array $expectedSegmentIds): bool
|
||||
{
|
||||
$segmentIds = $this->fetchContactToSegmentIdsRelationships($contactId, $expectedSegmentIds);
|
||||
|
||||
return !empty($segmentIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $expectedSegmentIds
|
||||
*/
|
||||
public function isNotContactInSegments(int $contactId, array $expectedSegmentIds): bool
|
||||
{
|
||||
$segmentIds = $this->fetchContactToSegmentIdsRelationships($contactId, $expectedSegmentIds);
|
||||
|
||||
if (empty($segmentIds)) {
|
||||
return true; // Contact is not associated wit any segment
|
||||
}
|
||||
|
||||
foreach ($expectedSegmentIds as $expectedSegmentId) {
|
||||
if (in_array($expectedSegmentId, $segmentIds)) { // No exact type comparison used!
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $expectedSegmentIds
|
||||
*/
|
||||
public function isContactInAllSegments(int $contactId, array $expectedSegmentIds): bool
|
||||
{
|
||||
$segmentIds = $this->fetchContactToSegmentIdsRelationships($contactId, $expectedSegmentIds);
|
||||
|
||||
return count($segmentIds) === count($expectedSegmentIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $expectedSegmentIds
|
||||
*/
|
||||
public function isNotContactInAllSegments(int $contactId, array $expectedSegmentIds): bool
|
||||
{
|
||||
$segmentIds = $this->fetchContactToSegmentIdsRelationships($contactId, $expectedSegmentIds);
|
||||
|
||||
return [] === $segmentIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $expectedSegmentIds
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
private function fetchContactToSegmentIdsRelationships(int $contactId, array $expectedSegmentIds): array
|
||||
{
|
||||
$tableName = MAUTIC_TABLE_PREFIX.'lead_lists_leads';
|
||||
|
||||
$sql = <<<SQL
|
||||
SELECT leadlist_id
|
||||
FROM $tableName
|
||||
WHERE lead_id = ?
|
||||
AND leadlist_id IN (?)
|
||||
AND manually_removed = 0
|
||||
SQL;
|
||||
|
||||
return $this->getEntityManager()->getConnection()
|
||||
->executeQuery(
|
||||
$sql,
|
||||
[$contactId, $expectedSegmentIds],
|
||||
[
|
||||
\PDO::PARAM_INT,
|
||||
ArrayParameterType::INTEGER,
|
||||
]
|
||||
)
|
||||
->fetchFirstColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getAllSegments(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('title', 'title');
|
||||
$rsm->addScalarResult('item_id', 'item_id');
|
||||
$rsm->addScalarResult('is_published', 'is_published');
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
ll.name as title,
|
||||
ll.id as item_id,
|
||||
ll.is_published as is_published
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'lead_lists ll', $rsm);
|
||||
|
||||
return $query->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getCampaignEntryPoints(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('item_id', 'item_id');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
leadlist_id as item_id
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'campaign_leadlist_xref
|
||||
GROUP BY leadlist_id', $rsm);
|
||||
|
||||
return $query->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getEmailIncludeExcludeList(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('item_id', 'item_id');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
leadlist_id as item_id
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'email_list_xref
|
||||
GROUP BY leadlist_id', $rsm);
|
||||
|
||||
$included = $query->getResult();
|
||||
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('item_id', 'item_id');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
leadlist_id as item_id
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'email_list_excluded
|
||||
GROUP BY leadlist_id', $rsm);
|
||||
|
||||
$excluded = $query->getResult();
|
||||
|
||||
return array_merge($included, $excluded);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getCampaignChangeSegmentAction(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('properties', 'properties');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
properties
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'campaign_events ce
|
||||
WHERE ce.type = \'lead.changelist\'', $rsm);
|
||||
|
||||
$segmentIds = [];
|
||||
foreach ($query->getResult() as $property) {
|
||||
$property = unserialize($property['properties']);
|
||||
$segmentIds = array_merge($property['addToLists'], $property['removeFromLists'], $segmentIds);
|
||||
}
|
||||
|
||||
return array_map(fn ($segment) => ['item_id' => (string) $segment], $segmentIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getFilterSegmentsAction(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('filters', 'filters');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
filters
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'lead_lists', $rsm);
|
||||
|
||||
$childSegmentIds = [];
|
||||
|
||||
foreach ($query->getResult() as $rowFilters) {
|
||||
$segmentMembershipFilters = array_filter(
|
||||
unserialize($rowFilters['filters']),
|
||||
fn (array $filter) => 'leadlist' === $filter['type']
|
||||
);
|
||||
|
||||
foreach ($segmentMembershipFilters as $filter) {
|
||||
if (is_array($filter['properties']['filter'])) {
|
||||
foreach ($filter['properties']['filter'] as $childSegmentId) {
|
||||
$childSegmentIds[] = ['item_id' => (string) $childSegmentId];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $childSegmentIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getLeadListLeads(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('item_id', 'item_id');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
leadlist_id as item_id
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'lead_lists_leads
|
||||
GROUP BY leadlist_id', $rsm);
|
||||
|
||||
return $query->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getNotificationIncludedList(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('item_id', 'item_id');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
leadlist_id as item_id
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'push_notification_list_xref
|
||||
GROUP BY leadlist_id', $rsm);
|
||||
|
||||
return $query->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getSMSIncludedList(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('item_id', 'item_id');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
leadlist_id as item_id
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'sms_message_list_xref
|
||||
GROUP BY leadlist_id', $rsm);
|
||||
|
||||
return $query->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getFormAction(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('properties', 'properties');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
properties
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'form_actions fa
|
||||
WHERE fa.type = \'lead.changelist\'', $rsm);
|
||||
|
||||
$segmentIds = [];
|
||||
foreach ($query->getResult() as $property) {
|
||||
$property = unserialize($property['properties']);
|
||||
$segmentIds = array_merge($property['addToLists'], $property['removeFromLists'], $segmentIds);
|
||||
}
|
||||
|
||||
return array_map(fn ($segment) => ['item_id' => (string) $segment], $segmentIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function getLeadSegmentIds(int $leadId): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('ll.id')
|
||||
->from(LeadList::class, 'll')
|
||||
->innerJoin('ll.leads', 'l')
|
||||
->where(
|
||||
$qb->expr()->eq('l.lead', ':leadId')
|
||||
)
|
||||
->setParameter('leadId', $leadId);
|
||||
$result = $qb->getQuery()->getArrayResult();
|
||||
|
||||
return array_column($result, 'id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\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 Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
class LeadNote extends FormEntity
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $type = 'general';
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateTime;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_notes')
|
||||
->setCustomRepositoryClass(LeadNoteRepository::class);
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', false, 'notes');
|
||||
|
||||
$builder->addField('text', 'text');
|
||||
|
||||
$builder->createField('type', 'string')
|
||||
->length(50)
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('dateTime', 'datetime')
|
||||
->columnName('date_time')
|
||||
->nullable()
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('leadNote')
|
||||
->addProperties(
|
||||
[
|
||||
'id',
|
||||
'text',
|
||||
'type',
|
||||
'dateTime',
|
||||
'lead',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set text.
|
||||
*
|
||||
* @param string $text
|
||||
*
|
||||
* @return LeadNote
|
||||
*/
|
||||
public function setText($text)
|
||||
{
|
||||
$this->isChanged('text', $text);
|
||||
$this->text = $text;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getText()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set type.
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return LeadNote
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->isChanged('type', $type);
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation rules.
|
||||
*/
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('text', new NotBlank(
|
||||
['message' => 'mautic.lead.note.text.notblank']
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
public function setLead(Lead $lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
public function convertToArray(): array
|
||||
{
|
||||
return get_object_vars($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDateTime()
|
||||
{
|
||||
return $this->dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $dateTime
|
||||
*/
|
||||
public function setDateTime($dateTime): void
|
||||
{
|
||||
$this->dateTime = $dateTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<LeadNote>
|
||||
*/
|
||||
class LeadNoteRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* {@inhertidoc}.
|
||||
*
|
||||
* @return Paginator
|
||||
*/
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$q = $this
|
||||
->createQueryBuilder('n')
|
||||
->select('n');
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getNoteCount($leadId, $filter = null, $noteTypes = null)
|
||||
{
|
||||
$q = $this
|
||||
->createQueryBuilder('n');
|
||||
$q->select('count(n.id) as note_count')
|
||||
->where($q->expr()->eq('IDENTITY(n.lead)', ':lead'))
|
||||
->setParameter('lead', $leadId);
|
||||
|
||||
if (null != $filter) {
|
||||
$q->andWhere(
|
||||
$q->expr()->like('n.text', ':filter')
|
||||
)->setParameter('filter', '%'.$filter.'%');
|
||||
}
|
||||
|
||||
if (null != $noteTypes) {
|
||||
$q->andWhere(
|
||||
$q->expr()->in('n.type', ':noteTypes')
|
||||
)->setParameter('noteTypes', $noteTypes);
|
||||
}
|
||||
|
||||
$results = $q->getQuery()->getArrayResult();
|
||||
|
||||
return $results[0]['note_count'];
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'n';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
*/
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardCatchAllWhereClause(
|
||||
$q,
|
||||
$filter,
|
||||
[
|
||||
'n.text',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
*/
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
$command = $filter->command;
|
||||
$string = $filter->string;
|
||||
$unique = $this->generateRandomParameterName();
|
||||
$returnParameter = false; // returning a parameter that is not used will lead to a Doctrine error
|
||||
[$expr, $parameters] = parent::addSearchCommandWhereClause($q, $filter);
|
||||
|
||||
switch ($command) {
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.type'):
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.type', [], null, 'en_US'):
|
||||
switch ($string) {
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.general'):
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.general', [], null, 'en_US'):
|
||||
$filter->string = 'general';
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.call'):
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.call', [], null, 'en_US'):
|
||||
$filter->string = 'call';
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.email'):
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.email', [], null, 'en_US'):
|
||||
$filter->string = 'email';
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.meeting'):
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.meeting', [], null, 'en_US'):
|
||||
$filter->string = 'meeting';
|
||||
$returnParameter = true;
|
||||
break;
|
||||
}
|
||||
$expr = $q->expr()->eq('n.type', ":$unique");
|
||||
$filter->strict = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($expr && $filter->not) {
|
||||
$expr = $q->expr()->not($expr);
|
||||
}
|
||||
|
||||
if ($returnParameter) {
|
||||
$string = ($filter->strict) ? $filter->string : "%{$filter->string}%";
|
||||
$parameters = ["$unique" => $string];
|
||||
}
|
||||
|
||||
return [
|
||||
$expr,
|
||||
$parameters,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string[]>
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
$commands = [
|
||||
'mautic.lead.note.searchcommand.type' => [
|
||||
'mautic.lead.note.searchcommand.general',
|
||||
'mautic.lead.note.searchcommand.call',
|
||||
'mautic.lead.note.searchcommand.email',
|
||||
'mautic.lead.note.searchcommand.meeting',
|
||||
],
|
||||
];
|
||||
|
||||
return array_merge($commands, parent::getSearchCommands());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
$this->_em->getConnection()->createQueryBuilder()
|
||||
->update(MAUTIC_TABLE_PREFIX.'lead_notes')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class ListLead
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const TABLE_NAME = 'lead_lists_leads';
|
||||
|
||||
/**
|
||||
* @var LeadList
|
||||
**/
|
||||
private $list;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $manuallyRemoved = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $manuallyAdded = false;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_lists_leads')
|
||||
->setCustomRepositoryClass(ListLeadRepository::class);
|
||||
|
||||
$builder->createManyToOne('list', 'LeadList')
|
||||
->isPrimaryKey()
|
||||
->inversedBy('leads')
|
||||
->addJoinColumn('leadlist_id', 'id', false, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', true);
|
||||
|
||||
$builder->addDateAdded();
|
||||
|
||||
$builder->createField('manuallyRemoved', 'boolean')
|
||||
->columnName('manually_removed')
|
||||
->build();
|
||||
|
||||
$builder->createField('manuallyAdded', 'boolean')
|
||||
->columnName('manually_added')
|
||||
->build();
|
||||
|
||||
$builder->addIndex(['manually_removed'], 'manually_removed');
|
||||
$builder->addIndex(['lead_id', 'leadlist_id', 'manually_removed'], 'lead_id_lists_id_removed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $date
|
||||
*/
|
||||
public function setDateAdded($date): void
|
||||
{
|
||||
$this->dateAdded = $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $lead
|
||||
*/
|
||||
public function setLead($lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadList
|
||||
*/
|
||||
public function getList()
|
||||
{
|
||||
return $this->list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LeadList $leadList
|
||||
*/
|
||||
public function setList($leadList): void
|
||||
{
|
||||
$this->list = $leadList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getManuallyRemoved()
|
||||
{
|
||||
return $this->manuallyRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $manuallyRemoved
|
||||
*/
|
||||
public function setManuallyRemoved($manuallyRemoved): void
|
||||
{
|
||||
$this->manuallyRemoved = $manuallyRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function wasManuallyRemoved()
|
||||
{
|
||||
return $this->manuallyRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getManuallyAdded()
|
||||
{
|
||||
return $this->manuallyAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $manuallyAdded
|
||||
*/
|
||||
public function setManuallyAdded($manuallyAdded): void
|
||||
{
|
||||
$this->manuallyAdded = $manuallyAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function wasManuallyAdded()
|
||||
{
|
||||
return $this->manuallyAdded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<ListLead>
|
||||
*/
|
||||
class ListLeadRepository extends CommonRepository
|
||||
{
|
||||
public const DELETE_BATCH_SIZE = 5000;
|
||||
|
||||
/**
|
||||
* 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('l.leadlist_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'l')
|
||||
->where('l.lead_id = '.$toLeadId)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$lists = [];
|
||||
foreach ($results as $r) {
|
||||
$lists[] = $r['leadlist_id'];
|
||||
}
|
||||
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'lead_lists_leads')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId);
|
||||
|
||||
if (!empty($lists)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->notIn('leadlist_id', $lists)
|
||||
)->executeStatement();
|
||||
|
||||
// Delete remaining leads as the new lead already belongs
|
||||
$this->_em->getConnection()->createQueryBuilder()
|
||||
->delete(MAUTIC_TABLE_PREFIX.'lead_lists_leads')
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
} else {
|
||||
$q->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $filters
|
||||
*/
|
||||
public function getContactsCountBySegment(int $segmentId, array $filters = []): int
|
||||
{
|
||||
$qb = $this->createQueryBuilder('ll');
|
||||
$qb->select('count(ll.list) as count')
|
||||
->where('ll.list = :segmentId')
|
||||
->setParameter('segmentId', $segmentId);
|
||||
|
||||
foreach ($filters as $colName => $val) {
|
||||
$entityFieldName = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $colName))));
|
||||
$qb->andWhere(sprintf('ll.%s=:%s', $entityFieldName, $entityFieldName));
|
||||
$qb->setParameter($entityFieldName, $val);
|
||||
}
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function deleteAnonymousContacts(): int
|
||||
{
|
||||
$conn = $this->getEntityManager()->getConnection();
|
||||
$tableName = $this->getTableName();
|
||||
$leadsTableName = MAUTIC_TABLE_PREFIX.'leads';
|
||||
$tempTableName = 'to_delete';
|
||||
$conn->executeQuery(sprintf('DROP TEMPORARY TABLE IF EXISTS %s', $tempTableName));
|
||||
$conn->executeQuery(sprintf('CREATE TEMPORARY TABLE %s select lll.leadlist_id, lll.lead_id from %s lll join %s l on l.id = lll.lead_id where l.date_identified is null;', $tempTableName, $tableName, $leadsTableName));
|
||||
$deleteQuery = sprintf('DELETE lll FROM %s lll JOIN (SELECT leadlist_id, lead_id FROM %s LIMIT %d) d USING (leadlist_id, lead_id); ', $tableName, $tempTableName, self::DELETE_BATCH_SIZE);
|
||||
$deletedRecordCount= 0;
|
||||
while ($deletedRows = $conn->executeQuery($deleteQuery)->rowCount()) {
|
||||
$deletedRecordCount += $deletedRows;
|
||||
}
|
||||
|
||||
return $deletedRecordCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class MergeRecord
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $contact;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $mergedId;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('contact_merge_records')
|
||||
->setCustomRepositoryClass(MergeRecordRepository::class)
|
||||
->addIndex(['date_added'], 'contact_merge_date_added')
|
||||
->addIndex(['merged_id'], 'contact_merge_ids');
|
||||
|
||||
$builder->createField('id', 'integer')
|
||||
->makePrimaryKey()
|
||||
->generatedValue()
|
||||
->build();
|
||||
|
||||
$builder->addContact()
|
||||
->addDateAdded()
|
||||
->addNamedField('mergedId', 'integer', 'merged_id')
|
||||
->addField('name', 'string');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getContact()
|
||||
{
|
||||
return $this->contact;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MergeRecord
|
||||
*/
|
||||
public function setContact(Lead $contact)
|
||||
{
|
||||
$this->contact = $contact;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MergeRecord
|
||||
*/
|
||||
public function setDateAdded(?\DateTime $dateAdded = null)
|
||||
{
|
||||
if (null === $dateAdded) {
|
||||
$dateAdded = new \DateTime();
|
||||
}
|
||||
|
||||
$this->dateAdded = $dateAdded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return MergeRecord
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getMergedId()
|
||||
{
|
||||
return $this->mergedId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $mergedId
|
||||
*
|
||||
* @return MergeRecord
|
||||
*/
|
||||
public function setMergedId($mergedId)
|
||||
{
|
||||
$this->mergedId = (int) $mergedId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<MergeRecord>
|
||||
*/
|
||||
class MergeRecordRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* @return Lead|null
|
||||
*/
|
||||
public function findMergedContact($id)
|
||||
{
|
||||
/** @var MergeRecord $record */
|
||||
if ($record = $this->findOneBy(['mergedId' => (int) $id], ['dateAdded' => 'desc'])) {
|
||||
$contact = $record->getContact();
|
||||
|
||||
// Clear these records from the EM so that subsequent fetches don't return deleted entities
|
||||
$this->getEntityManager()->detach($record);
|
||||
|
||||
return $contact;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep track of subseqent merges by cascading records to the latest lead that was merged into.
|
||||
*/
|
||||
public function moveMergeRecord($fromId, $toId): void
|
||||
{
|
||||
$this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->update(MAUTIC_TABLE_PREFIX.'contact_merge_records')
|
||||
->set('contact_id', (int) $toId)
|
||||
->where('contact_id = '.(int) $fromId)
|
||||
->executeQuery();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\LeadBundle\Segment\OperatorOptions;
|
||||
|
||||
trait OperatorListTrait
|
||||
{
|
||||
/**
|
||||
* @var array<string, array<string, array<int, string>>>
|
||||
*/
|
||||
protected $typeOperators = [
|
||||
'text' => [
|
||||
'include' => [
|
||||
OperatorOptions::EQUAL_TO,
|
||||
OperatorOptions::NOT_EQUAL_TO,
|
||||
OperatorOptions::EMPTY,
|
||||
OperatorOptions::NOT_EMPTY,
|
||||
OperatorOptions::LIKE,
|
||||
OperatorOptions::NOT_LIKE,
|
||||
OperatorOptions::REGEXP,
|
||||
OperatorOptions::NOT_REGEXP,
|
||||
OperatorOptions::STARTS_WITH,
|
||||
OperatorOptions::ENDS_WITH,
|
||||
OperatorOptions::CONTAINS,
|
||||
],
|
||||
],
|
||||
'select' => [
|
||||
'include' => [
|
||||
OperatorOptions::EQUAL_TO,
|
||||
OperatorOptions::NOT_EQUAL_TO,
|
||||
OperatorOptions::EMPTY,
|
||||
OperatorOptions::NOT_EMPTY,
|
||||
OperatorOptions::REGEXP,
|
||||
OperatorOptions::NOT_REGEXP,
|
||||
OperatorOptions::INCLUDING_ANY,
|
||||
OperatorOptions::EXCLUDING_ANY,
|
||||
OperatorOptions::INCLUDING_ALL,
|
||||
OperatorOptions::EXCLUDING_ALL,
|
||||
],
|
||||
],
|
||||
'bool' => [
|
||||
'include' => [
|
||||
OperatorOptions::EQUAL_TO,
|
||||
OperatorOptions::NOT_EQUAL_TO,
|
||||
],
|
||||
],
|
||||
'default' => [
|
||||
'include' => [
|
||||
OperatorOptions::EQUAL_TO,
|
||||
OperatorOptions::NOT_EQUAL_TO,
|
||||
OperatorOptions::GREATER_THAN,
|
||||
OperatorOptions::GREATER_THAN_OR_EQUAL,
|
||||
OperatorOptions::LESS_THAN,
|
||||
OperatorOptions::LESS_THAN_OR_EQUAL,
|
||||
OperatorOptions::EMPTY,
|
||||
OperatorOptions::NOT_EMPTY,
|
||||
OperatorOptions::LIKE,
|
||||
OperatorOptions::NOT_LIKE,
|
||||
OperatorOptions::BETWEEN,
|
||||
OperatorOptions::NOT_BETWEEN,
|
||||
OperatorOptions::REGEXP,
|
||||
OperatorOptions::NOT_REGEXP,
|
||||
OperatorOptions::STARTS_WITH,
|
||||
OperatorOptions::ENDS_WITH,
|
||||
OperatorOptions::CONTAINS,
|
||||
],
|
||||
],
|
||||
'multiselect' => [
|
||||
'include' => [
|
||||
OperatorOptions::INCLUDING_ANY,
|
||||
OperatorOptions::EXCLUDING_ANY,
|
||||
OperatorOptions::INCLUDING_ALL,
|
||||
OperatorOptions::EXCLUDING_ALL,
|
||||
OperatorOptions::EMPTY,
|
||||
OperatorOptions::NOT_EMPTY,
|
||||
],
|
||||
],
|
||||
'date' => [
|
||||
'include' => [
|
||||
OperatorOptions::EQUAL_TO,
|
||||
OperatorOptions::NOT_EQUAL_TO,
|
||||
OperatorOptions::GREATER_THAN,
|
||||
OperatorOptions::GREATER_THAN_OR_EQUAL,
|
||||
OperatorOptions::LESS_THAN,
|
||||
OperatorOptions::LESS_THAN_OR_EQUAL,
|
||||
OperatorOptions::EMPTY,
|
||||
OperatorOptions::NOT_EMPTY,
|
||||
OperatorOptions::LIKE,
|
||||
OperatorOptions::NOT_LIKE,
|
||||
OperatorOptions::BETWEEN,
|
||||
OperatorOptions::NOT_BETWEEN,
|
||||
OperatorOptions::REGEXP,
|
||||
OperatorOptions::NOT_REGEXP,
|
||||
OperatorOptions::DATE,
|
||||
OperatorOptions::STARTS_WITH,
|
||||
OperatorOptions::ENDS_WITH,
|
||||
OperatorOptions::CONTAINS,
|
||||
],
|
||||
],
|
||||
'lookup_id' => [
|
||||
'include' => [
|
||||
OperatorOptions::EQUAL_TO,
|
||||
OperatorOptions::NOT_EQUAL_TO,
|
||||
OperatorOptions::EMPTY,
|
||||
OperatorOptions::NOT_EMPTY,
|
||||
],
|
||||
],
|
||||
'number' => [
|
||||
'include' => [
|
||||
OperatorOptions::EQUAL_TO,
|
||||
OperatorOptions::NOT_EQUAL_TO,
|
||||
OperatorOptions::GREATER_THAN,
|
||||
OperatorOptions::GREATER_THAN_OR_EQUAL,
|
||||
OperatorOptions::LESS_THAN,
|
||||
OperatorOptions::LESS_THAN_OR_EQUAL,
|
||||
OperatorOptions::EMPTY,
|
||||
OperatorOptions::NOT_EMPTY,
|
||||
OperatorOptions::LIKE,
|
||||
OperatorOptions::NOT_LIKE,
|
||||
OperatorOptions::REGEXP,
|
||||
OperatorOptions::NOT_REGEXP,
|
||||
OperatorOptions::STARTS_WITH,
|
||||
OperatorOptions::ENDS_WITH,
|
||||
OperatorOptions::CONTAINS,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @deprecated to be removed in Mautic 3. Use FilterOperatorProvider::getAllOperators() instead.
|
||||
*
|
||||
* @param string|null $operator
|
||||
*
|
||||
* @return array<string,array<string,string>>|array<string,string>
|
||||
*/
|
||||
public function getFilterExpressionFunctions($operator = null)
|
||||
{
|
||||
$operatorOption = OperatorOptions::getFilterExpressionFunctions();
|
||||
|
||||
return (null === $operator) ? $operatorOption : $operatorOption[$operator];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|mixed[]|null $type
|
||||
* @param mixed[] $overrideHiddenTypes
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getOperatorsForFieldType($type = null, $overrideHiddenTypes = [])
|
||||
{
|
||||
static $processedTypes = [];
|
||||
|
||||
if (is_array($type)) {
|
||||
return $this->getOperatorChoiceList($type, $overrideHiddenTypes);
|
||||
} elseif (array_key_exists($type, $processedTypes)) {
|
||||
return $processedTypes[$type];
|
||||
}
|
||||
|
||||
$type = $this->normalizeType($type);
|
||||
|
||||
if (null === $type) {
|
||||
foreach ($this->typeOperators as $type => $def) {
|
||||
if (!array_key_exists($type, $processedTypes)) {
|
||||
$processedTypes[$type] = $this->getOperatorChoiceList($def, $overrideHiddenTypes);
|
||||
}
|
||||
}
|
||||
|
||||
return $processedTypes;
|
||||
}
|
||||
|
||||
$processedTypes[$type] = $this->getOperatorChoiceList($this->typeOperators[$type], $overrideHiddenTypes);
|
||||
|
||||
return $processedTypes[$type];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $definition
|
||||
* @param mixed[] $overrideHiddenOperators
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getOperatorChoiceList($definition, $overrideHiddenOperators = []): array
|
||||
{
|
||||
static $operatorChoices = [];
|
||||
if (empty($operatorChoices)) {
|
||||
$operatorList = $this->getFilterExpressionFunctions();
|
||||
$operatorChoices = [];
|
||||
foreach ($operatorList as $operator => $def) {
|
||||
if (empty($def['hide']) || in_array($operator, $overrideHiddenOperators)) {
|
||||
$operatorChoices[$operator] = $def['label'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$choices = $operatorChoices;
|
||||
if (isset($definition['include'])) {
|
||||
// Inclusive operators
|
||||
$choices = array_intersect_key($choices, array_flip($definition['include']));
|
||||
} elseif (isset($definition['exclude'])) {
|
||||
// Exclusive operators
|
||||
$choices = array_diff_key($choices, array_flip($definition['exclude']));
|
||||
}
|
||||
|
||||
if (property_exists($this, 'translator')) { // @phpstan-ignore-line based on https://github.com/phpstan/phpstan/issues/9095 (Call to function property_exists() with ... 'translator' will always evaluate to false.)
|
||||
foreach ($choices as $value => $label) {
|
||||
$choices[$value] = $this->translator->trans($label);
|
||||
}
|
||||
}
|
||||
|
||||
return array_flip($choices);
|
||||
}
|
||||
|
||||
protected function normalizeType(mixed $type): mixed
|
||||
{
|
||||
if (null === $type) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
if ('boolean' === $type) {
|
||||
return 'bool';
|
||||
}
|
||||
|
||||
if (in_array($type, ['country', 'timezone', 'region', 'locale'])) {
|
||||
return 'select';
|
||||
}
|
||||
|
||||
if (in_array($type, ['lookup', 'text', 'email', 'url', 'email', 'tel'])) {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
if ('datetime' === $type) {
|
||||
return 'date';
|
||||
}
|
||||
|
||||
if (!array_key_exists($type, $this->typeOperators)) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
|
||||
class PointsChangeLog
|
||||
{
|
||||
public const TABLE_NAME = 'lead_points_change_log';
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var IpAddress|null
|
||||
*/
|
||||
private $ipAddress;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $eventName;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $actionName;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $delta;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
private ?Group $group = null;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(PointsChangeLogRepository::class)
|
||||
->addIndex(['date_added'], 'point_date_added');
|
||||
|
||||
$builder->addBigIntIdField();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', false, 'pointsChangeLog');
|
||||
|
||||
$builder->addIpAddress(true);
|
||||
|
||||
$builder->createField('type', 'text')
|
||||
->length(50)
|
||||
->build();
|
||||
|
||||
$builder->createField('eventName', 'string')
|
||||
->columnName('event_name')
|
||||
->build();
|
||||
|
||||
$builder->createField('actionName', 'string')
|
||||
->columnName('action_name')
|
||||
->build();
|
||||
|
||||
$builder->addField('delta', 'integer');
|
||||
|
||||
$builder->createManyToOne('group', Group::class)
|
||||
->addJoinColumn('group_id', 'id', true, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->addDateAdded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*/
|
||||
public function getId(): int
|
||||
{
|
||||
return (int) $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set type.
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return PointsChangeLog
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set eventName.
|
||||
*
|
||||
* @param string $eventName
|
||||
*
|
||||
* @return PointsChangeLog
|
||||
*/
|
||||
public function setEventName($eventName)
|
||||
{
|
||||
$this->eventName = $eventName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get eventName.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEventName()
|
||||
{
|
||||
return $this->eventName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set actionName.
|
||||
*
|
||||
* @param string $actionName
|
||||
*
|
||||
* @return PointsChangeLog
|
||||
*/
|
||||
public function setActionName($actionName)
|
||||
{
|
||||
$this->actionName = $actionName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actionName.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getActionName()
|
||||
{
|
||||
return $this->actionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set delta.
|
||||
*
|
||||
* @param int $delta
|
||||
*
|
||||
* @return PointsChangeLog
|
||||
*/
|
||||
public function setDelta($delta)
|
||||
{
|
||||
$this->delta = $delta;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get delta.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getDelta()
|
||||
{
|
||||
return $this->delta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set dateAdded.
|
||||
*
|
||||
* @param \DateTime $dateAdded
|
||||
*
|
||||
* @return PointsChangeLog
|
||||
*/
|
||||
public function setDateAdded($dateAdded)
|
||||
{
|
||||
$this->dateAdded = $dateAdded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dateAdded.
|
||||
*
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lead.
|
||||
*
|
||||
* @return PointsChangeLog
|
||||
*/
|
||||
public function setLead(Lead $lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lead.
|
||||
*
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PointsChangeLog
|
||||
*/
|
||||
public function setIpAddress(IpAddress $ipAddress)
|
||||
{
|
||||
$this->ipAddress = $ipAddress;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IpAddress
|
||||
*/
|
||||
public function getIpAddress()
|
||||
{
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
public function getGroup(): ?Group
|
||||
{
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
public function setGroup(Group $group): void
|
||||
{
|
||||
$this->group = $group;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<PointsChangeLog>
|
||||
*/
|
||||
class PointsChangeLogRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
|
||||
/**
|
||||
* Get a lead's point log.
|
||||
*
|
||||
* @param int|null $leadId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLeadTimelineEvents($leadId = null, array $options = [])
|
||||
{
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_points_change_log', 'lp')
|
||||
->select('lp.event_name as eventName, lp.action_name as actionName, lp.date_added as dateAdded, lp.type, lp.delta, lp.id, lp.lead_id, pl.name as groupName')
|
||||
->leftJoin('lp', MAUTIC_TABLE_PREFIX.Group::TABLE_NAME, 'pl', 'lp.group_id = pl.id');
|
||||
|
||||
if ($leadId) {
|
||||
$query->where('lp.lead_id = '.(int) $leadId);
|
||||
}
|
||||
|
||||
if (isset($options['search']) && $options['search']) {
|
||||
$query->andWhere(
|
||||
$query->expr()->or(
|
||||
$query->expr()->like('lp.event_name', ':search'),
|
||||
$query->expr()->like('lp.action_name', ':search')
|
||||
)
|
||||
)->setParameter('search', '%'.$options['search'].'%');
|
||||
}
|
||||
|
||||
return $this->getTimelineResults($query, $options, 'lp.event_name', 'lp.date_added', [], ['dateAdded'], null, 'lp.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table stat data from point log table.
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getMostPoints(QueryBuilder $query, $limit = 10, $offset = 0): array
|
||||
{
|
||||
$query->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $query->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table stat data from lead table.
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getMostLeads(QueryBuilder $query, $limit = 10, $offset = 0): array
|
||||
{
|
||||
$query->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $query->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*
|
||||
* @param int $fromLeadId
|
||||
* @param int $toLeadId
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'lead_points_change_log')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'lp';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
trait RegexTrait
|
||||
{
|
||||
/**
|
||||
* Ensure that special characters are escaped correctly.
|
||||
*
|
||||
* @return string|string[]
|
||||
*/
|
||||
protected function prepareRegex($regex): string|array
|
||||
{
|
||||
$search = [
|
||||
'\\\\',
|
||||
];
|
||||
|
||||
$replace = [
|
||||
'\\',
|
||||
];
|
||||
|
||||
return str_replace($search, $replace, $regex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\StageBundle\Entity\Stage;
|
||||
|
||||
class StagesChangeLog
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var Stage|null
|
||||
*/
|
||||
private $stage;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $eventName;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $actionName;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_stages_change_log')
|
||||
->setCustomRepositoryClass(StagesChangeLogRepository::class)
|
||||
->addIndex(['date_added'], 'lead_stages_change_log_date_added');
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', false, 'stageChangeLog');
|
||||
|
||||
$builder->createField('eventName', 'string')
|
||||
->columnName('event_name')
|
||||
->build();
|
||||
|
||||
$builder->createField('actionName', 'string')
|
||||
->columnName('action_name')
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('stage', Stage::class)
|
||||
->inversedBy('log')
|
||||
->addJoinColumn('stage_id', 'id', true, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->addDateAdded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set eventName.
|
||||
*
|
||||
* @param string $eventName
|
||||
*
|
||||
* @return StagesChangeLog
|
||||
*/
|
||||
public function setEventName($eventName)
|
||||
{
|
||||
$this->eventName = $eventName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get eventName.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEventName()
|
||||
{
|
||||
return $this->eventName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set actionName.
|
||||
*
|
||||
* @param string $actionName
|
||||
*
|
||||
* @return StagesChangeLog
|
||||
*/
|
||||
public function setActionName($actionName)
|
||||
{
|
||||
$this->actionName = $actionName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actionName.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getActionName()
|
||||
{
|
||||
return $this->actionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set dateAdded.
|
||||
*
|
||||
* @param \DateTime $dateAdded
|
||||
*
|
||||
* @return StagesChangeLog
|
||||
*/
|
||||
public function setDateAdded($dateAdded)
|
||||
{
|
||||
$this->dateAdded = $dateAdded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dateAdded.
|
||||
*
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lead.
|
||||
*
|
||||
* @return StagesChangeLog
|
||||
*/
|
||||
public function setLead(Lead $lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lead.
|
||||
*
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set stage.
|
||||
*
|
||||
* @return StagesChangeLog
|
||||
*/
|
||||
public function setStage(Stage $stage)
|
||||
{
|
||||
$this->stage = $stage;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stage.
|
||||
*
|
||||
* @return Stage
|
||||
*/
|
||||
public function getStage()
|
||||
{
|
||||
return $this->stage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<StagesChangeLog>
|
||||
*/
|
||||
class StagesChangeLogRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
|
||||
/**
|
||||
* Get a lead's stage log.
|
||||
*
|
||||
* @param int|null $leadId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLeadTimelineEvents($leadId = null, array $options = [])
|
||||
{
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_stages_change_log', 'ls')
|
||||
->select('ls.id, ls.stage_id as reference, ls.event_name as eventName, ls.action_name as actionName, ls.date_added as dateAdded, ls.lead_id');
|
||||
|
||||
if ($leadId) {
|
||||
$query->where('ls.lead_id = '.(int) $leadId);
|
||||
}
|
||||
|
||||
if (isset($options['search']) && $options['search']) {
|
||||
$query->andWhere(
|
||||
$query->expr()->or(
|
||||
$query->expr()->like('ls.event_name', ':search'),
|
||||
$query->expr()->like('ls.action_name', ':search')
|
||||
)
|
||||
)->setParameter('search', '%'.$options['search'].'%');
|
||||
}
|
||||
|
||||
return $this->getTimelineResults($query, $options, 'ls.event_name', 'ls.date_added', [], ['dateAdded'], null, 'ls.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*
|
||||
* @param int $fromLeadId
|
||||
* @param int $toLeadId
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'lead_stages_change_log')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current stage assigned to a lead.
|
||||
*
|
||||
* @param int $leadId
|
||||
*/
|
||||
public function getCurrentLeadStage($leadId): ?int
|
||||
{
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$query->select('stage_id as stage')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_stages_change_log', 'ls')
|
||||
->where($query->expr()->eq('lead_id', ':value'))
|
||||
->setParameter('value', $leadId)
|
||||
->orderBy('date_added', 'DESC');
|
||||
|
||||
$result = $query->executeQuery()->fetchAssociative();
|
||||
|
||||
return (isset($result['stage'])) ? (int) $result['stage'] : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\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\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(security: "is_granted('tagManager:tagManager:view')"),
|
||||
new Post(security: "is_granted('tagManager:tagManager:create')"),
|
||||
new Get(security: "is_granted('tagManager:tagManager:view')"),
|
||||
new Put(security: "is_granted('tagManager:tagManager:edit')"),
|
||||
new Patch(security: "is_granted('tagManager:tagManager:edit')"),
|
||||
new Delete(security: "is_granted('tagManager:tagManager:delete')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['leadfield:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['leadfield:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class Tag implements UuidInterface
|
||||
{
|
||||
use UuidTrait;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['leadfield:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $tag;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $description;
|
||||
|
||||
public ?int $deletedId;
|
||||
|
||||
public function __construct(?string $tag = null, bool $clean = true)
|
||||
{
|
||||
$this->tag = $clean && $tag ? $this->validateTag($tag) : $tag;
|
||||
}
|
||||
|
||||
public static function loadMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
$builder->setTable('lead_tags')
|
||||
->setCustomRepositoryClass(TagRepository::class)
|
||||
->addIndex(['tag'], 'lead_tag_search');
|
||||
|
||||
$builder->addId();
|
||||
$builder->addField('tag', Types::STRING);
|
||||
$builder->addNamedField('description', Types::TEXT, 'description', true);
|
||||
static::addUuidField($builder);
|
||||
}
|
||||
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('tag')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'tag',
|
||||
'description',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTag()
|
||||
{
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Tag
|
||||
*/
|
||||
public function setTag(string $tag)
|
||||
{
|
||||
$this->tag = $this->validateTag($tag);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $description
|
||||
*
|
||||
* @return Tag
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function validateTag(string $tag): string
|
||||
{
|
||||
return InputHelper::string(trim((string) $tag));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Tag>
|
||||
*/
|
||||
class TagRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* Delete orphan tags that are not associated with any lead.
|
||||
*/
|
||||
public function deleteOrphans(): void
|
||||
{
|
||||
$qb = $this->_em->getConnection()->createQueryBuilder();
|
||||
$havingQb = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$havingQb->select('count(x.lead_id) as the_count')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_tags_xref', 'x')
|
||||
->where('x.tag_id = t.id');
|
||||
|
||||
$qb->select('t.id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_tags', 't')
|
||||
->having(sprintf('(%s)', $havingQb->getSQL()).' = 0');
|
||||
$delete = $qb->executeQuery()->fetchAssociative();
|
||||
|
||||
if (count($delete)) {
|
||||
$qb->resetQueryParts();
|
||||
$qb->delete(MAUTIC_TABLE_PREFIX.'lead_tags')
|
||||
->where(
|
||||
$qb->expr()->in('id', $delete)
|
||||
)
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag entities by name.
|
||||
*
|
||||
* @param string[] $tags
|
||||
*
|
||||
* @return Tag[]
|
||||
*/
|
||||
public function getTagsByName(array $tags): array
|
||||
{
|
||||
if (empty($tags)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tags = $this->removeMinusFromTags($tags);
|
||||
$qb = $this->createQueryBuilder('t', 't.tag');
|
||||
|
||||
if ($tags) {
|
||||
$qb->where(
|
||||
$qb->expr()->in('t.tag', ':tags')
|
||||
)
|
||||
->setParameter('tags', $tags);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes through each element in the array expecting it to be a tag label and removes the '-' character infront of it.
|
||||
* The minus character is used to identify that the tag should be removed.
|
||||
*/
|
||||
public function removeMinusFromTags(array $tags): array
|
||||
{
|
||||
return array_map(fn ($val) => (str_starts_with($val, '-')) ? substr($val, 1) : $val, $tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Lead tags by Ids.
|
||||
*/
|
||||
public function checkLeadByTags(Lead $lead, $tags): bool
|
||||
{
|
||||
if (empty($tags)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('l.id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
|
||||
->join('l', MAUTIC_TABLE_PREFIX.'lead_tags_xref', 'x', 'l.id = x.lead_id')
|
||||
->join('l', MAUTIC_TABLE_PREFIX.'lead_tags', 't', 'x.tag_id = t.id')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->in('t.tag', ':tags'),
|
||||
$q->expr()->eq('l.id', ':leadId')
|
||||
)
|
||||
)
|
||||
->setParameter('tags', $tags, ArrayParameterType::STRING)
|
||||
->setParameter('leadId', $lead->getId());
|
||||
|
||||
return (bool) $q->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return Tag
|
||||
*/
|
||||
public function getTagByNameOrCreateNewOne($name)
|
||||
{
|
||||
$tag = new Tag($name, true);
|
||||
|
||||
/** @var Tag|null $existingTag */
|
||||
$existingTag = $this->findOneBy(
|
||||
[
|
||||
'tag' => $tag->getTag(),
|
||||
]
|
||||
);
|
||||
|
||||
return $existingTag ?? $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tags to leads.
|
||||
*
|
||||
* @param array<int> $leadIds
|
||||
* @param array<int> $tagIds
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function addTagsToLeads(array $leadIds, array $tagIds): array
|
||||
{
|
||||
return $this->updateTagsInLeads($leadIds, $tagIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tags in leads.
|
||||
*
|
||||
* @param array<int> $leadIds
|
||||
* @param array<int> $tagIds
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function updateTagsInLeads(array $leadIds, array $tagIds, string $addOrRemove = 'add'): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
if (empty($leadIds) || empty($tagIds)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$tags = $this->getTagById($tagIds);
|
||||
|
||||
if (empty($tags)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
foreach ($leadIds as $leadId) {
|
||||
$lead = $this->_em->find(Lead::class, $leadId);
|
||||
foreach ($tags as $tag) {
|
||||
if ('add' === $addOrRemove) {
|
||||
$lead->addTag($tag);
|
||||
} else {
|
||||
$lead->removeTag($tag);
|
||||
}
|
||||
$result[$leadId][$tag->getId()] = true;
|
||||
}
|
||||
$this->_em->persist($lead);
|
||||
$this->_em->flush();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove tags from leads.
|
||||
*
|
||||
* @param array<int> $leadIds
|
||||
* @param array<int> $tagIds
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function removeTagsFromLeads(array $leadIds, array $tagIds): array
|
||||
{
|
||||
return $this->updateTagsInLeads($leadIds, $tagIds, 'remove');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tags by Id.
|
||||
*
|
||||
* @param array<int>|int $tagIds
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function getTagById(array|int $tagIds): array
|
||||
{
|
||||
if (empty($tagIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!is_array($tagIds)) {
|
||||
$tagIds = [$tagIds];
|
||||
}
|
||||
|
||||
$qb = $this->_em->getConnection()->createQueryBuilder();
|
||||
$tagsIdName = $qb->select('lt.id,lt.tag')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_tags', 'lt')
|
||||
->where('lt.id IN (:tag)')
|
||||
->setParameter('tag', $tagIds, ArrayParameterType::INTEGER)
|
||||
->executeQuery()->fetchAllKeyValue();
|
||||
|
||||
if (empty($tagsIdName)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->getTagsByName($tagsIdName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\CoreBundle\Helper\Serializer;
|
||||
|
||||
trait TimelineTrait
|
||||
{
|
||||
/**
|
||||
* @param QueryBuilder $query DBAL QueryBuilder
|
||||
* @param array<mixed> $options Query optons from LeadTimelineEvent
|
||||
* @param string $eventNameColumn Name of column to sort event name by
|
||||
* @param string $timestampColumn Name of column to sort timestamp by
|
||||
* @param array<mixed> $serializedColumns Array of columns to unserialize
|
||||
* @param array<mixed> $dateTimeColumns Array of columns to be converted to \DateTime
|
||||
* @param mixed|null $resultsParserCallback Callback to custom parse results
|
||||
* @param string|null $secondaryOrdering Name of column for secondary sort
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
private function getTimelineResults(
|
||||
QueryBuilder $query,
|
||||
array $options,
|
||||
$eventNameColumn,
|
||||
$timestampColumn,
|
||||
$serializedColumns = [],
|
||||
$dateTimeColumns = [],
|
||||
$resultsParserCallback = null,
|
||||
?string $secondaryOrdering = null,
|
||||
) {
|
||||
if (!empty($options['unitCounts'])) {
|
||||
[$tablePrefix, $column] = explode('.', $timestampColumn);
|
||||
|
||||
// Get counts grouped by unit based on date range
|
||||
/** @var ChartQuery $cq */
|
||||
$cq = $options['chartQuery'];
|
||||
$cq->modifyTimeDataQuery($query, $column, $tablePrefix);
|
||||
$cq->applyDateFilters($query, $column, $tablePrefix);
|
||||
$data = $query->executeQuery()->fetchAllAssociative();
|
||||
|
||||
return $cq->completeTimeData($data);
|
||||
}
|
||||
|
||||
if (!empty($options['fromDate']) && !empty($options['toDate'])) {
|
||||
$query->andWhere($timestampColumn.' BETWEEN :dateFrom AND :dateTo')
|
||||
->setParameter('dateFrom', $options['fromDate']->format('Y-m-d H:i:s'))
|
||||
->setParameter('dateTo', $options['toDate']->format('Y-m-d H:i:s'));
|
||||
} elseif (!empty($options['fromDate'])) {
|
||||
$query->andWhere($query->expr()->gte($timestampColumn, ':dateFrom'))
|
||||
->setParameter('dateFrom', $options['fromDate']->format('Y-m-d H:i:s'));
|
||||
} elseif (!empty($options['toDate'])) {
|
||||
$query->andWhere($query->expr()->lte($timestampColumn, ':dateTo'))
|
||||
->setParameter('dateTo', $options['toDate']->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
if (isset($options['leadIds'])) {
|
||||
$leadColumn = $this->getTableAlias().'.lead_id';
|
||||
$query->addSelect($leadColumn);
|
||||
$query->andWhere(
|
||||
$query->expr()->in($leadColumn, $options['leadIds'])
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($options['order'])) {
|
||||
[$orderBy, $orderByDir] = $options['order'];
|
||||
|
||||
$orderBy = match ($orderBy) {
|
||||
'eventLabel' => $eventNameColumn,
|
||||
default => $timestampColumn,
|
||||
};
|
||||
|
||||
$query->orderBy($orderBy, $orderByDir);
|
||||
|
||||
if ($secondaryOrdering) {
|
||||
$query->addOrderBy($secondaryOrdering, $orderByDir);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($options['limit'])) {
|
||||
$query->setMaxResults($options['limit']);
|
||||
if (!empty($options['start'])) {
|
||||
$query->setFirstResult($options['start']);
|
||||
}
|
||||
}
|
||||
|
||||
$results = $query->executeQuery()->fetchAllAssociative();
|
||||
|
||||
if (!empty($serializedColumns) || !empty($dateTimeColumns) || is_callable($resultsParserCallback)) {
|
||||
// Convert to array or \DateTime since we're using DBAL here
|
||||
foreach ($results as &$result) {
|
||||
foreach ($serializedColumns as $col) {
|
||||
if (isset($result[$col])) {
|
||||
$result[$col] = (null == $result[$col]) ? [] : Serializer::decode($result[$col]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($dateTimeColumns as $col) {
|
||||
if (isset($result[$col]) && !empty($result[$col])) {
|
||||
$dt = new DateTimeHelper($result[$col], 'Y-m-d H:i:s', 'UTC');
|
||||
$result[$col] = $dt->getLocalDateTime();
|
||||
unset($dt);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_callable($resultsParserCallback)) {
|
||||
$resultsParserCallback($result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($options['paginated'])) {
|
||||
// Get a total count along with results
|
||||
$query->resetQueryParts(['select', 'orderBy'])
|
||||
->setFirstResult(0)
|
||||
->setMaxResults(null)
|
||||
->select('count(*)');
|
||||
|
||||
$total = $query->executeQuery()->fetchOne();
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'results' => $results,
|
||||
];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class UtmTag
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $query = [];
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $referer;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $remoteHost;
|
||||
|
||||
private $url;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $userAgent;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $utmCampaign;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $utmContent;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $utmMedium;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $utmSource;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $utmTerm;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_utmtags');
|
||||
$builder->setCustomRepositoryClass(UtmTagRepository::class);
|
||||
$builder->addId();
|
||||
$builder->addDateAdded();
|
||||
$builder->addLead(false, 'CASCADE', false, 'utmtags');
|
||||
$builder->addNullableField('query', Types::ARRAY);
|
||||
$builder->addNullableField('referer', Types::TEXT);
|
||||
$builder->addNullableField('remoteHost', Types::STRING, 'remote_host');
|
||||
$builder->addNullableField('url', Types::TEXT);
|
||||
$builder->addNullableField('userAgent', Types::TEXT, 'user_agent');
|
||||
$builder->addNullableField('utmCampaign', Types::STRING, 'utm_campaign');
|
||||
$builder->addNullableField('utmContent', Types::STRING, 'utm_content');
|
||||
$builder->addNullableField('utmMedium', Types::STRING, 'utm_medium');
|
||||
$builder->addNullableField('utmSource', Types::STRING, 'utm_source');
|
||||
$builder->addNullableField('utmTerm', Types::STRING, 'utm_term');
|
||||
$builder->addIndex(['date_added'], 'utm_date_added');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('utmtags')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'lead',
|
||||
'query',
|
||||
'referer',
|
||||
'remoteHost',
|
||||
'url',
|
||||
'userAgent',
|
||||
'utmCampaign',
|
||||
'utmContent',
|
||||
'utmMedium',
|
||||
'utmSource',
|
||||
'utmTerm',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set date added.
|
||||
*
|
||||
* @return UtmTag
|
||||
*/
|
||||
public function setDateAdded(\DateTimeInterface $date)
|
||||
{
|
||||
$this->dateAdded = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date added.
|
||||
*
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UtmTag
|
||||
*/
|
||||
public function setLead(Lead $lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getQuery()
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $query
|
||||
*
|
||||
* @return UtmTag
|
||||
*/
|
||||
public function setQuery($query)
|
||||
{
|
||||
$this->query = $query;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set referer.
|
||||
*
|
||||
* @param string $referer
|
||||
*
|
||||
* @return UtmTag
|
||||
*/
|
||||
public function setReferer($referer)
|
||||
{
|
||||
$this->referer = $referer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get referer.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getReferer()
|
||||
{
|
||||
return $this->referer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set remoteHost.
|
||||
*
|
||||
* @param string $remoteHost
|
||||
*
|
||||
* @return UtmTag
|
||||
*/
|
||||
public function setRemoteHost($remoteHost)
|
||||
{
|
||||
$this->remoteHost = $remoteHost;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remoteHost.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRemoteHost()
|
||||
{
|
||||
return $this->remoteHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set url.
|
||||
*
|
||||
* @param string $url
|
||||
*
|
||||
* @return UtmTag
|
||||
*/
|
||||
public function setUrl($url)
|
||||
{
|
||||
$this->url = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get url.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set userAgent.
|
||||
*
|
||||
* @param string $userAgent
|
||||
*
|
||||
* @return UtmTag
|
||||
*/
|
||||
public function setUserAgent($userAgent)
|
||||
{
|
||||
$this->userAgent = $userAgent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get userAgent.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserAgent()
|
||||
{
|
||||
return $this->userAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUtmCampaign()
|
||||
{
|
||||
return $this->utmCampaign;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $utmCampaign
|
||||
*
|
||||
* @return UtmTag
|
||||
*/
|
||||
public function setUtmCampaign($utmCampaign)
|
||||
{
|
||||
$this->utmCampaign = $utmCampaign;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUtmContent()
|
||||
{
|
||||
return $this->utmContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $utmContent
|
||||
*
|
||||
* @return UtmTag
|
||||
*/
|
||||
public function setUtmContent($utmContent)
|
||||
{
|
||||
$utmContent = mb_strlen($utmContent) <= ClassMetadataBuilder::MAX_VARCHAR_INDEXED_LENGTH ? $utmContent : mb_substr($utmContent, 0, ClassMetadataBuilder::MAX_VARCHAR_INDEXED_LENGTH);
|
||||
$this->utmContent = $utmContent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUtmMedium()
|
||||
{
|
||||
return $this->utmMedium;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $utmMedium
|
||||
*
|
||||
* @return UtmTag
|
||||
*/
|
||||
public function setUtmMedium($utmMedium)
|
||||
{
|
||||
$this->utmMedium = $utmMedium;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUtmSource()
|
||||
{
|
||||
return $this->utmSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $utmSource
|
||||
*
|
||||
* @return UtmTag
|
||||
*/
|
||||
public function setUtmSource($utmSource)
|
||||
{
|
||||
$this->utmSource = $utmSource;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUtmTerm()
|
||||
{
|
||||
return $this->utmTerm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $utmTerm
|
||||
*
|
||||
* @return UtmTag
|
||||
*/
|
||||
public function setUtmTerm($utmTerm)
|
||||
{
|
||||
$this->utmTerm = $utmTerm;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasUtmTags(): bool
|
||||
{
|
||||
return !empty($this->utmCampaign) || !empty($this->utmSource) || !empty($this->utmMedium) || !empty($this->utmContent) || !empty($this->utmTerm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Available fields and it's setters.
|
||||
*/
|
||||
public function getFieldSetterList(): array
|
||||
{
|
||||
return [
|
||||
'utm_campaign' => 'setUtmCampaign',
|
||||
'utm_source' => 'setUtmSource',
|
||||
'utm_medium' => 'setUtmMedium',
|
||||
'utm_content' => 'setUtmContent',
|
||||
'utm_term' => 'setUtmTerm',
|
||||
'user_agent' => 'setUserAgent',
|
||||
'url' => 'setUrl',
|
||||
'referer' => 'setReferer',
|
||||
'query' => 'setQuery',
|
||||
'remote_host' => 'setRemoteHost',
|
||||
'date_added' => 'setDateAdded',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<UtmTag>
|
||||
*/
|
||||
class UtmTagRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
|
||||
/**
|
||||
* Get tag entities by lead.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getUtmTagsByLead(?Lead $lead = null, $options = [])
|
||||
{
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('*')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_utmtags', 'ut');
|
||||
|
||||
if ($lead instanceof Lead) {
|
||||
$qb->where('ut.lead_id = '.(int) $lead->getId());
|
||||
}
|
||||
|
||||
if (isset($options['search']) && $options['search']) {
|
||||
$qb->andWhere($qb->expr()->or(
|
||||
$qb->expr()->like('ut.utm_campaign', $qb->expr()->literal('%'.$options['search'].'%')),
|
||||
$qb->expr()->like('ut.utm_content', $qb->expr()->literal('%'.$options['search'].'%')),
|
||||
$qb->expr()->like('ut.utm_medium', $qb->expr()->literal('%'.$options['search'].'%')),
|
||||
$qb->expr()->like('ut.utm_source', $qb->expr()->literal('%'.$options['search'].'%')),
|
||||
$qb->expr()->like('ut.utm_term', $qb->expr()->literal('%'.$options['search'].'%'))
|
||||
));
|
||||
}
|
||||
|
||||
return $this->getTimelineResults($qb, $options, 'ut.utm_campaign', 'ut.date_added', ['query'], ['date_added'], null, 'ut.id');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user