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