mirror of
https://github.com/kevinpapst/kimai2.git
synced 2025-04-16 10:11:17 +00:00
support more complex metafield queries (find empty, find not existing, find with any value)
This commit is contained in:
parent
31743cf962
commit
eafdb6b207
6 changed files with 228 additions and 153 deletions
|
@ -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<string>
|
||||
*/
|
||||
private function getSearchableFields(): array
|
||||
{
|
||||
return ['a.name', 'a.comment'];
|
||||
}
|
||||
|
||||
public function countActivitiesForQuery(ActivityQuery $query): int
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
|
|
|
@ -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<string>
|
||||
*/
|
||||
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));
|
||||
|
|
|
@ -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<string>
|
||||
*/
|
||||
private function getSearchableFields(): array
|
||||
{
|
||||
return ['i.comment', 'customer.name', 'customer.company'];
|
||||
}
|
||||
|
||||
public function countInvoicesForQuery(InvoiceArchiveQuery $query): int
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
|
|
|
@ -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<string>
|
||||
*/
|
||||
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();
|
||||
|
|
118
src/Repository/RepositorySearchTrait.php
Normal file
118
src/Repository/RepositorySearchTrait.php
Normal file
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
trait RepositorySearchTrait
|
||||
{
|
||||
private function getMetaFieldClass(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getMetaFieldName(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<string>
|
||||
*/
|
||||
private function getSearchableFields(): array
|
||||
{
|
||||
return ['t.description'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User|null $user
|
||||
* @param DateTime|null $startFrom
|
||||
|
|
Loading…
Add table
Reference in a new issue