Initial commit: CloudOps infrastructure platform

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

View File

@@ -0,0 +1,330 @@
<?php
namespace Mautic\FormBundle\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\UuidInterface;
use Mautic\CoreBundle\Entity\UuidTrait;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
#[ApiResource(
operations: [
new GetCollection(security: "is_granted('form:forms:viewown')"),
new Post(security: "is_granted('form:forms:create')"),
new Get(security: "is_granted('form:forms:viewown')"),
new Put(security: "is_granted('form:forms:editown')"),
new Patch(security: "is_granted('form:forms:editother')"),
new Delete(security: "is_granted('form:forms:deleteown')"),
],
normalizationContext: [
'groups' => ['action:read'],
'swagger_definition_name' => 'Read',
],
denormalizationContext: [
'groups' => ['action:write'],
'swagger_definition_name' => 'Write',
]
)]
class Action implements UuidInterface
{
use UuidTrait;
public const ENTITY_NAME = 'form_action';
/**
* @var int
*/
#[Groups(['action:read', 'form:read'])]
private $id;
/**
* @var string
*/
#[Groups(['action:read', 'action:write', 'form:read'])]
private $name;
/**
* @var string|null
*/
#[Groups(['action:read', 'action:write', 'form:read'])]
private $description;
/**
* @var string
*/
#[Groups(['action:read', 'action:write', 'form:read'])]
private $type;
/**
* @var int
*/
#[Groups(['action:read', 'action:write', 'form:read'])]
private $order = 0;
/**
* @var array
*/
#[Groups(['action:read', 'action:write', 'form:read'])]
private $properties = [];
/**
* @var Form|null
*/
#[Groups(['action:read', 'action:write', 'form:read'])]
private $form;
/**
* @var array
*/
private $changes;
public function __clone()
{
$this->id = null;
$this->form = null;
}
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('form_actions')
->setCustomRepositoryClass(ActionRepository::class)
->addIndex(['type'], 'form_action_type_search');
$builder->addIdColumns();
$builder->createField('type', 'string')
->length(50)
->build();
$builder->createField('order', 'integer')
->columnName('action_order')
->build();
$builder->addField('properties', 'array');
$builder->createManyToOne('form', 'Form')
->inversedBy('actions')
->addJoinColumn('form_id', 'id', false, false, 'CASCADE')
->build();
static::addUuidField($builder);
}
/**
* Prepares the metadata for API usage.
*/
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
{
$metadata->setGroupPrefix('form')
->addProperties(
[
'id',
'name',
'description',
'type',
'order',
'properties',
]
)
->build();
}
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$metadata->addPropertyConstraint('type', new Assert\NotBlank([
'message' => 'mautic.core.name.required',
'groups' => ['action'],
]));
}
private function isChanged($prop, $val): void
{
if ($this->$prop != $val) {
$this->changes[$prop] = [$this->$prop, $val];
}
}
/**
* @return array
*/
public function getChanges()
{
return $this->changes;
}
/**
* Get id.
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Set order.
*
* @param int $order
*
* @return Action
*/
public function setOrder($order)
{
$this->isChanged('order', $order);
$this->order = $order;
return $this;
}
/**
* Get order.
*
* @return int
*/
public function getOrder()
{
return $this->order;
}
/**
* Set properties.
*
* @param array $properties
*
* @return Action
*/
public function setProperties($properties)
{
$this->isChanged('properties', $properties);
$this->properties = $properties;
return $this;
}
/**
* Get properties.
*
* @return array
*/
public function getProperties()
{
return $this->properties;
}
/**
* Set form.
*
* @return Action
*/
public function setForm(Form $form)
{
$this->form = $form;
return $this;
}
/**
* Get form.
*
* @return Form|null
*/
public function getForm()
{
return $this->form;
}
/**
* Set type.
*
* @param string $type
*
* @return Action
*/
public function setType($type)
{
$this->isChanged('type', $type);
$this->type = $type;
return $this;
}
/**
* Get type.
*
* @return string
*/
public function getType()
{
return $this->type;
}
public function convertToArray(): array
{
return get_object_vars($this);
}
/**
* Set description.
*
* @param string $description
*
* @return Action
*/
public function setDescription($description)
{
$this->isChanged('description', $description);
$this->description = $description;
return $this;
}
/**
* Get description.
*
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set name.
*
* @param string $name
*
* @return Action
*/
public function setName($name)
{
$this->isChanged('name', $name);
$this->name = $name;
return $this;
}
/**
* Get name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Mautic\FormBundle\Entity;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<Action>
*/
class ActionRepository extends CommonRepository
{
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Entity;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<Field>
*/
class FieldRepository extends CommonRepository
{
public function fieldExistsByFormAndType(int $formId, string $type): bool
{
return (bool) $this->getEntityManager()->getConnection()->createQueryBuilder()
->select('1')
->from(MAUTIC_TABLE_PREFIX.Field::TABLE_NAME, 'f')
->where('f.type = :type')
->andWhere('f.form_id = :formId')
->setParameter('type', $type)
->setParameter('formId', $formId)
->executeQuery()
->fetchOne();
}
}

View File

@@ -0,0 +1,940 @@
<?php
namespace Mautic\FormBundle\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
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\InputHelper;
use Mautic\ProjectBundle\Entity\ProjectTrait;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
#[ApiResource(
operations: [
new GetCollection(security: "is_granted('form:forms:viewown')"),
new Post(security: "is_granted('form:forms:create')"),
new Get(security: "is_granted('form:forms:viewown')"),
new Put(security: "is_granted('form:forms:editown')"),
new Patch(security: "is_granted('form:forms:editother')"),
new Delete(security: "is_granted('form:forms:deleteown')"),
],
normalizationContext: [
'groups' => ['form:read'],
'swagger_definition_name' => 'Read',
'api_included' => ['category', 'fields', 'actions'],
],
denormalizationContext: [
'groups' => ['form:write'],
'swagger_definition_name' => 'Write',
]
)]
class Form extends FormEntity implements UuidInterface
{
use UuidTrait;
use ProjectTrait;
public const ENTITY_NAME = 'forms';
/**
* @var int
*/
#[Groups(['form:read', 'download:read', 'campaign:read', 'email:read'])]
private $id;
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private ?string $language = null;
/**
* @var string
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $name;
/**
* @var string|null
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $formAttributes;
/**
* @var string|null
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $description;
/**
* @var string
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $alias;
/**
* @var Category|null
**/
#[Groups(['form:read', 'form:write', 'campaign:read', 'email:read'])]
private $category;
/**
* @var string|null
*/
#[Groups(['form:read', 'download:read', 'campaign:read', 'email:read'])]
private $cachedHtml;
/**
* @var string
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $postAction = 'message';
/**
* @var string|null
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $postActionProperty;
/**
* @var \DateTimeInterface
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $publishUp;
/**
* @var \DateTimeInterface
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $publishDown;
/**
* @var ArrayCollection<int, Field>
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $fields;
/**
* @var ArrayCollection<string, Action>
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $actions;
/**
* @var string|null
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $template;
/**
* @var bool|null
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $inKioskMode = false;
/**
* @var bool|null
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $renderStyle = false;
/**
* @var Collection<int, Submission>
*/
#[Groups(['form:read', 'download:read', 'campaign:read', 'email:read'])]
private Collection $submissions;
#[Groups(['form:read', 'download:read', 'campaign:read', 'email:read'])]
public int $submission_count = 0;
/**
* @var string|null
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $formType;
/**
* @var bool|null
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $noIndex;
/**
* @var int|null
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read', 'email:read'])]
private $progressiveProfilingLimit;
/**
* This var is used to cache the result once gained from the loop.
*
* @var bool
*/
#[Groups(['form:read', 'form:write', 'download:read', 'campaign:read'])]
private $usesProgressiveProfiling;
public function __clone()
{
$this->id = null;
parent::__clone();
}
public function __construct()
{
$this->fields = new ArrayCollection();
$this->actions = new ArrayCollection();
$this->submissions = new ArrayCollection();
$this->noIndex = true;
$this->initializeProjects();
}
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('forms')
->setCustomRepositoryClass(FormRepository::class);
$builder->addIdColumns();
$builder->addField('alias', 'string');
$builder->createField('language', 'string')
->columnName('lang')
->nullable()
->build();
$builder->addNullableField('formAttributes', 'string', 'form_attr');
$builder->addCategory();
$builder->createField('cachedHtml', 'text')
->columnName('cached_html')
->nullable()
->build();
$builder->createField('postAction', 'string')
->columnName('post_action')
->build();
$builder->createField('postActionProperty', Types::TEXT)
->columnName('post_action_property')
->nullable()
->build();
$builder->addPublishDates();
$builder->createOneToMany('fields', 'Field')
->setIndexBy('id')
->setOrderBy(['order' => 'ASC', 'id' => 'ASC'])
->mappedBy('form')
->cascadeAll()
->fetchExtraLazy()
->build();
$builder->createOneToMany('actions', 'Action')
->setIndexBy('id')
->setOrderBy(['order' => 'ASC'])
->mappedBy('form')
->cascadeAll()
->fetchExtraLazy()
->build();
$builder->createField('template', 'string')
->nullable()
->build();
$builder->createField('inKioskMode', 'boolean')
->columnName('in_kiosk_mode')
->nullable()
->build();
$builder->createField('renderStyle', 'boolean')
->columnName('render_style')
->nullable()
->build();
$builder->createOneToMany('submissions', 'Submission')
->setOrderBy(['dateSubmitted' => 'DESC'])
->mappedBy('form')
->fetchExtraLazy()
->build();
$builder->addNullableField('formType', 'string', 'form_type');
$builder->createField('noIndex', 'boolean')
->columnName('no_index')
->nullable()
->build();
$builder->addNullableField('progressiveProfilingLimit', Types::INTEGER, 'progressive_profiling_limit');
static::addUuidField($builder);
self::addProjectsField($builder, 'form_projects_xref', 'form_id');
}
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$metadata->addPropertyConstraint('name', new Assert\NotBlank([
'message' => 'mautic.core.name.required',
'groups' => ['form'],
]));
$metadata->addPropertyConstraint('postActionProperty', new Assert\NotBlank([
'message' => 'mautic.form.form.postactionproperty_message.notblank',
'groups' => ['messageRequired'],
]));
$metadata->addPropertyConstraint('postActionProperty', new Assert\NotBlank([
'message' => 'mautic.form.form.postactionproperty_redirect.notblank',
'groups' => ['urlRequired'],
]));
$metadata->addPropertyConstraint('postActionProperty', new Assert\Url([
'message' => 'mautic.form.form.postactionproperty_redirect.notblank',
'groups' => ['urlRequiredPassTwo'],
]));
$metadata->addPropertyConstraint('postActionProperty', new Assert\NotBlank([
'message' => 'mautic.form.form.postactionproperty_hideform.notblank',
'groups' => ['hideformRequired'],
]));
$metadata->addPropertyConstraint('formType', new Assert\Choice([
'choices' => ['standalone', 'campaign'],
]));
$metadata->addPropertyConstraint('progressiveProfilingLimit', new Assert\GreaterThan([
'value' => 0,
'message' => 'mautic.form.form.progressive_profiling_limit.error',
'groups' => ['progressiveProfilingLimit'],
]));
}
public static function determineValidationGroups(\Symfony\Component\Form\Form $form): array
{
$data = $form->getData();
$groups = ['form'];
$postAction = $data->getPostAction();
if ('message' == $postAction) {
$groups[] = 'messageRequired';
} elseif ('redirect' == $postAction) {
$groups[] = 'urlRequired';
} elseif ('hideform' == $postAction) {
$groups[] = 'hideformRequired';
}
if ('' != $data->getProgressiveProfilingLimit()) {
$groups[] = 'progressiveProfilingLimit';
}
return $groups;
}
/**
* Prepares the metadata for API usage.
*/
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
{
$metadata->setGroupPrefix('form')
->addListProperties(
[
'id',
'name',
'alias',
'category',
]
)
->addProperties(
[
'description',
'cachedHtml',
'publishUp',
'publishDown',
'fields',
'actions',
'template',
'inKioskMode',
'renderStyle',
'formType',
'postAction',
'postActionProperty',
'noIndex',
'formAttributes',
'language',
]
)
->build();
self::addProjectsInLoadApiMetadata($metadata, 'form');
}
protected function isChanged($prop, $val)
{
if ('actions' == $prop || 'fields' == $prop) {
// changes are already computed so just add them
$this->changes[$prop][$val[0]] = $val[1];
} else {
parent::isChanged($prop, $val);
}
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @param string $name
*
* @return Form
*/
public function setName($name)
{
$this->isChanged('name', $name);
$this->name = $name;
return $this;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param string $description
*
* @return Form
*/
public function setDescription($description)
{
$this->isChanged('description', $description);
$this->description = $description;
return $this;
}
/**
* @return string
*/
public function getDescription($truncate = false, $length = 45)
{
if ($truncate) {
if (strlen($this->description) > $length) {
return substr($this->description, 0, $length).'...';
}
}
return $this->description;
}
/**
* @param string $cachedHtml
*
* @return Form
*/
public function setCachedHtml($cachedHtml)
{
$this->cachedHtml = $cachedHtml;
return $this;
}
/**
* @return string
*/
public function getCachedHtml()
{
return $this->cachedHtml;
}
/**
* @return bool|null
*/
public function getRenderStyle()
{
return $this->renderStyle;
}
/**
* @param string $postAction
*
* @return Form
*/
public function setPostAction($postAction)
{
$this->isChanged('postAction', $postAction);
$this->postAction = $postAction;
return $this;
}
/**
* @return string
*/
public function getPostAction()
{
return $this->postAction;
}
/**
* @param string $postActionProperty
*
* @return Form
*/
public function setPostActionProperty($postActionProperty)
{
$this->isChanged('postActionProperty', $postActionProperty);
$this->postActionProperty = $postActionProperty;
return $this;
}
public function getPostActionProperty(): ?string
{
if ('return' === $this->getPostAction()) {
return null;
}
return $this->postActionProperty;
}
public function getResultCount(): int
{
return count($this->submissions);
}
/**
* @param \DateTime $publishUp
*
* @return Form
*/
public function setPublishUp($publishUp)
{
$this->isChanged('publishUp', $publishUp);
$this->publishUp = $publishUp;
return $this;
}
/**
* @return \DateTimeInterface
*/
public function getPublishUp()
{
return $this->publishUp;
}
/**
* @param \DateTime $publishDown
*
* @return Form
*/
public function setPublishDown($publishDown)
{
$this->isChanged('publishDown', $publishDown);
$this->publishDown = $publishDown;
return $this;
}
/**
* @return \DateTimeInterface
*/
public function getPublishDown()
{
return $this->publishDown;
}
/**
* @param int|string $key
*
* @return Form
*/
public function addField($key, Field $field)
{
if ($changes = $field->getChanges()) {
$this->isChanged('fields', [$key, $changes]);
}
$this->fields[$key] = $field;
return $this;
}
/**
* @param int|string $key
*/
public function removeField($key, Field $field): void
{
if ($changes = $field->getChanges()) {
$this->isChanged('fields', [$key, $changes]);
}
$this->fields->removeElement($field);
}
/**
* @return ArrayCollection<int, Field>
*/
public function getFields()
{
return $this->fields;
}
public function getFieldAliases(): array
{
$aliases = [];
$fields = $this->getFields();
if ($fields) {
foreach ($fields as $field) {
$aliases[] = $field->getAlias();
}
}
return $aliases;
}
/**
* Loops through the form fields and returns an array of fields with mapped data.
*
* @return array<int, array<string, int|string>>
*/
public function getMappedFieldValues(): array
{
return array_filter(
array_map(
fn (Field $field): array => [
'formFieldId' => $field->getId(),
'mappedObject' => $field->getMappedObject(),
'mappedField' => $field->getMappedField(),
],
$this->getFields()->getValues()
),
fn ($elem) => isset($elem['mappedObject']) && isset($elem['mappedField'])
);
}
/**
* Loops trough the form fields and returns a simple array of mapped object keys if any.
*
* @return string[]
*/
public function getMappedFieldObjects(): array
{
return array_values(
array_filter(
array_unique(
$this->getFields()->map(
fn (Field $field) => $field->getMappedObject()
)->toArray()
)
)
);
}
/**
* @param string $alias
*
* @return Form
*/
public function setAlias($alias)
{
$this->isChanged('alias', $alias);
$this->alias = $alias;
return $this;
}
/**
* @return string
*/
public function getAlias()
{
return $this->alias;
}
/**
* @return Form
*/
public function addSubmission(Submission $submissions)
{
$this->submissions[] = $submissions;
return $this;
}
public function removeSubmission(Submission $submissions): void
{
$this->submissions->removeElement($submissions);
}
/**
* @return Collection|Submission[]
*/
public function getSubmissions()
{
return $this->submissions;
}
/**
* @param int|string $key
*
* @return Form
*/
public function addAction($key, Action $action)
{
if ($changes = $action->getChanges()) {
$this->isChanged('actions', [$key, $changes]);
}
$this->actions[$key] = $action;
return $this;
}
public function removeAction(Action $action): void
{
$this->actions->removeElement($action);
}
/**
* Removes all actions.
*/
public function clearActions(): void
{
$this->actions = new ArrayCollection();
}
/**
* @return ArrayCollection<string, Action>
*/
public function getActions()
{
return $this->actions;
}
/**
* @return mixed
*/
public function getCategory()
{
return $this->category;
}
/**
* @param mixed $category
*/
public function setCategory($category): void
{
$this->category = $category;
}
/**
* @return mixed
*/
public function getTemplate()
{
return $this->template;
}
/**
* @param mixed $template
*/
public function setTemplate($template): void
{
$this->template = $template;
}
/**
* @return mixed
*/
public function getInKioskMode()
{
return $this->inKioskMode;
}
/**
* @param mixed $inKioskMode
*/
public function setInKioskMode($inKioskMode): void
{
$this->inKioskMode = $inKioskMode;
}
/**
* @param mixed $renderStyle
*/
public function setRenderStyle($renderStyle): void
{
$this->renderStyle = $renderStyle;
}
/**
* @return mixed
*/
public function isInKioskMode()
{
return $this->getInKioskMode();
}
/**
* @return mixed
*/
public function getFormType()
{
return $this->formType;
}
/**
* @param mixed $formType
*
* @return Form
*/
public function setFormType($formType)
{
$this->formType = $formType;
return $this;
}
/**
* @param bool|null $noIndex
*/
public function setNoIndex($noIndex): void
{
$sanitizedNoIndex = null === $noIndex ? null : (bool) $noIndex;
$this->isChanged('noIndex', $sanitizedNoIndex);
$this->noIndex = $sanitizedNoIndex;
}
/**
* @return bool|null
*/
public function getNoIndex()
{
return $this->noIndex;
}
/**
* @param string $formAttributes
*
* @return Form
*/
public function setFormAttributes($formAttributes)
{
$this->isChanged('formAttributes', $formAttributes);
$this->formAttributes = $formAttributes;
return $this;
}
/**
* @return string
*/
public function getFormAttributes()
{
return $this->formAttributes;
}
public function setLanguage(?string $language): self
{
$this->isChanged('language', $language);
$this->language = $language;
return $this;
}
public function getLanguage(): ?string
{
return $this->language;
}
public function isStandalone(): bool
{
return 'campaign' != $this->formType;
}
/**
* Generate a form name for HTML attributes.
*/
public function generateFormName(): string
{
return $this->name ? strtolower(InputHelper::alphanum(InputHelper::transliterate($this->name))) : 'form-'.$this->id;
}
/**
* Check if some Progressive Profiling setting is turned on on any of the form fields.
*
* @return bool
*/
public function usesProgressiveProfiling()
{
if (null !== $this->usesProgressiveProfiling) {
return $this->usesProgressiveProfiling;
}
// Progressive profiling must be turned off in the kiosk mode
if (false === $this->getInKioskMode()) {
if ('' != $this->getProgressiveProfilingLimit()) {
$this->usesProgressiveProfiling = true;
return $this->usesProgressiveProfiling;
}
// Search for a field with a progressive profiling setting on
foreach ($this->fields->toArray() as $field) {
if (false === $field->getShowWhenValueExists() || $field->getShowAfterXSubmissions() > 0) {
$this->usesProgressiveProfiling = true;
return $this->usesProgressiveProfiling;
}
}
}
$this->usesProgressiveProfiling = false;
return $this->usesProgressiveProfiling;
}
/**
* @param int $progressiveProfilingLimit
*
* @return Form
*/
public function setProgressiveProfilingLimit($progressiveProfilingLimit)
{
$this->isChanged('progressiveProfilingLimit', $progressiveProfilingLimit);
$this->progressiveProfilingLimit = $progressiveProfilingLimit;
return $this;
}
/**
* @return int
*/
public function getProgressiveProfilingLimit()
{
return $this->progressiveProfilingLimit;
}
}

View File

@@ -0,0 +1,267 @@
<?php
namespace Mautic\FormBundle\Entity;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Mautic\CoreBundle\Entity\CommonRepository;
use Mautic\CoreBundle\Event\GlobalSearchEvent;
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
/**
* @extends CommonRepository<Form>
*/
class FormRepository extends CommonRepository
{
use ProjectRepositoryTrait;
public function getEntities(array $args = [])
{
$q = $this->createQueryBuilder('f');
$q->select('f');
// use a subquery to get a count of submissions otherwise doctrine will not pull all of the results
$sq = $this->_em->createQueryBuilder()
->select('count(fs.id)')
->from(Submission::class, 'fs')
->where('fs.form = f');
$q->addSelect('('.$sq->getDql().') as submission_count');
$q->leftJoin('f.category', 'c');
$args['qb'] = $q;
return parent::getEntities($args);
}
/**
* @param array<string, string|array<int, array<int|string, int|string|bool|null>>> $filter
*/
public function getEntitiesForGlobalSearch(array $filter): Paginator
{
$q = $this->createQueryBuilder('f');
$q->select('f');
$args = [
'qb' => $q,
'filter' => $filter,
'start' => 0,
'limit' => GlobalSearchEvent::RESULTS_LIMIT,
'ignore_paginator' => false,
];
return parent::getEntities($args);
}
/**
* @param string $search
* @param int $limit
* @param int $start
* @param bool $viewOther
*/
public function getFormList($search = '', $limit = 10, $start = 0, $viewOther = false, $formType = null): array
{
$q = $this->createQueryBuilder('f');
$q->select('partial f.{id, name, alias}');
if (!empty($search)) {
$q->andWhere($q->expr()->like('f.name', ':search'))
->setParameter('search', "{$search}%");
}
if (!$viewOther) {
$q->andWhere($q->expr()->eq('f.createdBy', ':id'))
->setParameter('id', $this->currentUser->getId());
}
if (!empty($formType)) {
$q->andWhere(
$q->expr()->eq('f.formType', ':type')
)->setParameter('type', $formType);
}
$q->orderBy('f.name');
if (!empty($limit)) {
$q->setFirstResult($start)
->setMaxResults($limit);
}
return $q->getQuery()->getArrayResult();
}
protected function addCatchAllWhereClause($q, $filter): array
{
return $this->addStandardCatchAllWhereClause($q, $filter, [
'f.name',
'f.description',
]);
}
protected function addSearchCommandWhereClause($q, $filter): array
{
[$expr, $standardSearchParameters] = $this->addStandardSearchCommandWhereClause($q, $filter);
if ($expr) {
return [$expr, $standardSearchParameters];
}
$command = $filter->command;
$unique = $this->generateRandomParameterName();
$parameters = [];
$returnParameter = false; // returning a parameter that is not used will lead to a Doctrine error
switch ($command) {
case $this->translator->trans('mautic.form.form.searchcommand.isexpired'):
case $this->translator->trans('mautic.form.form.searchcommand.isexpired', [], null, 'en_US'):
$expr = $q->expr()->and(
$q->expr()->eq('f.isPublished', ":$unique"),
$q->expr()->isNotNull('f.publishDown'),
$q->expr()->neq('f.publishDown', $q->expr()->literal('')),
$q->expr()->lt('f.publishDown', 'CURRENT_TIMESTAMP()')
);
$forceParameters = [$unique => true];
break;
case $this->translator->trans('mautic.form.form.searchcommand.ispending'):
case $this->translator->trans('mautic.form.form.searchcommand.ispending', [], null, 'en_US'):
$expr = $q->expr()->and(
$q->expr()->eq('f.isPublished', ":$unique"),
$q->expr()->isNotNull('f.publishUp'),
$q->expr()->neq('f.publishUp', $q->expr()->literal('')),
$q->expr()->gt('f.publishUp', 'CURRENT_TIMESTAMP()')
);
$forceParameters = [$unique => true];
break;
case $this->translator->trans('mautic.form.form.searchcommand.hasresults'):
case $this->translator->trans('mautic.form.form.searchcommand.hasresults', [], null, 'en_US'):
$sq = $this->getEntityManager()->createQueryBuilder();
$subquery = $sq->select('count(s.id)')
->from(Submission::class, 's')
->leftJoin(Form::class, 'f2',
Join::WITH,
$sq->expr()->eq('s.form', 'f2')
)
->where(
$q->expr()->eq('s.form', 'f')
)
->getDql();
$expr = $q->expr()->gt(sprintf('(%s)', $subquery), 1);
break;
case $this->translator->trans('mautic.core.searchcommand.name'):
case $this->translator->trans('mautic.core.searchcommand.name', [], null, 'en_US'):
$expr = $q->expr()->like('f.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(),
'form_id',
'form_projects_xref',
$this->getTableAlias(),
$filter->string,
$filter->not
);
}
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,
];
}
/**
* Fetch the form results.
*
* @throws \Doctrine\ORM\NoResultException
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function getFormResults(Form $form, array $options = []): array
{
$query = $this->_em->getConnection()->createQueryBuilder();
$query->from(MAUTIC_TABLE_PREFIX.'form_submissions', 'fs')
->select('fr.*')
->leftJoin('fs', $this->getResultsTableName($form->getId(), $form->getAlias()), 'fr', 'fr.submission_id = fs.id')
->where('fs.form_id = :formId')
->setParameter('formId', $form->getId());
if (!empty($options['leadId'])) {
$query->andWhere('fs.lead_id = :leadId')
->setParameter('leadId', $options['leadId']);
}
if (!empty($options['formId'])) {
$query->andWhere($query->expr()->eq('fs.form_id', ':id'))
->setParameter('id', $options['formId']);
}
if (!empty($options['limit'])) {
$query->setMaxResults((int) $options['limit']);
}
return $query->executeQuery()->fetchAllAssociative();
}
/**
* Compile and return the form result table name.
*
* @param int $formId
* @param string $formAlias
*/
public function getResultsTableName($formId, $formAlias): string
{
return MAUTIC_TABLE_PREFIX.'form_results_'.$formId.'_'.$formAlias;
}
public function getFormTableIdViaResults(string $resultsTableName): ?string
{
$regexp = '/.*'.MAUTIC_TABLE_PREFIX.'form_results_([0-9]+)_(.*)/i';
preg_match($regexp, $resultsTableName, $matches);
return $matches[1] ?? null;
}
/**
* @return string[]
*/
public function getSearchCommands(): array
{
$commands = [
'mautic.core.searchcommand.ispublished',
'mautic.core.searchcommand.isunpublished',
'mautic.core.searchcommand.isuncategorized',
'mautic.core.searchcommand.ismine',
'mautic.form.form.searchcommand.isexpired',
'mautic.form.form.searchcommand.ispending',
'mautic.form.form.searchcommand.hasresults',
'mautic.core.searchcommand.category',
'mautic.core.searchcommand.name',
'mautic.project.searchcommand.name',
];
return array_merge($commands, parent::getSearchCommands());
}
protected function getDefaultOrder(): array
{
return [
['f.name', 'ASC'],
];
}
public function getTableAlias(): string
{
return 'f';
}
}

View File

@@ -0,0 +1,337 @@
<?php
namespace Mautic\FormBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\IpAddress;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\PageBundle\Entity\Page;
class Submission
{
public const TABLE_NAME = 'form_submissions';
/**
* @var string
*/
private $id;
/**
* @var Form
**/
private $form;
/**
* @var IpAddress|null
*/
private $ipAddress;
/**
* @var Lead|null
*/
private $lead;
/**
* @var string|null
*/
private $trackingId;
/**
* @var \DateTimeInterface
*/
private $dateSubmitted;
/**
* @var string
*/
private $referer;
/**
* @var Page|null
*/
private $page;
/**
* @var array
*/
private $results = [];
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable(self::TABLE_NAME)
->setCustomRepositoryClass(SubmissionRepository::class)
->addIndex(['tracking_id'], 'form_submission_tracking_search')
->addIndex(['date_submitted'], 'form_date_submitted');
$builder->addBigIntIdField();
$builder->createManyToOne('form', 'Form')
->inversedBy('submissions')
->addJoinColumn('form_id', 'id', false, false, 'CASCADE')
->build();
$builder->addIpAddress(true);
$builder->addLead(true, 'SET NULL');
$builder->createField('trackingId', 'string')
->columnName('tracking_id')
->nullable()
->build();
$builder->createField('dateSubmitted', 'datetime')
->columnName('date_submitted')
->build();
$builder->addField('referer', 'text');
$builder->createManyToOne('page', Page::class)
->addJoinColumn('page_id', 'id', true, false, 'SET NULL')
->fetchExtraLazy()
->build();
}
/**
* Prepares the metadata for API usage.
*/
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
{
$metadata->setGroupPrefix('submission')
->addProperties(
[
'id',
'ipAddress',
'form',
'lead',
'trackingId',
'dateSubmitted',
'referer',
'page',
'results',
]
)
->setGroupPrefix('submissionEvent')
->addProperties(
[
'id',
'ipAddress',
'form',
'trackingId',
'dateSubmitted',
'referer',
'page',
'results',
]
)
->build();
}
/**
* Get id.
*/
public function getId(): int
{
return (int) $this->id;
}
/**
* Set dateSubmitted.
*
* @param \DateTime $dateSubmitted
*
* @return Submission
*/
public function setDateSubmitted($dateSubmitted)
{
$this->dateSubmitted = $dateSubmitted;
return $this;
}
/**
* Get dateSubmitted.
*
* @return \DateTimeInterface
*/
public function getDateSubmitted()
{
return $this->dateSubmitted;
}
/**
* Set referer.
*
* @param string $referer
*
* @return Submission
*/
public function setReferer($referer)
{
$this->referer = $referer;
return $this;
}
/**
* Get referer.
*
* @return string
*/
public function getReferer()
{
return $this->referer;
}
/**
* Set form.
*
* @return Submission
*/
public function setForm(Form $form)
{
$this->form = $form;
return $this;
}
/**
* Get form.
*
* @return Form
*/
public function getForm()
{
return $this->form;
}
/**
* Set ipAddress.
*
* @return Submission
*/
public function setIpAddress(?IpAddress $ipAddress = null)
{
$this->ipAddress = $ipAddress;
return $this;
}
/**
* @return IpAddress
*/
public function getIpAddress()
{
return $this->ipAddress;
}
/**
* Get results.
*
* @return array
*/
public function getResults()
{
return $this->results;
}
/**
* Get results.
*
* @return Submission
*/
public function setResults($results)
{
$this->results = $results;
return $this;
}
/**
* Set page.
*
* @return Submission
*/
public function setPage(?Page $page = null)
{
$this->page = $page;
return $this;
}
/**
* Get page.
*
* @return Page
*/
public function getPage()
{
return $this->page;
}
/**
* @return Lead
*/
public function getLead()
{
return $this->lead;
}
/**
* @return $this
*/
public function setLead(?Lead $lead = null)
{
$this->lead = $lead;
return $this;
}
/**
* @return mixed
*/
public function getTrackingId()
{
return $this->trackingId;
}
/**
* @return $this
*/
public function setTrackingId($trackingId)
{
$this->trackingId = $trackingId;
return $this;
}
/**
* This method is used by standard entity algorithms to check if the current
* user has permission to view/edit/delete this item. Provide the form creator for it.
*
* @return mixed
*/
public function getCreatedBy()
{
return $this->getForm()->getCreatedBy();
}
/**
* @param string $alias
*
* @return Field|null
*/
public function getFieldByAlias($alias)
{
foreach ($this->getForm()->getFields() as $field) {
if ($field->getAlias() === $alias) {
return $field;
}
}
return null;
}
}

View File

@@ -0,0 +1,534 @@
<?php
namespace Mautic\FormBundle\Entity;
use Doctrine\Common\Collections\Order;
use Doctrine\DBAL\Query\QueryBuilder as DbalQueryBuilder;
use Doctrine\ORM\QueryBuilder;
use Mautic\CoreBundle\Entity\CommonRepository;
use Mautic\CoreBundle\Helper\DateTimeHelper;
use Mautic\LeadBundle\Entity\TimelineTrait;
/**
* @extends CommonRepository<Submission>
*/
class SubmissionRepository extends CommonRepository
{
use TimelineTrait;
public function saveEntity($entity, $flush = true): void
{
parent::saveEntity($entity, $flush);
// add the results
$results = $entity->getResults();
$results['submission_id'] = $entity->getId();
$form = $entity->getForm();
$results['form_id'] = $form->getId();
if (!empty($results)) {
// Check that alias is SQL safe since it will be used for the column name
$databasePlatform = $this->_em->getConnection()->getDatabasePlatform();
$reservedWords = $databasePlatform->getReservedKeywordsList();
foreach ($results as $alias => $value) {
if ($reservedWords->isKeyword($alias)) {
$results[$databasePlatform->quoteIdentifier($alias)] = $value;
unset($results[$alias]);
}
}
$this->_em->getConnection()->insert($this->getResultsTableName($form->getId(), $form->getAlias()), $results);
}
}
public function getEntities(array $args = [])
{
$form = $args['form'];
// DBAL
if (!isset($args['viewOnlyFields'])) {
$args['viewOnlyFields'] = ['button', 'freetext', 'freehtml', 'pagebreak', 'captcha'];
}
$viewOnlyFields = array_map(
fn ($value): string => '"'.$value.'"',
$args['viewOnlyFields']
);
// Get the list of custom fields
$fq = $this->_em->getConnection()->createQueryBuilder();
$fq->select('f.id, f.label, f.alias, f.type')
->from(MAUTIC_TABLE_PREFIX.'form_fields', 'f')
->where('f.form_id = '.$form->getId())
->andWhere(
$fq->expr()->notIn('f.type', $viewOnlyFields),
$fq->expr()->eq('f.save_result', ':saveResult')
)
->orderBy('f.field_order, f.id', 'ASC')
->setParameter('saveResult', true);
$results = $fq->executeQuery()->fetchAllAssociative();
$fields = [];
foreach ($results as $r) {
$fields[$r['alias']] = $r;
}
unset($results);
$fieldAliases = array_keys($fields);
$dq = $this->_em->getConnection()->createQueryBuilder();
$dq->select('count(r.submission_id) as count')
->from($this->getResultsTableName($form->getId(), $form->getAlias()), 'r')
->innerJoin('r', MAUTIC_TABLE_PREFIX.'form_submissions', 's', 'r.submission_id = s.id')
->leftJoin('s', MAUTIC_TABLE_PREFIX.'ip_addresses', 'i', 's.ip_id = i.id')
->where('r.form_id = '.$form->getId());
$this->buildWhereClause($dq, $args);
// get a total count
$result = $dq->executeQuery()->fetchAllAssociative();
$total = $result[0]['count'];
// now get the actual paginated results
$this->buildOrderByClause($dq, $args);
$this->buildLimiterClauses($dq, $args);
$dq->resetQueryPart('select');
$databasePlatform = $this->_em->getConnection()->getDatabasePlatform();
// Quote reserved keywords in field aliases
$fieldAliases = array_map(fn ($alias) => $databasePlatform->quoteIdentifier($alias), $fieldAliases);
$fieldAliasSql = (!empty($fieldAliases)) ? ', r.'.implode(',r.', $fieldAliases) : '';
$dq->select('r.submission_id, s.date_submitted as dateSubmitted, s.lead_id as leadId, s.referer, i.ip_address as ipAddress'.$fieldAliasSql);
$results = $dq->executeQuery()->fetchAllAssociative();
// loop over results to put form submission results in something that can be assigned to the entities
$values = [];
$flattenResults = !empty($args['flatten_results']);
foreach ($results as &$result) {
$submissionId = $result['submission_id'];
unset($result['submission_id']);
$values[$submissionId] = [];
foreach ($result as $k => $r) {
if (isset($fields[$k])) {
if ($flattenResults) {
$values[$submissionId][$k] = $r;
} else {
$values[$submissionId][$k] = $fields[$k];
$values[$submissionId][$k]['value'] = $r;
}
}
}
$result['id'] = $submissionId;
$result['results'] = $values[$submissionId];
}
if (empty($args['simpleResults'])) {
// get an array of IDs for ORM query
$ids = array_keys($values);
if (count($ids)) {
// ORM
// build the order by id since the order was applied above
// unfortunately, can't use MySQL's FIELD function since we have to be cross-platform
$order = '(CASE';
foreach ($ids as $count => $id) {
$order .= ' WHEN s.id = '.$id.' THEN '.$count;
++$count;
}
$order .= ' ELSE '.$count.' END) AS HIDDEN ORD';
// ORM - generates lead entities
$returnEntities = !empty($args['return_entities']);
$leadSelect = $returnEntities ? 'l' : 'partial l.{id}';
$q = $this
->createQueryBuilder('s');
$q->select('s, p, i,'.$leadSelect.','.$order)
->leftJoin('s.ipAddress', 'i')
->leftJoin('s.page', 'p')
->leftJoin('s.lead', 'l');
// only pull the submissions as filtered via DBAL
$q->where(
$q->expr()->in('s.id', ':ids')
)->setParameter('ids', $ids);
$q->orderBy('ORD', Order::Ascending->value);
$results = $returnEntities ? $q->getQuery()->getResult() : $q->getQuery()->getArrayResult();
foreach ($results as &$r) {
if ($r instanceof Submission) {
$r->setResults($values[$r->getId()]);
} else {
$r['results'] = $values[$r['id']];
}
}
}
}
return (!empty($args['withTotalCount'])) ?
[
'count' => $total,
'results' => $results,
] : $results;
}
/**
* @param int $id
*/
public function getEntity($id = 0): ?Submission
{
$entity = parent::getEntity($id);
if (null != $entity) {
$form = $entity->getForm();
// use DBAL to get entity fields
$q = $this->_em->getConnection()->createQueryBuilder();
$q->select('*')
->from($this->getResultsTableName($form->getId(), $form->getAlias()), 'r')
->where('r.submission_id = :id')
->setParameter('id', $id);
$results = $q->executeQuery()->fetchAllAssociative();
if (!empty($results)) {
unset($results[0]['submission_id']);
$entity->setResults($results[0]);
}
}
return $entity;
}
/**
* Get all submissions that derive from a landing page.
*
* @param array<mixed> $args
*
* @return array<mixed>
*/
public function getEntitiesByPage(array $args = []): array
{
$activePage = $args['activePage'];
$dq = $this->_em->getConnection()->createQueryBuilder();
$dq->select('count(s.id) as count')
->from(MAUTIC_TABLE_PREFIX.'form_submissions', 's')
->innerJoin('s', MAUTIC_TABLE_PREFIX.'pages', 'p', 's.page_id = p.id')
->leftJoin('s', MAUTIC_TABLE_PREFIX.'ip_addresses', 'i', 's.ip_id = i.id')
->where($dq->expr()->eq('s.page_id', ':page'))
->setParameter('page', $activePage->getId());
$this->buildWhereClause($dq, $args);
// get a total count
$result = $dq->executeQuery()->fetchAllAssociative();
$total = $result[0]['count'];
// now get the actual paginated results
$this->buildOrderByClause($dq, $args);
$this->buildLimiterClauses($dq, $args);
$dq->resetQueryPart('select');
$dq->select('s.id, s.date_submitted as dateSubmitted, s.lead_id as leadId, s.form_id as formId, s.referer, i.ip_address as ipAddress');
$results = $dq->executeQuery()->fetchAllAssociative();
return [
'count' => $total,
'results' => $results,
];
}
/**
* @param QueryBuilder|DbalQueryBuilder $q
* @param array<mixed> $filter
*/
public function getFilterExpr($q, array $filter, ?string $unique = null): array
{
if ('s.date_submitted' === $filter['column']) {
$date = (new DateTimeHelper($filter['value'], 'Y-m-d'))->toUtcString();
$date1 = $this->generateRandomParameterName();
$date2 = $this->generateRandomParameterName();
$parameters = [$date1 => $date.' 00:00:00', $date2 => $date.' 23:59:59'];
$expr = $q->expr()->and(
$q->expr()->gte('s.date_submitted', ":$date1"),
$q->expr()->lte('s.date_submitted', ":$date2")
);
return [$expr, $parameters];
}
return parent::getFilterExpr($q, $filter);
}
protected function getDefaultOrder(): array
{
return [
['s.date_submitted', 'ASC'],
];
}
/**
* Fetch the base submission data from the database.
*
* @return array
*
* @throws \Doctrine\ORM\NoResultException
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function getSubmissions(array $options = [])
{
$query = $this->getEntityManager()->getConnection()->createQueryBuilder();
$query->select('fs.id, f.name, fs.form_id, fs.page_id, fs.date_submitted AS "dateSubmitted", fs.lead_id')
->from(MAUTIC_TABLE_PREFIX.'form_submissions', 'fs')
->leftJoin('fs', MAUTIC_TABLE_PREFIX.'forms', 'f', 'f.id = fs.form_id');
if (!empty($options['leadId'])) {
$query->andWhere('fs.lead_id = :leadId')
->setParameter('leadId', $options['leadId']);
}
if (!empty($options['id'])) {
$query->andWhere($query->expr()->eq('fs.form_id', ':id'))
->setParameter('id', $options['id']);
}
if (isset($options['search']) && $options['search']) {
$query->andWhere(
$query->expr()->like('f.name', ':search')
)->setParameter('search', '%'.$options['search'].'%');
}
return $this->getTimelineResults($query, $options, 'f.name', 'fs.date_submitted', [], ['dateSubmitted'], null, 'fs.id');
}
/**
* Get list of forms ordered by it's count.
*
* @param DbalQueryBuilder $query
* @param int $limit
* @param int $offset
*
* @throws \Doctrine\ORM\NoResultException
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function getTopReferrers($query, $limit = 10, $offset = 0): array
{
$query->select('fs.referer, count(fs.referer) as sessions')
->groupBy('fs.referer')
->orderBy('sessions', 'DESC')
->setMaxResults($limit)
->setFirstResult($offset);
return $query->executeQuery()->fetchAllAssociative();
}
/**
* Get list of forms ordered by it's count.
*
* @param DbalQueryBuilder $query
* @param int $limit
* @param int $offset
*
* @throws \Doctrine\ORM\NoResultException
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function getMostSubmitted($query, $limit = 10, $offset = 0, $column = 'fs.id', $as = 'submissions'): array
{
$asSelect = ($as) ? ' as '.$as : '';
$query->select('f.name as title, f.id, count(distinct '.$column.')'.$asSelect)
->groupBy('f.id, f.name')
->orderBy($as, 'DESC')
->setMaxResults($limit)
->setFirstResult($offset);
return $query->executeQuery()->fetchAllAssociative();
}
/**
* @return mixed[]
*/
public function getSubmissionCountsByPage($pageId, ?\DateTime $fromDate = null): array
{
$q = $this->_em->getConnection()->createQueryBuilder();
$q->select('count(distinct(s.tracking_id)) as count, s.page_id as id, p.title as name, p.variant_hits as total')
->from(MAUTIC_TABLE_PREFIX.'form_submissions', 's')
->join('s', MAUTIC_TABLE_PREFIX.'pages', 'p', 's.page_id = p.id');
if (is_array($pageId)) {
$q->where($q->expr()->in('s.page_id', $pageId))
->groupBy('s.page_id, p.title, p.variant_hits');
} else {
$q->where($q->expr()->eq('s.page_id', ':page'))
->setParameter('page', (int) $pageId);
}
if (null != $fromDate) {
$dh = new DateTimeHelper($fromDate);
$q->andWhere($q->expr()->gte('s.date_submitted', ':date'))
->setParameter('date', $dh->toUtcString());
}
return $q->executeQuery()->fetchAllAssociative();
}
/**
* Get submission count by email by linking emails that have been associated with a page hit that has the
* same tracking ID as a form submission tracking ID and thus assumed happened in the same session.
*
* @return mixed[]
*/
public function getSubmissionCountsByEmail($emailId, ?\DateTime $fromDate = null): array
{
// link email to page hit tracking id to form submission tracking id
$q = $this->_em->getConnection()->createQueryBuilder();
$q->select('count(distinct(s.tracking_id)) as count, e.id, e.subject as name, e.variant_sent_count as total')
->from(MAUTIC_TABLE_PREFIX.'form_submissions', 's')
->join('s', MAUTIC_TABLE_PREFIX.'page_hits', 'h', 's.tracking_id = h.tracking_id')
->join('h', MAUTIC_TABLE_PREFIX.'emails', 'e', 'h.email_id = e.id');
if (is_array($emailId)) {
$q->where($q->expr()->in('e.id', $emailId))
->groupBy('e.id, e.subject, e.variant_sent_count');
} else {
$q->where($q->expr()->eq('e.id', ':id'))
->setParameter('id', (int) $emailId);
}
if (null != $fromDate) {
$dh = new DateTimeHelper($fromDate);
$q->andWhere($q->expr()->gte('s.date_submitted', ':date'))
->setParameter('date', $dh->toUtcString());
}
return $q->executeQuery()->fetchAllAssociative();
}
/**
* Updates lead ID (e.g. after a lead merge).
*/
public function updateLead($fromLeadId, $toLeadId): void
{
$q = $this->_em->getConnection()->createQueryBuilder();
$q->update(MAUTIC_TABLE_PREFIX.'form_submissions')
->set('lead_id', (int) $toLeadId)
->where('lead_id = '.(int) $fromLeadId)
->executeStatement();
}
/**
* Validates that an array of submission IDs belong to a specific form.
*/
public function validateSubmissions($ids, $formId): array
{
$q = $this->_em->getConnection()->createQueryBuilder();
$q->select('s.id')
->from(MAUTIC_TABLE_PREFIX.'form_submissions', 's')
->where(
$q->expr()->and(
$q->expr()->eq('s.form_id', (int) $formId),
$q->expr()->in('s.id', $ids)
)
);
$validIds = [];
$results = $q->executeQuery()->fetchAllAssociative();
foreach ($results as $r) {
$validIds[] = $r['id'];
}
return $validIds;
}
/**
* Compare a form result value with defined value for defined lead.
*
* @param int $lead ID
* @param int $form ID
* @param string $formAlias
* @param int $field alias
* @param string $value to compare with
* @param string $operatorExpr for WHERE clause
* @param string|null $type
*/
public function compareValue($lead, $form, $formAlias, $field, $value, $operatorExpr, $type = null): bool
{
// Modify operator
switch ($operatorExpr) {
case 'like':
case 'notLike':
$value = !str_contains($value, '%') ? '%'.$value.'%' : $value;
break;
case 'startsWith':
$operatorExpr = 'like';
$value = $value.'%';
break;
case 'endsWith':
$operatorExpr = 'like';
$value = '%'.$value;
break;
case 'contains':
$operatorExpr = 'like';
$value = '%'.$value.'%';
break;
}
// use DBAL to get entity fields
$q = $this->_em->getConnection()->createQueryBuilder();
$q->select('s.id')
->from($this->getResultsTableName($form, $formAlias), 'r')
->leftJoin('r', MAUTIC_TABLE_PREFIX.'form_submissions', 's', 's.id = r.submission_id')
->where(
$q->expr()->and(
$q->expr()->eq('s.lead_id', ':lead'),
$q->expr()->eq('s.form_id', ':form')
)
)
->setParameter('lead', (int) $lead)
->setParameter('form', (int) $form);
match ($type) {
'boolean', 'number' => $q->andWhere($q->expr()->$operatorExpr('r.'.$field, $value)),
default => $q->andWhere($q->expr()->$operatorExpr('r.'.$field, ':value'))
->setParameter('value', $value),
};
$result = $q->executeQuery()->fetchAssociative();
return !empty($result['id']);
}
/**
* @param Form $form
*/
public function getSubmissionCounts($form)
{
$query = $this->getEntityManager()->getConnection()->createQueryBuilder();
$query->select('COUNT(fs.id) AS `total`, COUNT(DISTINCT (fs.lead_id)) AS `unique`')
->from(MAUTIC_TABLE_PREFIX.'form_submissions', 'fs');
$query->where($query->expr()->eq('fs.form_id', ':id'))
->setParameter('id', $form->getId());
return $query->executeQuery()->fetchAssociative();
}
/**
* Compile and return the form result table name.
*
* @param int $formId
* @param string $formAlias
*/
public function getResultsTableName($formId, $formAlias): string
{
return MAUTIC_TABLE_PREFIX.'form_results_'.$formId.'_'.$formAlias;
}
public function getTableAlias(): string
{
return 'fs';
}
}