From eb2664ac79b61a1d57ee7fd4da9a48cae434ebac Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Wed, 30 Mar 2022 19:01:22 +0200 Subject: [PATCH] support more complex metafield queries (#3228) --- src/Repository/ActivityRepository.php | 52 +++++----- src/Repository/CustomerRepository.php | 59 +++++------- src/Repository/InvoiceRepository.php | 50 +++++----- src/Repository/ProjectRepository.php | 53 +++++----- src/Repository/RepositorySearchTrait.php | 118 +++++++++++++++++++++++ src/Repository/TimesheetRepository.php | 49 +++++----- 6 files changed, 228 insertions(+), 153 deletions(-) create mode 100644 src/Repository/RepositorySearchTrait.php diff --git a/src/Repository/ActivityRepository.php b/src/Repository/ActivityRepository.php index 7fb9710c..03689275 100644 --- a/src/Repository/ActivityRepository.php +++ b/src/Repository/ActivityRepository.php @@ -10,6 +10,7 @@ namespace App\Repository; use App\Entity\Activity; +use App\Entity\ActivityMeta; use App\Entity\Project; use App\Entity\Team; use App\Entity\Timesheet; @@ -33,6 +34,8 @@ use Pagerfanta\Pagerfanta; */ class ActivityRepository extends EntityRepository { + use RepositorySearchTrait; + /** * @param mixed $id * @param null $lockMode @@ -394,40 +397,29 @@ class ActivityRepository extends EntityRepository $this->addPermissionCriteria($qb, $query->getCurrentUser(), $query->getTeams(), $query->isGlobalsOnly()); - if ($query->hasSearchTerm()) { - $searchAnd = $qb->expr()->andX(); - $searchTerm = $query->getSearchTerm(); - - foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) { - $qb->leftJoin('a.meta', 'meta'); - $searchAnd->add( - $qb->expr()->andX( - $qb->expr()->eq('meta.name', ':metaName'), - $qb->expr()->like('meta.value', ':metaValue') - ) - ); - $qb->setParameter('metaName', $metaName); - $qb->setParameter('metaValue', '%' . $metaValue . '%'); - } - - if ($searchTerm->hasSearchTerm()) { - $searchAnd->add( - $qb->expr()->orX( - $qb->expr()->like('a.name', ':searchTerm'), - $qb->expr()->like('a.comment', ':searchTerm') - ) - ); - $qb->setParameter('searchTerm', '%' . $searchTerm->getSearchTerm() . '%'); - } - - if ($searchAnd->count() > 0) { - $qb->andWhere($searchAnd); - } - } + $this->addSearchTerm($qb, $query); return $qb; } + private function getMetaFieldClass(): string + { + return ActivityMeta::class; + } + + private function getMetaFieldName(): string + { + return 'activity'; + } + + /** + * @return array + */ + private function getSearchableFields(): array + { + return ['a.name', 'a.comment']; + } + public function countActivitiesForQuery(ActivityQuery $query): int { $qb = $this->getQueryBuilderForQuery($query); diff --git a/src/Repository/CustomerRepository.php b/src/Repository/CustomerRepository.php index 39e88dc9..03a4def1 100644 --- a/src/Repository/CustomerRepository.php +++ b/src/Repository/CustomerRepository.php @@ -12,6 +12,7 @@ namespace App\Repository; use App\Entity\Activity; use App\Entity\Customer; use App\Entity\CustomerComment; +use App\Entity\CustomerMeta; use App\Entity\Project; use App\Entity\Team; use App\Entity\Timesheet; @@ -34,6 +35,8 @@ use Pagerfanta\Pagerfanta; */ class CustomerRepository extends EntityRepository { + use RepositorySearchTrait; + /** * @param mixed $id * @param null $lockMode @@ -292,47 +295,29 @@ class CustomerRepository extends EntityRepository $this->addPermissionCriteria($qb, $query->getCurrentUser(), $query->getTeams()); - if ($query->hasSearchTerm()) { - $searchAnd = $qb->expr()->andX(); - $searchTerm = $query->getSearchTerm(); - - foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) { - $qb->leftJoin('c.meta', 'meta'); - $searchAnd->add( - $qb->expr()->andX( - $qb->expr()->eq('meta.name', ':metaName'), - $qb->expr()->like('meta.value', ':metaValue') - ) - ); - $qb->setParameter('metaName', $metaName); - $qb->setParameter('metaValue', '%' . $metaValue . '%'); - } - - if ($searchTerm->hasSearchTerm()) { - $searchAnd->add( - $qb->expr()->orX( - $qb->expr()->like('c.name', ':searchTerm'), - $qb->expr()->like('c.comment', ':searchTerm'), - $qb->expr()->like('c.company', ':searchTerm'), - $qb->expr()->like('c.vatId', ':searchTerm'), - $qb->expr()->like('c.number', ':searchTerm'), - $qb->expr()->like('c.contact', ':searchTerm'), - $qb->expr()->like('c.phone', ':searchTerm'), - $qb->expr()->like('c.email', ':searchTerm'), - $qb->expr()->like('c.address', ':searchTerm') - ) - ); - $qb->setParameter('searchTerm', '%' . $searchTerm->getSearchTerm() . '%'); - } - - if ($searchAnd->count() > 0) { - $qb->andWhere($searchAnd); - } - } + $this->addSearchTerm($qb, $query); return $qb; } + private function getMetaFieldClass(): string + { + return CustomerMeta::class; + } + + private function getMetaFieldName(): string + { + return 'customer'; + } + + /** + * @return array + */ + private function getSearchableFields(): array + { + return ['c.name', 'c.comment', 'c.company', 'c.vatId', 'c.number', 'c.contact', 'c.phone', 'c.email', 'c.address']; + } + public function getPagerfantaForQuery(CustomerQuery $query): Pagerfanta { $paginator = new Pagerfanta($this->getPaginatorForQuery($query)); diff --git a/src/Repository/InvoiceRepository.php b/src/Repository/InvoiceRepository.php index aefff55b..23677090 100644 --- a/src/Repository/InvoiceRepository.php +++ b/src/Repository/InvoiceRepository.php @@ -11,6 +11,7 @@ namespace App\Repository; use App\Entity\Customer; use App\Entity\Invoice; +use App\Entity\InvoiceMeta; use App\Entity\Team; use App\Entity\User; use App\Repository\Loader\InvoiceLoader; @@ -26,6 +27,8 @@ use Pagerfanta\Pagerfanta; */ class InvoiceRepository extends EntityRepository { + use RepositorySearchTrait; + public function saveInvoice(Invoice $invoice) { $entityManager = $this->getEntityManager(); @@ -222,39 +225,30 @@ class InvoiceRepository extends EntityRepository if ($query->hasSearchTerm()) { $qb->leftJoin('i.customer', 'customer'); - $searchAnd = $qb->expr()->andX(); - $searchTerm = $query->getSearchTerm(); - - foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) { - $qb->leftJoin('customer.meta', 'meta'); - $searchAnd->add( - $qb->expr()->andX( - $qb->expr()->eq('meta.name', ':metaName'), - $qb->expr()->like('meta.value', ':metaValue') - ) - ); - $qb->setParameter('metaName', $metaName); - $qb->setParameter('metaValue', '%' . $metaValue . '%'); - } - - if ($searchTerm->hasSearchTerm()) { - $searchAnd->add( - $qb->expr()->orX( - $qb->expr()->like('customer.name', ':searchTerm'), - $qb->expr()->like('customer.company', ':searchTerm') - ) - ); - $qb->setParameter('searchTerm', '%' . $searchTerm->getSearchTerm() . '%'); - } - - if ($searchAnd->count() > 0) { - $qb->andWhere($searchAnd); - } + $this->addSearchTerm($qb, $query); } return $qb; } + private function getMetaFieldClass(): string + { + return InvoiceMeta::class; + } + + private function getMetaFieldName(): string + { + return 'invoice'; + } + + /** + * @return array + */ + private function getSearchableFields(): array + { + return ['i.comment', 'customer.name', 'customer.company']; + } + public function countInvoicesForQuery(InvoiceArchiveQuery $query): int { $qb = $this->getQueryBuilderForQuery($query); diff --git a/src/Repository/ProjectRepository.php b/src/Repository/ProjectRepository.php index 5c648774..a36fa9dc 100644 --- a/src/Repository/ProjectRepository.php +++ b/src/Repository/ProjectRepository.php @@ -12,6 +12,7 @@ namespace App\Repository; use App\Entity\Activity; use App\Entity\Project; use App\Entity\ProjectComment; +use App\Entity\ProjectMeta; use App\Entity\Team; use App\Entity\Timesheet; use App\Entity\User; @@ -35,6 +36,8 @@ use Pagerfanta\Pagerfanta; */ class ProjectRepository extends EntityRepository { + use RepositorySearchTrait; + /** * @param mixed $id * @param null $lockMode @@ -378,41 +381,29 @@ class ProjectRepository extends EntityRepository $this->addPermissionCriteria($qb, $query->getCurrentUser()); - if ($query->hasSearchTerm()) { - $searchAnd = $qb->expr()->andX(); - $searchTerm = $query->getSearchTerm(); - - foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) { - $qb->leftJoin('p.meta', 'meta'); - $searchAnd->add( - $qb->expr()->andX( - $qb->expr()->eq('meta.name', ':metaName'), - $qb->expr()->like('meta.value', ':metaValue') - ) - ); - $qb->setParameter('metaName', $metaName); - $qb->setParameter('metaValue', '%' . $metaValue . '%'); - } - - if ($searchTerm->hasSearchTerm()) { - $searchAnd->add( - $qb->expr()->orX( - $qb->expr()->like('p.name', ':searchTerm'), - $qb->expr()->like('p.comment', ':searchTerm'), - $qb->expr()->like('p.orderNumber', ':searchTerm') - ) - ); - $qb->setParameter('searchTerm', '%' . $searchTerm->getSearchTerm() . '%'); - } - - if ($searchAnd->count() > 0) { - $qb->andWhere($searchAnd); - } - } + $this->addSearchTerm($qb, $query); return $qb; } + private function getMetaFieldClass(): string + { + return ProjectMeta::class; + } + + private function getMetaFieldName(): string + { + return 'project'; + } + + /** + * @return array + */ + private function getSearchableFields(): array + { + return ['p.name', 'p.comment', 'p.orderNumber']; + } + private function addProjectStartAndEndDate(QueryBuilder $qb, ?DateTime $begin, ?DateTime $end): Andx { $and = $qb->expr()->andX(); diff --git a/src/Repository/RepositorySearchTrait.php b/src/Repository/RepositorySearchTrait.php new file mode 100644 index 00000000..fbea13ab --- /dev/null +++ b/src/Repository/RepositorySearchTrait.php @@ -0,0 +1,118 @@ + + */ + private function getSearchableFields(): array + { + return []; + } + + private function supportsMetaFields(): bool + { + /* @phpstan-ignore-next-line */ + return $this->getMetaFieldClass() !== null && $this->getMetaFieldName() !== null; + } + + private function addSearchTerm(QueryBuilder $qb, BaseQuery $query): void + { + if (!$query->hasSearchTerm()) { + return; + } + + $searchTerm = $query->getSearchTerm(); + + if (!$this->supportsMetaFields() && !$searchTerm->hasSearchTerm()) { + return; + } + + $aliases = $qb->getRootAliases(); + if (!isset($aliases[0])) { + throw new RepositoryException('No alias was set before invoking addSearchTerm().'); + } + $rootAlias = $aliases[0]; + + $searchAnd = $qb->expr()->andX(); + + if ($this->supportsMetaFields()) { + foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) { + $and = $qb->expr()->andX(); + + if ($metaValue === '*') { + $qb->leftJoin($rootAlias . '.meta', 'meta'); + $and->add($qb->expr()->eq('meta.name', ':metaName')); + $qb->setParameter('metaName', $metaName); + $and->add($qb->expr()->isNotNull('meta.value')); + } elseif ($metaValue === '~') { + $and->add( + sprintf('NOT EXISTS(SELECT metaNotExists FROM %s metaNotExists WHERE metaNotExists.%s = %s.id)', $this->getMetaFieldClass(), $this->getMetaFieldName(), $rootAlias) + ); + } elseif ($metaValue === '' || $metaValue === null) { + $qb->leftJoin($rootAlias . '.meta', 'meta'); + $and->add( + $qb->expr()->orX( + $qb->expr()->andX( + $qb->expr()->eq('meta.name', ':metaName'), + $qb->expr()->isNull('meta.value') + ), + sprintf('NOT EXISTS(SELECT metaNotExists FROM %s metaNotExists WHERE metaNotExists.%s = %s.id)', $this->getMetaFieldClass(), $this->getMetaFieldName(), $rootAlias) + ) + ); + $qb->setParameter('metaName', $metaName); + } else { + $qb->leftJoin($rootAlias . '.meta', 'meta'); + $and->add($qb->expr()->eq('meta.name', ':metaName')); + $and->add($qb->expr()->like('meta.value', ':metaValue')); + $qb->setParameter('metaName', $metaName); + $qb->setParameter('metaValue', '%' . $metaValue . '%'); + } + + $searchAnd->add($and); + } + } + + $fields = $this->getSearchableFields(); + + if ($searchTerm->hasSearchTerm() && \count($fields) > 0) { + $or = $qb->expr()->orX(); + foreach ($fields as $field) { + if (stripos($field, '.') === false) { + $field = $rootAlias . '.' . $field; + } + $or->add( + $qb->expr()->like($field, ':searchTerm'), + ); + } + $searchAnd->add($or); + $qb->setParameter('searchTerm', '%' . $searchTerm->getSearchTerm() . '%'); + } + + if ($searchAnd->count() > 0) { + $qb->andWhere($searchAnd); + } + } +} diff --git a/src/Repository/TimesheetRepository.php b/src/Repository/TimesheetRepository.php index aaa1d88a..cba65ddc 100644 --- a/src/Repository/TimesheetRepository.php +++ b/src/Repository/TimesheetRepository.php @@ -16,6 +16,7 @@ use App\Entity\ProjectRate; use App\Entity\RateInterface; use App\Entity\Team; use App\Entity\Timesheet; +use App\Entity\TimesheetMeta; use App\Entity\User; use App\Model\Statistic\Day; use App\Model\Statistic\Month; @@ -41,6 +42,8 @@ use Pagerfanta\Pagerfanta; */ class TimesheetRepository extends EntityRepository { + use RepositorySearchTrait; + public const STATS_QUERY_DURATION = 'duration'; public const STATS_QUERY_RATE = 'rate'; public const STATS_QUERY_USER = 'users'; @@ -1001,33 +1004,7 @@ class TimesheetRepository extends EntityRepository $requiresTeams = $this->addPermissionCriteria($qb, $query->getCurrentUser(), $query->getTeams()); - if ($query->hasSearchTerm()) { - $searchAnd = $qb->expr()->andX(); - $searchTerm = $query->getSearchTerm(); - - foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) { - $qb->leftJoin('t.meta', 'meta'); - $searchAnd->add( - $qb->expr()->andX( - $qb->expr()->eq('meta.name', ':metaName'), - $qb->expr()->like('meta.value', ':metaValue') - ) - ); - $qb->setParameter('metaName', $metaName); - $qb->setParameter('metaValue', '%' . $metaValue . '%'); - } - - if ($searchTerm->hasSearchTerm()) { - $searchAnd->add( - $qb->expr()->like('t.description', ':searchTerm') - ); - $qb->setParameter('searchTerm', '%' . $searchTerm->getSearchTerm() . '%'); - } - - if ($searchAnd->count() > 0) { - $qb->andWhere($searchAnd); - } - } + $this->addSearchTerm($qb, $query); if ($requiresCustomer || $requiresProject || $requiresTeams) { $qb->leftJoin('t.project', 'p'); @@ -1048,6 +1025,24 @@ class TimesheetRepository extends EntityRepository return $qb; } + private function getMetaFieldClass(): string + { + return TimesheetMeta::class; + } + + private function getMetaFieldName(): string + { + return 'timesheet'; + } + + /** + * @return array + */ + private function getSearchableFields(): array + { + return ['t.description']; + } + /** * @param User|null $user * @param DateTime|null $startFrom