Spaces:
No application file
No application file
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'; | |
} | |
} | |