mautic / app /bundles /FormBundle /Entity /SubmissionRepository.php
chrisbryan17's picture
Upload folder using huggingface_hub
d2897cd verified
raw
history blame contribute delete
19.4 kB
<?php
namespace Mautic\FormBundle\Entity;
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', \Doctrine\Common\Collections\Criteria::ASC);
$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 = '.(int) $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', $query->expr()->literal('%'.$options['search'].'%'))
);
}
return $this->getTimelineResults($query, $options, 'f.name', 'fs.date_submitted', [], ['dateSubmitted']);
}
/**
* 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';
}
}