mautic / app /bundles /CoreBundle /Entity /CommonRepository.php
chrisbryan17's picture
Upload folder using huggingface_hub
d2897cd verified
raw
history blame contribute delete
63.7 kB
<?php
namespace Mautic\CoreBundle\Entity;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\ExpressionBuilder;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Query\Expression\CompositeExpression;
use Doctrine\DBAL\Query\QueryBuilder as DbalQueryBuilder;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Doctrine\Persistence\ManagerRegistry;
use Mautic\CoreBundle\Cache\ResultCacheHelper;
use Mautic\CoreBundle\Cache\ResultCacheOptions;
use Mautic\CoreBundle\Doctrine\Paginator\SimplePaginator;
use Mautic\CoreBundle\Helper\DateTimeHelper;
use Mautic\CoreBundle\Helper\InputHelper;
use Mautic\CoreBundle\Helper\SearchStringHelper;
use Mautic\UserBundle\Entity\User;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @template T of object
*
* @extends ServiceEntityRepository<T>
*/
class CommonRepository extends ServiceEntityRepository
{
/**
* @phpstan-param class-string<T>|null $entityFQCN
*/
public function __construct(ManagerRegistry $registry, string $entityFQCN = null)
{
parent::__construct($registry, $entityFQCN ?? str_replace('Repository', '', static::class));
}
/**
* Stores the parsed columns and their negate status for addAdvancedSearchWhereClause().
*
* @var array
*/
protected $advancedFilterCommands = [];
/**
* @var User|null
*/
protected $currentUser;
/**
* @var TranslatorInterface
*/
protected $translator;
/**
* This eliminates chance for parameter name collision.
*
* @see CommonRepository::generateRandomParameterName()
*
* @var int
*/
protected $lastUsedParameterId = 0;
/**
* @var ExpressionBuilder|null
*/
private $expressionBuilder;
/**
* @param string $alias
* @param object $entity
*
* @return mixed
*/
public function checkUniqueAlias($alias, $entity = null)
{
$q = $this->createQueryBuilder('e')
->select('count(e.id) as aliascount')
->where('e.alias = :alias');
$q->setParameter('alias', $alias);
if (!empty($entity) && $entity->getId()) {
$q->andWhere('e.id != :id');
$q->setParameter('id', $entity->getId());
}
$results = $q->getQuery()->getSingleResult();
return $results['aliascount'];
}
/**
* Examines the arguments passed to getEntities and converts ORM properties to dBAL column names.
*
* @param string $entityClass
*/
public function convertOrmProperties($entityClass, array $args): array
{
$properties = $this->getBaseColumns($entityClass);
// check force filters
if (isset($args['filter']['force']) && is_array($args['filter']['force'])) {
$this->convertOrmPropertiesToColumns($args['filter']['force'], $properties);
}
if (isset($args['filter']['where']) && is_array($args['filter']['where'])) {
$this->convertOrmPropertiesToColumns($args['filter']['where'], $properties);
}
// check order by
if (isset($args['order'])) {
if (is_array($args['order'])) {
foreach ($args['order'] as &$o) {
$alias = '';
if (str_contains($o, '.')) {
[$alias, $o] = explode('.', $o);
}
if (in_array($o, $properties)) {
$o = preg_replace('/(?<=\\w)(?=[A-Z])/', '_$1', $o);
$o = strtolower($o);
}
$o = (!empty($alias)) ? $alias.'.'.$o : $o;
}
}
}
return $args;
}
/**
* @param class-string $className
*
* @return mixed
*
* @throws \Doctrine\ORM\Mapping\MappingException
* @throws \Exception
*/
public function createFromArray($className, &$data)
{
$entity = new $className();
$meta = $this->_em->getClassMetadata($className);
$ormProperties = $this->getBaseColumns($className, true);
foreach ($ormProperties as $property => $dbCol) {
if (isset($data[$dbCol])) {
$v = $data[$dbCol];
if ($v && $meta->hasAssociation($property)) {
$map = $meta->getAssociationMapping($property);
$v = $this->_em->getRepository($map['targetEntity'])->find($v);
if (empty($v)) {
throw new \Exception('Associate data not found');
}
}
$method = 'set'.ucfirst($property);
if (method_exists($entity, $method)) {
$entity->$method($v);
}
unset($data[$dbCol]);
}
}
return $entity;
}
/**
* Delete an array of entities.
*
* @param array $entities
*/
public function deleteEntities($entities): void
{
// iterate over the results so the events are dispatched on each delete
$batchSize = 20;
$i = 0;
foreach ($entities as $entity) {
$this->deleteEntity($entity, false);
if (0 === ++$i % $batchSize) {
$this->_em->flush();
}
}
$this->_em->flush();
}
/**
* Delete an entity through the repository.
*
* @param object $entity
* @param bool $flush true by default; use false if persisting in batches
*/
public function deleteEntity($entity, $flush = true): void
{
// delete entity
$this->_em->remove($entity);
if ($flush) {
$this->_em->flush();
}
}
public function detachEntities(array $entities): void
{
foreach ($entities as $entity) {
$this->getEntityManager()->detach($entity);
}
}
/**
* @param mixed $entity
*/
public function detachEntity($entity): void
{
$this->getEntityManager()->detach($entity);
}
public function refetchEntity(object &$entity): void
{
if ($this->getEntityManager()->contains($entity)) {
$this->getEntityManager()->detach($entity);
$metadata = $this->getEntityManager()->getClassMetadata(ClassUtils::getClass($entity));
$identifierValues = $metadata->getIdentifierValues($entity);
if (count($identifierValues) > 1) {
throw new \RuntimeException('Multiple identifiers are not supported.');
}
$entity = $this->getEntity(array_pop($identifierValues));
}
}
/**
* @return mixed|null
*/
public function findOneBySlugs($alias, $catAlias = null, $lang = null)
{
try {
$q = $this->createQueryBuilder($this->getTableAlias())
->setParameter('alias', $alias);
$expr = $q->expr()->andX(
$q->expr()->eq($this->getTableAlias().'.alias', ':alias')
);
$metadata = $this->getClassMetadata();
if (null !== $catAlias) {
if (isset($metadata->associationMappings['category'])) {
$q->leftJoin($this->getTableAlias().'.category', 'category')
->setParameter('catAlias', $catAlias);
$expr->add(
$q->expr()->eq('category.alias', ':catAlias')
);
} else {
// This entity does not have a category mapping so return null
return null;
}
}
if (isset($metadata->fieldMappings['language'])) {
if ($lang) {
// Find the landing page with the specific requested locale
$q->setParameter('lang', $lang);
$expr->add(
$q->expr()->eq($this->getTableAlias().'.language', ':lang')
);
} elseif (isset($metadata->associationMappings['translationParent'])) {
// Find the parent translation
$expr->add(
$q->expr()->isNull($this->getTableAlias().'.translationParent')
);
}
}
// Check for variants and return parent only
if (isset($metadata->associationMappings['variantParent'])) {
$expr->add(
$q->expr()->isNull($this->getTableAlias().'.variantParent')
);
}
$q->where($expr);
$entity = $q->getQuery()->getSingleResult();
} catch (\Exception) {
$entity = null;
}
return $entity;
}
/**
* Gets the properties of an ORM entity.
*
* @param string $entityClass
* @param bool $returnColumnNames
*
* @return array
*/
public function getBaseColumns($entityClass, $returnColumnNames = false)
{
static $baseCols = [true => [], false => []];
if ($this->getEntityName() === $entityClass) {
if (empty($baseCols[$returnColumnNames][$entityClass])) {
// Use metadata
$metadata = $this->getClassMetadata();
$baseCols[true][$entityClass] = $metadata->getColumnNames();
$baseCols[false][$entityClass] = $metadata->getFieldNames();
foreach ($metadata->getAssociationMappings() as $field => $association) {
if (in_array($association['type'], [ClassMetadataInfo::ONE_TO_ONE, ClassMetadataInfo::MANY_TO_ONE])) {
$baseCols[true][$entityClass][] = $association['joinColumns'][0]['name'];
$baseCols[false][$entityClass][] = $field;
}
}
}
return $baseCols[$returnColumnNames][$entityClass];
}
return $this->getEntityManager()->getRepository($entityClass)->getBaseColumns($entityClass, $returnColumnNames);
}
/**
* Get a list of entities.
*
* @param array<string,mixed> $args
*
* @return object[]|array<int,mixed>|iterable<object>|\Doctrine\ORM\Internal\Hydration\IterableResult<object>|Paginator<object>|SimplePaginator<mixed>
*/
public function getEntities(array $args = [])
{
$alias = $this->getTableAlias();
if (isset($args['qb'])) {
$q = $args['qb'];
} else {
$q = $this->_em
->createQueryBuilder()
->select($alias)
->from($this->getEntityName(), $alias, "{$alias}.id");
if ($this->getClassMetadata()->hasAssociation('category')) {
$q->leftJoin($this->getTableAlias().'.category', 'cat');
}
}
$this->buildClauses($q, $args);
$query = $q->getQuery();
if (isset($args['result_cache'])) {
if (!$args['result_cache'] instanceof ResultCacheOptions) {
throw new \InvalidArgumentException(sprintf('The value of the key "result_cache" must be an instance of "%s"', ResultCacheOptions::class));
}
ResultCacheHelper::enableOrmQueryCache($query, $args['result_cache']);
}
if (isset($args['hydration_mode'])) {
$hydrationMode = constant('\\Doctrine\\ORM\\Query::'.strtoupper($args['hydration_mode']));
$query->setHydrationMode($hydrationMode);
} else {
$hydrationMode = Query::HYDRATE_OBJECT;
}
if (array_key_exists('iterable_mode', $args) && true === $args['iterable_mode']) {
// Hydrate one by one
return $query->toIterable([], $hydrationMode);
}
if (!empty($args['iterator_mode'])) {
// When you remove the following, please search for the "iterator_mode" in the project.
@\trigger_error('Using "iterator_mode" is deprecated. Use "iterable_mode" instead. Usage of "iterator_mode" will be removed in 6.0.', \E_USER_DEPRECATED);
return $query->iterate(null, $hydrationMode);
} elseif (empty($args['ignore_paginator'])) {
if (!empty($args['use_simple_paginator'])) {
// FAST paginator that can handle only simple queries using no joins or ManyToOne joins.
return new SimplePaginator($query);
} else {
// SLOW paginator that can handle complex queries using oneToMany/ManyToMany joins.
return new Paginator($query, false);
}
} else {
// All results
return $query->getResult($hydrationMode);
}
}
/**
* Get a single entity.
*
* @param int $id
*/
public function getEntity($id = 0): ?object
{
try {
if (is_array($id)) {
$q = $this->createQueryBuilder($this->getTableAlias());
$this->buildSelectClause($q, $id['select']);
$q->where($this->getTableAlias().'.id = :id')
->setParameter('id', (int) $id['id']);
$entity = $q->getQuery()->getSingleResult();
} else {
$entity = $this->find((int) $id);
}
} catch (\Exception) {
$entity = null;
}
return $entity;
}
/**
* @return ExpressionBuilder
*/
public function getExpressionBuilder()
{
if (null === $this->expressionBuilder) {
$this->expressionBuilder = new ExpressionBuilder();
}
return $this->expressionBuilder;
}
/**
* @param QueryBuilder|DbalQueryBuilder $q
* @param array<mixed> $filter
*/
public function getFilterExpr($q, array $filter, ?string $unique = null): array
{
$unique = ($unique) ?: $this->generateRandomParameterName();
$parameter = [];
if (isset($filter['group'])) {
$expr = $q->expr()->orX();
foreach ($filter['group'] as $orGroup) {
$groupExpr = $q->expr()->andX();
foreach ($orGroup as $subFilter) {
[$subExpr, $subParameters] = $this->getFilterExpr($q, $subFilter);
$groupExpr->add($subExpr);
if (!empty($subParameters)) {
$parameter = array_merge($parameter, $subParameters);
}
}
$expr->add($groupExpr);
}
} elseif (str_contains($filter['column'], ',')) {
$columns = explode(',', $filter['column']);
$expr = $q->expr()->orX();
$setParameter = false;
foreach ($columns as $c) {
$subFilter = $filter;
$subFilter['column'] = trim($c);
[$subExpr, $parameterUsed] = $this->getFilterExpr($q, $subFilter, $unique);
if ($parameterUsed) {
$setParameter = true;
}
$expr->add($subExpr);
}
if ($setParameter) {
$parameter = [$unique => $filter['value']];
}
} else {
$func = (!empty($filter['operator'])) ? $filter['operator'] : $filter['expr'];
if (in_array($func, ['isNull', 'isNotNull'])) {
$expr = $q->expr()->{$func}($filter['column']);
} elseif (in_array($func, ['in', 'notIn'])) {
$expr = $q->expr()->{$func}($filter['column'], ':'.$unique);
$q->setParameter($unique, $filter['value'], ArrayParameterType::STRING);
} elseif (in_array($func, ['like', 'notLike'])) {
if (isset($filter['strict']) && !$filter['strict']) {
if (is_numeric($filter['value'])) {
// Postgres doesn't like using "LIKE" with numbers
$func = ('like' == $func) ? 'eq' : 'neq';
} else {
$filter['value'] = "%{$filter['value']}%";
}
}
$expr = $q->expr()->{$func}($filter['column'], ':'.$unique);
$parameter = [$unique => $filter['value']];
} else {
if (isset($filter['strict']) && !$filter['strict']) {
$filter['value'] = "%{$filter['value']}%";
}
$expr = $q->expr()->{$func}($filter['column'], ':'.$unique);
$parameter = [$unique => $filter['value']];
}
if (!empty($filter['not'])) {
$expr = $q->expr()->not($expr);
}
}
return [$expr, $parameter];
}
/**
* Returns a andX Expr() that takes into account isPublished, publishUp and publishDown dates
* The Expr() sets a :now and :true parameter that must be set in the calling function.
*
* @param string|null $alias
* @param bool $setNowParameter
* @param bool $setTrueParameter
* @param bool $allowNullForPublishedUp Allow entities without a published up date
*
* @return mixed
*/
public function getPublishedByDateExpression(
$q,
$alias = null,
$setNowParameter = true,
$setTrueParameter = true,
$allowNullForPublishedUp = true
) {
$isORM = $q instanceof QueryBuilder;
if (null === $alias) {
$alias = $this->getTableAlias();
}
if ($setNowParameter) {
$now = new \DateTime();
if (!$isORM) {
$dtHelper = new DateTimeHelper($now, 'Y-m-d H:i:s', 'local');
$now = $dtHelper->toUtcString();
}
$q->setParameter('now', $now);
}
if ($setTrueParameter) {
$q->setParameter('true', true, 'boolean');
}
if ($isORM) {
$pub = 'isPublished';
$pubUp = 'publishUp';
$pubDown = 'publishDown';
} else {
$pub = 'is_published';
$pubUp = 'publish_up';
$pubDown = 'publish_down';
}
$expr = $q->expr()->andX(
$q->expr()->eq("$alias.$pub", ':true'),
$q->expr()->orX(
$q->expr()->isNull("$alias.$pubDown"),
$q->expr()->gte("$alias.$pubDown", ':now')
)
);
if ($allowNullForPublishedUp) {
$expr->add(
$q->expr()->orX(
$q->expr()->isNull("$alias.$pubUp"),
$q->expr()->lte("$alias.$pubUp", ':now')
)
);
} else {
$expr->add(
$q->expr()->andX(
$q->expr()->isNotNull("$alias.$pubUp"),
$q->expr()->lte("$alias.$pubUp", ':now')
)
);
}
return $expr;
}
/**
* Get an array of rows from one table using DBAL.
*
* @param int $start
* @param int $limit
*/
public function getRows($start = 0, $limit = 100, array $order = [], array $where = [], array $select = null, array $allowedJoins = []): array
{
$alias = $this->getTableAlias();
$metadata = $this->getClassMetadata();
$table = $metadata->getTableName();
$q = $this->_em->getConnection()->createQueryBuilder();
$q->select('count(*)')
->from($table, $alias);
// Join associations for permission filtering
$this->buildDbalJoinsFromAssociations($q, $metadata->getAssociationMappings(), $alias, $allowedJoins);
$this->buildWhereClauseFromArray($q, $where);
$count = $q->executeQuery()->fetchOne();
if ($select) {
foreach ($select as &$column) {
if (!str_contains($column, '.')) {
$column = $alias.'.'.$column;
}
}
$selectString = implode(', ', $select);
} else {
$selectString = $alias.'.*';
}
$q->resetQueryPart('select')
->select($selectString)
->setFirstResult($start)
->setMaxResults($limit);
$this->buildOrderByClauseFromArray($q, $order);
$results = $q->executeQuery()->fetchAllAssociative();
return [
'total' => $count,
'results' => $results,
];
}
/**
* Returns a single value for a single row.
*
* @param int $id
* @param string $column
*
* @return string|null
*/
public function getValue($id, $column)
{
$q = $this->_em->getConnection()->createQueryBuilder();
$q->select($this->getTableAlias().'.'.$column)
->from($this->getClassMetadata()->getTableName(), $this->getTableAlias())
->where($this->getTableAlias().'.id = :id')
->setParameter('id', $id);
$result = $q->executeQuery()->fetchAssociative();
return $result[$column] ?? null;
}
/**
* Array of search commands supported by the repository.
*
* @return string[]|array<string, string[]>
*/
public function getSearchCommands(): array
{
return ['mautic.core.searchcommand.ids'];
}
/**
* Gets a list of published entities as an array id => label.
*
* @param array $parameters Parameters used in $expr
* @param string $labelColumn Column that houses the label
* @param string $valueColumn Column that houses the value
* @param string $extraColumns String of extra select columns
* @param int $limit Limit for results
*
* @return mixed[]
*/
public function getSimpleList(CompositeExpression $expr = null, array $parameters = [], $labelColumn = null, $valueColumn = 'id', $extraColumns = null, $limit = 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) {
$labelColumn = $reflection->hasMethod('getTitle') ? 'title' : 'name';
}
$q->select($prefix.$valueColumn.' as value, '.$prefix.$labelColumn.' as label'.($extraColumns ? ", $extraColumns" : ''))
->from($tableName, $alias)
->orderBy($prefix.$labelColumn);
if (null !== $expr && $expr->count()) {
$q->where($expr);
}
foreach ($parameters as $key => $value) {
$q->setParameter($key, $value, is_array($value) ? ArrayParameterType::STRING : null);
}
// Published only
if ($reflection->hasMethod('getIsPublished')) {
$q->andWhere(
$q->expr()->eq($prefix.'is_published', ':true')
)
->setParameter('true', true, 'boolean');
}
if ($limit) {
$q->setMaxResults((int) $limit);
}
return $q->executeQuery()->fetchAllAssociative();
}
/**
* @return string[]
*/
public function getStandardSearchCommands(): array
{
return [
'mautic.core.searchcommand.ispublished',
'mautic.core.searchcommand.isunpublished',
'mautic.core.searchcommand.isuncategorized',
'mautic.core.searchcommand.ismine',
'mautic.core.searchcommand.category',
'mautic.core.searchcommand.ids',
];
}
/**
* @return literal-string
*/
public function getTableAlias(): string
{
return 'e';
}
/**
* @return mixed[]
*/
public function getTableColumns(): array
{
$columns = $this->getClassMetadata()->getColumnNames();
if ($associations = $this->getClassMetadata()->getAssociationMappings()) {
foreach ($associations as $association) {
if (!empty($association['joinColumnFieldNames'])) {
$columns = array_merge($columns, array_values($association['joinColumnFieldNames']));
}
}
}
natcasesort($columns);
return array_values($columns);
}
/**
* Returns entity table name.
*
* @return string
*/
public function getTableName()
{
return $this->getClassMetadata()->getTableName();
}
/**
* Persist an array of entities.
*
* @param array|ArrayCollection $entities
*/
public function saveEntities($entities): void
{
// iterate over the results so the events are dispatched on each delete
$batchSize = 20;
$i = 0;
foreach ($entities as $entity) {
$this->saveEntity($entity, false);
if (0 === ++$i % $batchSize) {
$this->getEntityManager()->flush();
}
}
$this->getEntityManager()->flush();
}
/**
* Save an entity through the repository.
*
* @param object $entity
* @param bool $flush true by default; use false if persisting in batches
*/
public function saveEntity($entity, $flush = true): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush($entity);
}
}
/**
* Insert entity if it does not exist, update if it does.
* ID is set to the enity after upsert.
* Main reason to use this over fetch/save is to avoid race conditions.
*
* Warning: This method use DBAL, not ORM. It will save only the entity you send it.
* It will NOT save the entity's associations. Entity manager won't know that the entity was flushed.
*/
public function upsert(object $entity): void
{
$connection = $this->getEntityManager()->getConnection();
$metadata = $this->getClassMetadata();
$identifier = $metadata->getSingleIdentifierFieldName();
$makeUpdate = fn (string $column) => "{$column} = VALUES({$column})";
$columns = [];
$values = [];
$types = [];
$set = [];
$update = [];
$hasId = $metadata->containsForeignIdentifier;
foreach ($metadata->getFieldNames() as $fieldName) {
$value = $metadata->getFieldValue($entity, $fieldName);
if ($metadata->isIdentifier($fieldName)) {
if ($value) {
$hasId = true;
} else {
continue;
}
}
$column = $metadata->getColumnName($fieldName);
$columns[] = $column;
$values[] = $value;
$types[] = $metadata->getTypeOfField($fieldName);
$set[] = '?';
$update[] = $makeUpdate($column);
}
foreach ($metadata->getAssociationNames() as $fieldName) {
$assocEntity = $metadata->getFieldValue($entity, $fieldName);
if (!$metadata->isAssociationWithSingleJoinColumn($fieldName) || !is_object($assocEntity)) {
continue;
}
$idCol = ucfirst($metadata->getSingleAssociationReferencedJoinColumnName($fieldName));
$idGetter = "get{$idCol}";
$column = $metadata->getSingleAssociationJoinColumnName($fieldName);
$columns[] = $column;
$values[] = $assocEntity->$idGetter();
$types[] = Types::STRING;
$set[] = '?';
$update[] = $makeUpdate($column);
}
$numberOfRowsAffected = $connection->executeStatement(
'INSERT INTO '.$this->getTableName().' ('.implode(', ', $columns).')'.
' VALUES ('.implode(', ', $set).')'.
' ON DUPLICATE KEY UPDATE '.implode(', ', $update),
$values,
$types
);
if ($entity instanceof UpsertInterface) {
$entity->setHasBeenInserted(UpsertInterface::ROWS_AFFECTED_ON_INSERT === $numberOfRowsAffected);
$entity->setHasBeenUpdated(UpsertInterface::ROWS_AFFECTED_ON_UPDATE === $numberOfRowsAffected);
}
if ($hasId) {
return;
}
$id = (int) $connection->lastInsertId();
$metadata->setFieldValue($entity, $identifier, $id);
}
/**
* Set the current user (i.e. from security context) for use within repositories.
*
* @param User $user
*/
public function setCurrentUser($user): void
{
if (!$user instanceof User) {
// just create a blank user entity
$user = new User();
}
$this->currentUser = $user;
}
public function setTranslator(TranslatorInterface $translator): void
{
$this->translator = $translator;
}
/**
* Validate array for one order by condition.
*
* @param array $clause ['col' => 'column_a', 'dir' => 'ASC']
*
* @return array
*
* @throws \InvalidArgumentException
*/
protected function validateOrderByClause($clause)
{
$msg = '"%s" is missing in the order by clause array.';
if (empty($clause['col'])) {
throw new \InvalidArgumentException(sprintf($msg, 'col'));
}
if (empty($clause['dir'])) {
$clause['dir'] = 'ASC';
}
$clause['dir'] = $this->sanitize(strtoupper($clause['dir']));
$clause['col'] = $this->sanitize($clause['col'], ['_.']);
return $clause;
}
/**
* Validate the array for one where condition.
*
* @param array $clause ['expr' => 'expression', 'col' => 'DB column', 'val' => 'value to search for']
*
* @throws \InvalidArgumentException
*/
protected function validateWhereClause(array $clause): array
{
$msg = '"%s" is missing in the where clause array.';
if (empty($clause['expr'])) {
throw new \InvalidArgumentException(sprintf($msg, 'expr'));
}
if (empty($clause['col']) && empty($clause['column'])) {
throw new \InvalidArgumentException(sprintf($msg, 'col'));
}
if (!array_key_exists('val', $clause) && !array_key_exists('value', $clause)) {
$clause['val'] = '';
}
$clause['expr'] = $this->sanitize($clause['expr']);
$clause['col'] = $this->sanitize($clause['column'] ?? $clause['col'], ['_', '.']);
if (isset($clause['value'])) {
$clause['val'] = $clause['value'];
}
unset($clause['value'], $clause['column']);
// Value will be sanitized by Doctrine
return $clause;
}
/**
* @param QueryBuilder|DbalQueryBuilder $qb
* @param \StdClass|mixed[] $filters
*
* @return mixed[]
*/
protected function addAdvancedSearchWhereClause($qb, $filters): array
{
$parseFilters = [];
if (isset($filters->root[0])) {
// Function is determined by the second clause type
$type = (isset($filters->root[1])) ? $filters->root[1]->type : $filters->root[0]->type;
$parseFilters = &$filters->root;
} elseif (isset($filters->children[0])) {
$type = (isset($filters->children[1])) ? $filters->children[1]->type : $filters->children[0]->type;
$parseFilters = &$filters->children;
} elseif (is_array($filters)) {
$type = (isset($filters[1])) ? $filters[1]->type : $filters[0]->type;
$parseFilters = &$filters;
}
if (empty($type)) {
$type = 'and';
}
$parameters = [];
$expressions = $qb->expr()->{"{$type}X"}();
if ($parseFilters) {
$this->parseSearchFilters($parseFilters, $qb, $expressions, $parameters);
}
return [$expressions, $parameters];
}
/**
* @param QueryBuilder|DbalQueryBuilder $qb
* @param \StdClass $filter
*/
protected function addCatchAllWhereClause($qb, $filter): array
{
foreach (['name', 'title'] as $column) {
if ($this->getClassMetadata()->hasField($column)) {
return $this->addStandardCatchAllWhereClause(
$qb,
$filter,
[
$this->getTableAlias().'.'.$column,
]
);
}
}
return [
false,
[],
];
}
/**
* Unique handling for $filter->not since dbal does not support the not() function with it's QueryBuilder.
*
* @param QueryBuilder $q
* @param object $filter
*/
protected function addDbalCatchAllWhereClause(&$q, $filter, array $columns): array
{
$unique = $this->generateRandomParameterName(); // ensure that the string has a unique parameter identifier
$string = ($filter->strict) ? $filter->string : "{$filter->string}";
if ($filter->not) {
$xFunc = 'andX';
$exprFunc = 'notLike';
} else {
$xFunc = 'orX';
$exprFunc = 'like';
}
$expr = $q->expr()->$xFunc();
foreach ($columns as $column) {
$expr->add(
$q->expr()->$exprFunc($column, ":$unique")
);
}
return [
$expr,
["$unique" => $string],
];
}
/**
* @param QueryBuilder|DbalQueryBuilder $q
* @param \StdClass $filter
*/
protected function addSearchCommandWhereClause($q, $filter): array
{
$command = $filter->command;
$expr = false;
switch ($command) {
case $this->translator->trans('mautic.core.searchcommand.ids'):
case $this->translator->trans('mautic.core.searchcommand.ids', [], null, 'en_US'):
$expr = $this->getIdsExpr($q, $filter);
break;
}
return [
$expr,
[],
];
}
/**
* @param QueryBuilder $q
* @param object $filter
*/
protected function addStandardCatchAllWhereClause(&$q, $filter, array $columns): array
{
$unique = $this->generateRandomParameterName(); // ensure that the string has a unique parameter identifier
$string = $filter->string;
if (!$filter->strict) {
if (!str_contains($string, '%')) {
$string = "%$string%";
}
}
$ormQb = true;
if ($q instanceof QueryBuilder) {
$xFunc = 'orX';
$exprFunc = 'like';
} else {
$ormQb = false;
if ($filter->not) {
$xFunc = 'andX';
$exprFunc = 'notLike';
} else {
$xFunc = 'orX';
$exprFunc = 'like';
}
}
$expr = $q->expr()->$xFunc();
foreach ($columns as $col) {
$expr->add(
$q->expr()->$exprFunc($col, ":$unique")
);
}
if ($ormQb && $filter->not) {
$expr = $q->expr()->not($expr);
}
return [
$expr,
["$unique" => $string],
];
}
/**
* @param DbalQueryBuilder|QueryBuilder $q
* @param \StdClass $filter
*/
protected function addStandardSearchCommandWhereClause(&$q, $filter): array
{
$command = $filter->command;
$unique = $this->generateRandomParameterName();
$returnParameter = true; // returning a parameter that is not used will lead to a Doctrine error
$expr = false;
$prefix = $this->getTableAlias();
$isDbalQB = $q instanceof DbalQueryBuilder;
switch ($command) {
case $this->translator->trans('mautic.core.searchcommand.ispublished'):
case $this->translator->trans('mautic.core.searchcommand.ispublished', [], null, 'en_US'):
$column = $isDbalQB ? 'is_published' : 'isPublished';
$expr = $q->expr()->eq("{$prefix}.{$column}", ":{$unique}");
$forceParameters = [$unique => true];
break;
case $this->translator->trans('mautic.core.searchcommand.isunpublished'):
case $this->translator->trans('mautic.core.searchcommand.isunpublished', [], null, 'en_US'):
$column = $isDbalQB ? 'is_published' : 'isPublished';
$expr = $q->expr()->eq("{$prefix}.{$column}", ":{$unique}");
$forceParameters = [$unique => false];
break;
case $this->translator->trans('mautic.core.searchcommand.isuncategorized'):
case $this->translator->trans('mautic.core.searchcommand.isuncategorized', [], null, 'en_US'):
$expr = $q->expr()->orX(
$q->expr()->isNull("$prefix.category"),
$q->expr()->eq("$prefix.category", $q->expr()->literal(''))
);
$returnParameter = false;
break;
case $this->translator->trans('mautic.core.searchcommand.ismine'):
case $this->translator->trans('mautic.core.searchcommand.ismine', [], null, 'en_US'):
$column = $isDbalQB ? 'created_by' : 'createdBy';
$expr = $q->expr()->eq("{$prefix}.{$column}", ":{$unique}");
$forceParameters = [$unique => $this->currentUser->getId()];
break;
case $this->translator->trans('mautic.core.searchcommand.category'):
case $this->translator->trans('mautic.core.searchcommand.category', [], null, 'en_US'):
// Find the category prefix
$joins = $q->getDQLPart('join');
$catPrefix = false;
foreach ($joins as $joinStatements) {
/** @var Query\Expr\Join $join */
foreach ($joinStatements as $join) {
if (str_contains($join->getJoin(), '.category')) {
$catPrefix = $join->getAlias();
break;
}
}
if (false !== $catPrefix) {
break;
}
}
if (false === $catPrefix) {
$catPrefix = 'c';
}
$expr = $q->expr()->like("{$catPrefix}.alias", ":{$unique}");
$filter->strict = true;
break;
case $this->translator->trans('mautic.core.searchcommand.ids'):
case $this->translator->trans('mautic.core.searchcommand.ids', [], null, 'en_US'):
$expr = $this->getIdsExpr($q, $filter);
$returnParameter = false;
break;
}
if ($expr && $filter->not) {
$expr = $q->expr()->not($expr);
}
if (!empty($forceParameters)) {
$parameters = $forceParameters;
} elseif (!$returnParameter) {
$parameters = [];
} else {
$string = $filter->string;
if (!$filter->strict) {
if (!str_contains($string, '%')) {
$string = "$string%";
}
}
$parameters = ["$unique" => $string];
}
return [$expr, $parameters];
}
protected function appendExpression($appendTo, $expr)
{
if ($expr instanceof CompositeExpression || $expr instanceof Query\Expr\Composite) {
if ($expr->count()) {
$appendTo->add($expr);
}
} elseif (!empty($expr)) {
$appendTo->add($expr);
}
}
/**
* @param QueryBuilder $q
*/
protected function buildClauses($q, array $args): bool
{
$this->buildSelectClause($q, $args);
$this->buildIndexByClause($q, $args);
$this->buildWhereClause($q, $args);
$this->buildOrderByClause($q, $args);
$this->buildLimiterClauses($q, $args);
return true;
}
protected function buildDbalJoinsFromAssociations(DbalQueryBuilder $q, $associations, $alias, array $allowed): bool
{
$joinAdded = false;
foreach ($associations as $property => $association) {
$subJoinAdded = false;
$targetMetdata = $this->_em->getRepository($association['targetEntity'])->getClassMetadata();
if ($propertyAllowedJoins = preg_grep('/^'.$property.'\..*/', $allowed)) {
foreach ($propertyAllowedJoins as $key => $join) {
$propertyAllowedJoins[$key] = str_replace($property.'.', '', $join);
}
$subJoinAdded = $this->buildDbalJoinsFromAssociations($q, $targetMetdata->getAssociationMappings(), $property, $propertyAllowedJoins);
}
if ($subJoinAdded || in_array($property, $allowed)) {
// Unset the property so that it's not used again in other the next level
unset($allowed[$property]);
$targetTable = $targetMetdata->getTableName();
$hasNullable = false;
$joinColumns = [];
foreach ($association['joinColumns'] as $join) {
if (!empty($join['nullable'])) {
$hasNullable = true;
}
$joinColumns[] = $alias.'.'.$join['name'].' = '.$property.'.'.$join['referencedColumnName'];
}
$joinType = ($hasNullable) ? 'leftJoin' : 'join';
$q->$joinType($alias, $targetTable, $property, implode(' AND ', $joinColumns));
$joinAdded = true;
}
}
return $joinAdded;
}
protected function buildIndexByClause($q, array $args)
{
if (!empty($args['index_by'])) {
if (is_array($args['index_by'])) {
[$indexAlias, $indexBy] = $args['index_by'];
} else {
$indexAlias = $this->getTableAlias();
$indexBy = $args['index_by'];
}
if (!str_starts_with($indexBy, $indexAlias)) {
$indexBy = $indexAlias.'.'.$indexBy;
}
$q->indexBy($indexAlias, $indexBy);
}
}
/**
* @param QueryBuilder|DbalQueryBuilder $q
*/
protected function buildLimiterClauses($q, array $args): void
{
$start = array_key_exists('start', $args) ? $args['start'] : 0;
$limit = array_key_exists('limit', $args) ? $args['limit'] : 0;
if (!empty($limit)) {
$q->setFirstResult($start)
->setMaxResults($limit);
}
}
/**
* @param QueryBuilder|DbalQueryBuilder $q
*/
protected function buildOrderByClause($q, array $args): void
{
$orderBy = array_key_exists('orderBy', $args) ? $args['orderBy'] : '';
if (!empty($args['filter']['order'])) {
$this->buildOrderByClauseFromArray($q, $args['filter']['order']);
} elseif (empty($orderBy)) {
$defaultOrder = $this->getDefaultOrder();
foreach ($defaultOrder as $order) {
$q->addOrderBy($order[0], $order[1]);
}
} else {
$orderByDir = $this->sanitize(
array_key_exists('orderByDir', $args) ? $args['orderByDir'] : ''
);
// add direction after each column
$parts = explode(',', $orderBy);
foreach ($parts as $order) {
$order = $this->sanitize($order, ['_', '.']);
$q->addOrderBy($order, $orderByDir);
}
}
}
/**
* Build order by from an array.
*
* @param QueryBuilder|DbalQueryBuilder $query
* @param array $clauses [['col' => 'column_a', 'dir' => 'ASC']]
*
* @return array
*/
protected function buildOrderByClauseFromArray($query, array $clauses)
{
if ($clauses && is_array($clauses)) {
foreach ($clauses as $clause) {
$clause = $this->validateOrderByClause($clause);
$column = (!str_contains($clause['col'], '.')) ? $this->getTableAlias().'.'.$clause['col'] : $clause['col'];
$query->addOrderBy($column, $clause['dir']);
}
}
}
/**
* @param QueryBuilder|DbalQueryBuilder $q
*/
protected function buildSelectClause($q, array $args)
{
$isOrm = $q instanceof QueryBuilder;
if (isset($args['select'])) {
// Build a custom select
if (is_string($args['select'])) {
$args['select'] = explode(',', $args['select']);
}
$selects = [];
foreach ($args['select'] as $select) {
if (str_contains($select, '.')) {
[$alias, $select] = explode('.', $select);
} else {
$alias = $this->getTableAlias();
}
if (!isset($selects[$alias])) {
$selects[$alias] = [];
}
$selects[$alias][] = $select;
}
$partials = [];
$ormColumns = $this->getBaseColumns($this->getClassName());
$dbalColumns = $this->getTableColumns();
foreach ($selects as $alias => $columns) {
if ($isOrm) {
if ($columns = array_intersect($columns, $ormColumns)) {
$columns = array_map([$this, 'sanitize'], $columns);
$partials[] = 'partial '.$alias.'.{'.implode(',', $columns).'}';
}
} else {
if ($columns = array_intersect($columns, $dbalColumns)) {
foreach ($columns as $column) {
$partials[] = $alias.'.'.$this->sanitize($column);
}
}
}
}
if ($partials) {
$newSelect = implode(', ', $partials);
$select = ($isOrm) ? $q->getDQLPart('select') : $q->getQueryPart('select');
if ($isOrm) {
$q->select($newSelect);
} else {
if (!$select || $this->getTableAlias() === $select || $this->getTableAlias().'.*' === $select) {
$q->select($newSelect);
} elseif (is_string($select) && str_contains($select, $this->getTableAlias().',')) {
$q->select(str_replace($this->getTableAlias().',', $newSelect.',', $select));
} elseif (is_string($select) && str_contains($select, $this->getTableAlias().'.*,')) {
$q->select(str_replace($this->getTableAlias().'.*,', $newSelect.',', $select));
}
}
}
}
if ($isOrm) {
if (!$q->getDQLPart('select')) {
$q->select($this->getTableAlias());
}
} else {
if (!$q->getQueryPart('select')) {
$q->select($this->getTableAlias().'.*');
}
}
}
/**
* @param QueryBuilder|DbalQueryBuilder $q
*/
protected function buildWhereClause($q, array $args)
{
$filter = array_key_exists('filter', $args) ? $args['filter'] : '';
$filterHelper = new SearchStringHelper();
$advancedFilters = new \stdClass();
$advancedFilters->root = [];
$advancedFilters->commands = [];
// Reset advanced filter commands to be used in search query building
$this->advancedFilterCommands = [];
$advancedFilterStrings = [];
$queryParameters = [];
$queryExpression = $q->expr()->andX();
if (isset($args['ids'])) {
$ids = array_map('intval', $args['ids']);
$param = $this->generateRandomParameterName();
if ($q instanceof QueryBuilder) {
$queryExpression->add(
$q->expr()->in($this->getTableAlias().'.id', ':'.$param)
);
$queryParameters[$param] = $ids;
} else {
$queryExpression->add(
$q->expr()->in($this->getTableAlias().'.id', ':'.$param)
);
$q->setParameter($param, $ids, ArrayParameterType::INTEGER);
}
} elseif (!empty($args['ownedBy'])) {
$queryExpression->add(
$q->expr()->in($this->getTableAlias().'.'.$args['ownedBy'][0], (string) $args['ownedBy'][1])
);
}
if (!empty($filter)) {
if (is_array($filter)) {
if (!empty($filter['where'])) {
// build clauses from array
$this->buildWhereClauseFromArray($q, $filter['where']);
} elseif (!empty($filter['criteria']) || !empty($filter['force'])) {
$criteria = !empty($filter['criteria']) ? $filter['criteria'] : $filter['force'];
if (is_array($criteria)) {
// defined columns with keys of column, expr, value
foreach ($criteria as $criterion) {
if ($criterion instanceof Query\Expr || $criterion instanceof CompositeExpression) {
$queryExpression->add($criterion);
if (isset($criterion->parameters) && is_array($criterion->parameters)) {
$queryParameters = array_merge($queryParameters, $criterion->parameters);
unset($criterion->parameters);
}
} elseif (is_array($criterion)) {
[$expr, $parameters] = $this->getFilterExpr($q, $criterion);
$queryExpression->add($expr);
if (is_array($parameters)) {
$queryParameters = array_merge($queryParameters, $parameters);
}
} else {
// string so parse as advanced search
$advancedFilterStrings[] = $criterion;
}
}
} else {
// string so parse as advanced search
$advancedFilterStrings[] = $criteria;
}
}
if (!empty($filter['string'])) {
$advancedFilterStrings[] = $filter['string'];
}
} else {
$advancedFilterStrings[] = $filter;
}
if (!empty($advancedFilterStrings)) {
foreach ($advancedFilterStrings as $parseString) {
$parsed = $filterHelper->parseString($parseString);
$advancedFilters->root = array_merge($advancedFilters->root, $parsed->root);
$filterHelper->mergeCommands($advancedFilters, $parsed->commands);
}
$this->advancedFilterCommands = $advancedFilters->commands;
[$expr, $parameters] = $this->addAdvancedSearchWhereClause($q, $advancedFilters);
$this->appendExpression($queryExpression, $expr);
if (is_array($parameters)) {
$queryParameters = array_merge($queryParameters, $parameters);
}
}
}
// parse the filter if set
if ($queryExpression->count()) {
$q->andWhere($queryExpression);
}
// Parameters have to be set even if there are no expressions just in case a search command
// passed back a parameter it used
foreach ($queryParameters as $k => $v) {
if (true === $v || false === $v) {
$q->setParameter($k, $v, 'boolean');
} else {
$q->setParameter($k, $v);
}
}
}
/**
* @param QueryBuilder|DbalQueryBuilder $query
* @param array $clauses [['expr' => 'expression', 'col' => 'DB column', 'val' => 'value to search for']]
*/
protected function buildWhereClauseFromArray($query, array $clauses, $expr = null)
{
$columnValue = ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'like', 'notLike', 'in', 'notIn', 'between', 'notBetween'];
$justColumn = ['isNull', 'isNotNull', 'isEmpty', 'isNotEmpty'];
$andOr = ['andX', 'orX'];
if ($clauses && is_array($clauses)) {
foreach ($clauses as $clause) {
if (!empty($clause['internal']) && 'formula' === $clause['expr']) {
$whereClause = array_key_exists('value', $clause) ? $clause['value'] : $clause['val'];
if ($expr) {
$expr->add($whereClause);
} else {
$query->andWhere($whereClause);
}
continue;
}
if (in_array($clause['expr'], $andOr)) {
$composite = $query->expr()->{$clause['expr']}();
$this->buildWhereClauseFromArray($query, $clause['val'], $composite);
if (null === $expr) {
$query->andWhere($composite);
} else {
$expr->add($composite);
}
} else {
$clause = $this->validateWhereClause($clause);
$column = (!str_contains($clause['col'], '.')) ? $this->getTableAlias().'.'.$clause['col'] : $clause['col'];
$whereClause = null;
switch ($clause['expr']) {
case 'between':
case 'notBetween':
if (is_array($clause['val']) && 2 === count($clause['val'])) {
$not = 'notBetween' === $clause['expr'] ? ' NOT' : '';
$param = $this->generateRandomParameterName();
$query->setParameter($param, $clause['val'][0]);
$param2 = $this->generateRandomParameterName();
$query->setParameter($param2, $clause['val'][1]);
$whereClause = $column.$not.' BETWEEN :'.$param.' AND :'.$param2;
}
break;
case 'isEmpty':
case 'isNotEmpty':
if ('isEmpty' === $clause['expr']) {
$whereClause = $query->expr()->orX(
$query->expr()->eq($column, $query->expr()->literal('')),
$query->expr()->isNull($column)
);
} else {
$whereClause = $query->expr()->andX(
$query->expr()->neq($column, $query->expr()->literal('')),
$query->expr()->isNotNull($column)
);
}
break;
case 'in':
case 'notIn':
$parsed = str_getcsv(html_entity_decode($clause['val']), ',', '"');
$param = $this->generateRandomParameterName();
$arg = count($parsed) > 1 ? $parsed : array_shift($parsed);
if (is_array($arg)) {
$whereClause = $query->expr()->{$clause['expr']}($column, ':'.$param);
$query->setParameter($param, $arg, ArrayParameterType::STRING);
} else {
$expression = 'in' === $clause['expr'] ? 'eq' : 'neq';
$whereClause = $query->expr()->{$expression}($column, ':'.$param);
$query->setParameter($param, $arg);
}
break;
default:
if (method_exists($query->expr(), $clause['expr'])) {
if (in_array($clause['expr'], $columnValue)) {
$param = $this->generateRandomParameterName();
$whereClause = $query->expr()->{$clause['expr']}($column, ':'.$param);
$query->setParameter($param, $clause['val']);
} elseif (in_array($clause['expr'], $justColumn)) {
$whereClause = $query->expr()->{$clause['expr']}($column);
}
}
}
if ($whereClause) {
if ($expr) {
$expr->add($whereClause);
} else {
$query->andWhere($whereClause);
}
}
}
}
}
}
/**
* Generate a unique parameter name from int using base conversion.
* This eliminates chance for parameter name collision and provides unique result for each number.
* Duplicate method because of DI refactoring difficulty.
*
* @see \Mautic\LeadBundle\Segment\RandomParameterName
* @see https://stackoverflow.com/questions/307486/short-unique-id-in-php/1516430#1516430
*/
public function generateRandomParameterName(): string
{
$value = base_convert((string) $this->lastUsedParameterId, 10, 36);
++$this->lastUsedParameterId;
return 'par'.$value;
}
protected function getDefaultOrder(): array
{
return [];
}
/**
* @return mixed
*/
protected function getIdsExpr(&$q, $filter)
{
if ($ids = array_map('intval', explode(',', $filter->string))) {
return $q->expr()->in($this->getTableAlias().'.id', $ids);
}
return false;
}
/**
* Test to see if a given command is supported by the repository.
*
* @param string $command
* @param string $subcommand
*/
protected function isSupportedSearchCommand(&$command, &$subcommand): bool
{
$commands = $this->getSearchCommands();
foreach ($commands as $k => $c) {
if (is_array($c)) {
// subcommands
if ($this->translator->trans($k) == $command || $this->translator->trans($k, [], null, 'en_US') == $command) {
foreach ($c as $subc) {
if ($this->translator->trans($subc) == $subcommand || $this->translator->trans($subc, [], null, 'en_US') == $subcommand) {
return true;
}
}
}
} elseif ($this->translator->trans($c) == $command || $this->translator->trans($c, [], null, 'en_US') == $command) {
return true;
} elseif ($this->translator->trans($c) == "{$command}:{$subcommand}"
|| $this->translator->trans($c, [], null, 'en_US') == "{$command}:{$subcommand}"
) {
$command = "{$command}:{$subcommand}";
$subcommand = '';
return true;
}
}
return false;
}
/**
* @param \StdClass $parseFilters
* @param QueryBuilder|DbalQueryBuilder $qb
*/
protected function parseSearchFilters($parseFilters, $qb, $expressions, &$parameters)
{
foreach ($parseFilters as $f) { /** @phpstan-ignore-line we are iterating over StdClass. We should refactor this into a collection of DTO objects in M6 */
if (isset($f->children)) {
[$expr, $params] = $this->addAdvancedSearchWhereClause($qb, $f);
} else {
if (!empty($f->command)) {
if ($this->isSupportedSearchCommand($f->command, $f->string)) {
[$expr, $params] = $this->addSearchCommandWhereClause($qb, $f);
} else {
// treat the command:string as if its a single word
$f->string = $f->command.':'.$f->string;
$f->not = false;
$f->strict = true;
[$expr, $params] = $this->addCatchAllWhereClause($qb, $f);
}
} else {
[$expr, $params] = $this->addCatchAllWhereClause($qb, $f);
}
}
if (!empty($params)) {
$parameters = array_merge($parameters, $params);
}
$this->appendExpression($expressions, $expr);
}
}
/**
* Sanitizes a string to alphanum plus characters in the second argument.
*
* @param string $sqlAttr
* @param array $allowedCharacters
*/
protected function sanitize($sqlAttr, $allowedCharacters = []): string
{
return InputHelper::alphanum($sqlAttr, false, null, $allowedCharacters);
}
private function convertOrmPropertiesToColumns(array &$filters, array $properties): void
{
foreach ($filters as &$f) {
$key = (isset($f['col'])) ? 'col' : 'column';
$col = $f[$key];
$alias = '';
if (str_contains($col, '.')) {
[$alias, $col] = explode('.', $col);
}
if (in_array($col, $properties)) {
$col = preg_replace('/(?<=\\w)(?=[A-Z])/', '_$1', $col);
$col = strtolower($col);
}
$f[$key] = (!empty($alias)) ? $alias.'.'.$col : $col;
}
}
/**
* Checks if table contains any rows.
*/
protected function tableHasRows(string $table): bool
{
$query = $this->getEntityManager()->getConnection()->createQueryBuilder();
$query->select('null')
->from($table)
->setMaxResults(1);
return (bool) count($query->executeQuery()->fetchAllAssociative());
}
}