refactored repositories and DB queries (#5026)
* removed unused teams from export order * added new paginator for query instead of querybuilder * added field hydrate enums * hide PARTIAL deprecation * never log deprecations in production * replaced InvoiceLoader with native Doctrine feature * prevent excessive permission queries * support loading customers of team * improved findByIds * internalized API * fix null string deprecations
This commit is contained in:
@@ -16,14 +16,16 @@ use App\Entity\Team;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Loader\ActivityLoader;
|
||||
use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\LoaderQueryPaginator;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\ActivityQueryHydrate;
|
||||
use App\Utils\Pagination;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Expr\Andx;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@@ -37,53 +39,53 @@ class ActivityRepository extends EntityRepository
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @return Activity[]
|
||||
* @return array<Activity>
|
||||
*/
|
||||
public function findByProject(Project $project): array
|
||||
{
|
||||
return $this->findBy(['project' => $project]);
|
||||
$query = new ActivityQuery();
|
||||
$query->addProject($project);
|
||||
|
||||
return $this->getActivitiesForQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $activityIds
|
||||
* @return Activity[]
|
||||
* @return array<Activity>
|
||||
*/
|
||||
public function findByIds(array $activityIds): array
|
||||
{
|
||||
$ids = array_filter(
|
||||
array_unique($activityIds),
|
||||
function ($value) {
|
||||
return $value > 0;
|
||||
}
|
||||
);
|
||||
|
||||
if (\count($ids) === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$qb = $this->createQueryBuilder('a');
|
||||
$qb
|
||||
->where($qb->expr()->in('a.id', ':id'))
|
||||
->setParameter('id', $activityIds)
|
||||
->setParameter('id', $ids)
|
||||
;
|
||||
|
||||
$activities = $qb->getQuery()->getResult();
|
||||
|
||||
$loader = new ActivityLoader($qb->getEntityManager(), true);
|
||||
$loader->loadResults($activities);
|
||||
|
||||
return $activities;
|
||||
return $this->getActivities($this->prepareActivityQuery($qb->getQuery()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function saveActivity(Activity $activity)
|
||||
public function saveActivity(Activity $activity): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($activity);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|bool $visible
|
||||
* @return int
|
||||
*/
|
||||
public function countActivity($visible = null): int
|
||||
public function countActivity(?bool $visible = null): int
|
||||
{
|
||||
if (null !== $visible) {
|
||||
return $this->count(['visible' => (bool) $visible]);
|
||||
return $this->count(['visible' => $visible]);
|
||||
}
|
||||
|
||||
return $this->count([]);
|
||||
@@ -157,8 +159,7 @@ class ActivityRepository extends EntityRepository
|
||||
/**
|
||||
* Returns a query builder that is used for ActivityType and your own 'query_builder' option.
|
||||
*
|
||||
* @param ActivityFormTypeQuery $query
|
||||
* @return QueryBuilder
|
||||
* @internal
|
||||
*/
|
||||
public function getQueryBuilderForFormType(ActivityFormTypeQuery $query): QueryBuilder
|
||||
{
|
||||
@@ -252,15 +253,17 @@ class ActivityRepository extends EntityRepository
|
||||
|
||||
private function getQueryBuilderForQuery(ActivityQuery $query): QueryBuilder
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb = $this->createQueryBuilder('a');
|
||||
|
||||
$qb
|
||||
->select('a')
|
||||
->from(Activity::class, 'a')
|
||||
->leftJoin('a.project', 'p')
|
||||
->leftJoin('p.customer', 'c')
|
||||
;
|
||||
|
||||
if (\count($query->getActivityIds()) > 0) {
|
||||
$qb->andWhere($qb->expr()->in('a.id', ':id'))->setParameter('id', $query->getActivityIds());
|
||||
}
|
||||
|
||||
foreach ($query->getOrderGroups() as $orderBy => $order) {
|
||||
switch ($orderBy) {
|
||||
case 'project':
|
||||
@@ -356,6 +359,9 @@ class ActivityRepository extends EntityRepository
|
||||
return ['a.name', 'a.comment', 'a.number'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function countActivitiesForQuery(ActivityQuery $query): int
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
@@ -366,7 +372,7 @@ class ActivityRepository extends EntityRepository
|
||||
->select($qb->expr()->countDistinct('a.id'))
|
||||
;
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
return (int) $qb->getQuery()->getSingleScalarResult(); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
public function getPagerfantaForQuery(ActivityQuery $query): Pagination
|
||||
@@ -374,33 +380,41 @@ class ActivityRepository extends EntityRepository
|
||||
return new Pagination($this->getPaginatorForQuery($query), $query);
|
||||
}
|
||||
|
||||
protected function getPaginatorForQuery(ActivityQuery $query): PaginatorInterface
|
||||
/**
|
||||
* @return PaginatorInterface<Activity>
|
||||
*/
|
||||
private function getPaginatorForQuery(ActivityQuery $activityQuery): PaginatorInterface
|
||||
{
|
||||
$counter = $this->countActivitiesForQuery($query);
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
$counter = $this->countActivitiesForQuery($activityQuery);
|
||||
$query = $this->createActivityQuery($activityQuery);
|
||||
|
||||
return new LoaderPaginator(new ActivityLoader($qb->getEntityManager()), $qb, $counter);
|
||||
return new LoaderQueryPaginator(new ActivityLoader($this->getEntityManager()), $query, $counter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ActivityQuery $query
|
||||
* @return Activity[]
|
||||
*/
|
||||
public function getActivitiesForQuery(ActivityQuery $query): iterable
|
||||
public function getActivitiesForQuery(ActivityQuery $query): array
|
||||
{
|
||||
// this is using the paginator internally, as it will load all joined entities into the working unit
|
||||
// do not "optimize" to use the query directly, as it would results in hundreds of additional lazy queries
|
||||
$paginator = $this->getPaginatorForQuery($query);
|
||||
|
||||
return $paginator->getAll();
|
||||
return $this->getActivities($this->createActivityQuery($query));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $delete
|
||||
* @param Activity|null $replace
|
||||
* @throws \Doctrine\ORM\Exception\ORMException
|
||||
* @param Query<Activity> $query
|
||||
* @return Activity[]
|
||||
*/
|
||||
public function deleteActivity(Activity $delete, ?Activity $replace = null)
|
||||
public function getActivities(Query $query): array
|
||||
{
|
||||
/** @var array<Activity> $activities */
|
||||
$activities = $query->execute();
|
||||
|
||||
$loader = new ActivityLoader($this->getEntityManager());
|
||||
$loader->loadResults($activities);
|
||||
|
||||
return $activities;
|
||||
}
|
||||
|
||||
public function deleteActivity(Activity $delete, ?Activity $replace = null): void
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
@@ -425,4 +439,47 @@ class ActivityRepository extends EntityRepository
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Query<Activity>
|
||||
*/
|
||||
private function createActivityQuery(ActivityQuery $activityQuery): Query
|
||||
{
|
||||
$query = $this->getQueryBuilderForQuery($activityQuery)->getQuery();
|
||||
$query = $this->prepareActivityQuery($query);
|
||||
|
||||
foreach ($activityQuery->getHydrate() as $hydrate) {
|
||||
switch ($hydrate) {
|
||||
case ActivityQueryHydrate::TEAMS:
|
||||
// does not yet work, see https://github.com/doctrine/orm/pull/8391
|
||||
// $query->setFetchMode(Activity::class, 'teams', ClassMetadata::FETCH_EAGER);
|
||||
break;
|
||||
|
||||
case ActivityQueryHydrate::TEAM_MEMBER:
|
||||
// does not yet work, see https://github.com/doctrine/orm/issues/11254
|
||||
// $query->setFetchMode(Activity::class, 'teams', ClassMetadata::FETCH_EAGER);
|
||||
// $query->setFetchMode(Team::class, 'members', ClassMetadata::FETCH_EAGER);
|
||||
// $query->setFetchMode(TeamMember::class, 'user', ClassMetadata::FETCH_EAGER);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Query<Activity> $query
|
||||
* @return Query<Activity>
|
||||
*/
|
||||
public function prepareActivityQuery(Query $query): Query
|
||||
{
|
||||
$this->getEntityManager()->getConfiguration()->setEagerFetchBatchSize(300);
|
||||
|
||||
$query->setFetchMode(Activity::class, 'meta', ClassMetadata::FETCH_EAGER);
|
||||
$query->setFetchMode(Activity::class, 'project', ClassMetadata::FETCH_EAGER);
|
||||
|
||||
// $query->setFetchMode(Project::class, 'customer', ClassMetadata::FETCH_EAGER);
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,16 @@ use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Loader\CustomerLoader;
|
||||
use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\LoaderQueryPaginator;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\CustomerQueryHydrate;
|
||||
use App\Utils\Pagination;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Expr\Andx;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@@ -37,22 +39,28 @@ class CustomerRepository extends EntityRepository
|
||||
|
||||
/**
|
||||
* @param int[] $customerIDs
|
||||
* @return Customer[]
|
||||
* @return array<Customer>
|
||||
*/
|
||||
public function findByIds(array $customerIDs): array
|
||||
{
|
||||
$ids = array_filter(
|
||||
array_unique($customerIDs),
|
||||
function ($value) {
|
||||
return $value > 0;
|
||||
}
|
||||
);
|
||||
|
||||
if (\count($ids) === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$qb = $this->createQueryBuilder('c');
|
||||
$qb
|
||||
->where($qb->expr()->in('c.id', ':id'))
|
||||
->setParameter('id', $customerIDs)
|
||||
->setParameter('id', $ids)
|
||||
;
|
||||
|
||||
$customers = $qb->getQuery()->getResult();
|
||||
|
||||
$loader = new CustomerLoader($qb->getEntityManager(), true);
|
||||
$loader->loadResults($customers);
|
||||
|
||||
return $customers;
|
||||
return $this->getCustomers($this->prepareCustomerQuery($qb->getQuery()), new CustomerQuery());
|
||||
}
|
||||
|
||||
public function saveCustomer(Customer $customer): void
|
||||
@@ -120,6 +128,8 @@ class CustomerRepository extends EntityRepository
|
||||
|
||||
/**
|
||||
* Returns a query builder that is used for CustomerType and your own 'query_builder' option.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getQueryBuilderForFormType(CustomerFormTypeQuery $query): QueryBuilder
|
||||
{
|
||||
@@ -163,18 +173,14 @@ class CustomerRepository extends EntityRepository
|
||||
|
||||
private function getQueryBuilderForQuery(CustomerQuery $query): QueryBuilder
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb = $this->createQueryBuilder('c');
|
||||
|
||||
$qb
|
||||
->select('c')
|
||||
->from(Customer::class, 'c')
|
||||
;
|
||||
if (\count($query->getCustomerIds()) > 0) {
|
||||
$qb->andWhere($qb->expr()->in('c.id', ':id'))->setParameter('id', $query->getCustomerIds());
|
||||
}
|
||||
|
||||
if ($query->getCountry() !== null) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->eq('c.country', ':country'))
|
||||
->setParameter('country', $query->getCountry())
|
||||
;
|
||||
$qb->andWhere($qb->expr()->eq('c.country', ':country'))->setParameter('country', $query->getCountry());
|
||||
}
|
||||
|
||||
foreach ($query->getOrderGroups() as $orderBy => $order) {
|
||||
@@ -190,11 +196,9 @@ class CustomerRepository extends EntityRepository
|
||||
}
|
||||
|
||||
if ($query->isShowVisible()) {
|
||||
$qb->andWhere($qb->expr()->eq('c.visible', ':visible'));
|
||||
$qb->setParameter('visible', true, ParameterType::BOOLEAN);
|
||||
$qb->andWhere($qb->expr()->eq('c.visible', ':visible'))->setParameter('visible', true, ParameterType::BOOLEAN);
|
||||
} elseif ($query->isShowHidden()) {
|
||||
$qb->andWhere($qb->expr()->eq('c.visible', ':visible'));
|
||||
$qb->setParameter('visible', false, ParameterType::BOOLEAN);
|
||||
$qb->andWhere($qb->expr()->eq('c.visible', ':visible'))->setParameter('visible', false, ParameterType::BOOLEAN);
|
||||
}
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getCurrentUser(), $query->getTeams());
|
||||
@@ -227,6 +231,10 @@ class CustomerRepository extends EntityRepository
|
||||
return new Pagination($this->getPaginatorForQuery($query), $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME make this private and remove the widget that this currently uses
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function countCustomersForQuery(CustomerQuery $query): int
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
@@ -237,27 +245,81 @@ class CustomerRepository extends EntityRepository
|
||||
->select($qb->expr()->countDistinct('c.id'))
|
||||
;
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
return (int) $qb->getQuery()->getSingleScalarResult(); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
protected function getPaginatorForQuery(CustomerQuery $query): PaginatorInterface
|
||||
/**
|
||||
* @return PaginatorInterface<Customer>
|
||||
*/
|
||||
private function getPaginatorForQuery(CustomerQuery $customerQuery): PaginatorInterface
|
||||
{
|
||||
$counter = $this->countCustomersForQuery($query);
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
$counter = $this->countCustomersForQuery($customerQuery);
|
||||
$query = $this->createCustomerQuery($customerQuery);
|
||||
|
||||
return new LoaderPaginator(new CustomerLoader($qb->getEntityManager()), $qb, $counter);
|
||||
return new LoaderQueryPaginator(new CustomerLoader($this->getEntityManager(), $customerQuery), $query, $counter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Query<Customer>
|
||||
*/
|
||||
private function createCustomerQuery(CustomerQuery $customerQuery): Query
|
||||
{
|
||||
$query = $this->getQueryBuilderForQuery($customerQuery)->getQuery();
|
||||
$query = $this->prepareCustomerQuery($query);
|
||||
|
||||
foreach ($customerQuery->getHydrate() as $hydrate) {
|
||||
switch ($hydrate) {
|
||||
case CustomerQueryHydrate::TEAMS:
|
||||
// does not yet work, see https://github.com/doctrine/orm/pull/8391
|
||||
// $query->setFetchMode(Customer::class, 'teams', ClassMetadata::FETCH_EAGER);
|
||||
break;
|
||||
|
||||
case CustomerQueryHydrate::TEAM_MEMBER:
|
||||
// does not yet work, see https://github.com/doctrine/orm/issues/11254
|
||||
// $query->setFetchMode(Customer::class, 'teams', ClassMetadata::FETCH_EAGER);
|
||||
// $query->setFetchMode(Team::class, 'members', ClassMetadata::FETCH_EAGER);
|
||||
// $query->setFetchMode(TeamMember::class, 'user', ClassMetadata::FETCH_EAGER);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Query<Customer> $query
|
||||
* @return Query<Customer>
|
||||
*/
|
||||
public function prepareCustomerQuery(Query $query): Query
|
||||
{
|
||||
$this->getEntityManager()->getConfiguration()->setEagerFetchBatchSize(300);
|
||||
|
||||
$query->setFetchMode(Customer::class, 'meta', ClassMetadata::FETCH_EAGER);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Customer[]
|
||||
*/
|
||||
public function getCustomersForQuery(CustomerQuery $query): iterable
|
||||
public function getCustomersForQuery(CustomerQuery $customerQuery): array
|
||||
{
|
||||
// this is using the paginator internally, as it will load all joined entities into the working unit
|
||||
// do not "optimize" to use the query directly, as it would results in hundreds of additional lazy queries
|
||||
$paginator = $this->getPaginatorForQuery($query);
|
||||
return $this->getCustomers($this->createCustomerQuery($customerQuery), $customerQuery);
|
||||
}
|
||||
|
||||
return $paginator->getAll();
|
||||
/**
|
||||
* @param Query<Customer> $query
|
||||
* @return Customer[]
|
||||
*/
|
||||
public function getCustomers(Query $query, CustomerQuery $customerQuery): array
|
||||
{
|
||||
/** @var array<Customer> $customers */
|
||||
$customers = $query->execute();
|
||||
|
||||
$loader = new CustomerLoader($this->getEntityManager(), $customerQuery);
|
||||
$loader->loadResults($customers);
|
||||
|
||||
return $customers;
|
||||
}
|
||||
|
||||
public function deleteCustomer(Customer $delete, ?Customer $replace = null): void
|
||||
|
||||
@@ -14,12 +14,13 @@ use App\Entity\Invoice;
|
||||
use App\Entity\InvoiceMeta;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Loader\InvoiceLoader;
|
||||
use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Paginator\QueryPaginator;
|
||||
use App\Repository\Query\InvoiceArchiveQuery;
|
||||
use App\Utils\Pagination;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
@@ -254,7 +255,10 @@ class InvoiceRepository extends EntityRepository
|
||||
return ['i.comment', 'customer.name', 'customer.company'];
|
||||
}
|
||||
|
||||
public function countInvoicesForQuery(InvoiceArchiveQuery $query): int
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
private function countInvoicesForQuery(InvoiceArchiveQuery $query): int
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
$qb
|
||||
@@ -264,7 +268,7 @@ class InvoiceRepository extends EntityRepository
|
||||
->select($qb->expr()->countDistinct('i.id'))
|
||||
;
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
return (int) $qb->getQuery()->getSingleScalarResult(); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,23 +277,38 @@ class InvoiceRepository extends EntityRepository
|
||||
*/
|
||||
public function getInvoicesForQuery(InvoiceArchiveQuery $query): iterable
|
||||
{
|
||||
// this is using the paginator internally, as it will load all joined entities into the working unit
|
||||
// do not "optimize" to use the query directly, as it would results in hundreds of additional lazy queries
|
||||
$paginator = $this->getPaginatorForQuery($query);
|
||||
|
||||
return $paginator->getAll();
|
||||
return $this->createInvoiceQuery($query)->execute(); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
protected function getPaginatorForQuery(InvoiceArchiveQuery $query): PaginatorInterface
|
||||
/**
|
||||
* @return PaginatorInterface<Invoice>
|
||||
*/
|
||||
private function getPaginatorForQuery(InvoiceArchiveQuery $query): PaginatorInterface
|
||||
{
|
||||
$counter = $this->countInvoicesForQuery($query);
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
$query = $this->createInvoiceQuery($query);
|
||||
|
||||
return new LoaderPaginator(new InvoiceLoader($qb->getEntityManager()), $qb, $counter);
|
||||
return new QueryPaginator($query, $counter);
|
||||
}
|
||||
|
||||
public function getPagerfantaForQuery(InvoiceArchiveQuery $query): Pagination
|
||||
{
|
||||
return new Pagination($this->getPaginatorForQuery($query), $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Query<Invoice>
|
||||
*/
|
||||
private function createInvoiceQuery(InvoiceArchiveQuery $invoiceArchiveQuery): Query
|
||||
{
|
||||
$query = $this->getQueryBuilderForQuery($invoiceArchiveQuery)->getQuery();
|
||||
|
||||
$this->getEntityManager()->getConfiguration()->setEagerFetchBatchSize(300);
|
||||
|
||||
$query->setFetchMode(Invoice::class, 'meta', ClassMetadata::FETCH_EAGER);
|
||||
$query->setFetchMode(Invoice::class, 'user', ClassMetadata::FETCH_EAGER);
|
||||
$query->setFetchMode(Invoice::class, 'customer', ClassMetadata::FETCH_EAGER);
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@ namespace App\Repository;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Paginator\QueryBuilderPaginator;
|
||||
use App\Repository\Paginator\QueryPaginator;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Utils\Pagination;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
@@ -49,12 +50,16 @@ class InvoiceTemplateRepository extends EntityRepository
|
||||
return $qb;
|
||||
}
|
||||
|
||||
protected function getPaginatorForQuery(BaseQuery $query): PaginatorInterface
|
||||
/**
|
||||
* @return PaginatorInterface<InvoiceTemplate>
|
||||
*/
|
||||
private function getPaginatorForQuery(BaseQuery $baseQuery): PaginatorInterface
|
||||
{
|
||||
$counter = $this->countTemplatesForQuery($query);
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
$counter = $this->countTemplatesForQuery($baseQuery);
|
||||
/** @var Query<InvoiceTemplate> $query */
|
||||
$query = $this->getQueryBuilderForQuery($baseQuery)->getQuery();
|
||||
|
||||
return new QueryBuilderPaginator($qb, $counter);
|
||||
return new QueryPaginator($query, $counter);
|
||||
}
|
||||
|
||||
public function getPagerfantaForQuery(BaseQuery $query): Pagination
|
||||
@@ -62,6 +67,9 @@ class InvoiceTemplateRepository extends EntityRepository
|
||||
return new Pagination($this->getPaginatorForQuery($query), $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function countTemplatesForQuery(BaseQuery $query): int
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
@@ -71,7 +79,7 @@ class InvoiceTemplateRepository extends EntityRepository
|
||||
->select($qb->expr()->countDistinct('t.id'))
|
||||
;
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
return (int) $qb->getQuery()->getSingleScalarResult(); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
public function saveTemplate(InvoiceTemplate $template): void
|
||||
|
||||
@@ -12,65 +12,44 @@ namespace App\Repository\Loader;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @implements LoaderInterface<Activity>
|
||||
*/
|
||||
final class ActivityLoader implements LoaderInterface
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $entityManager, private bool $fullyHydrated = false)
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|Activity> $results
|
||||
* @param array<Activity> $results
|
||||
*/
|
||||
public function loadResults(array $results): void
|
||||
{
|
||||
if (empty($results)) {
|
||||
if (\count($results) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ids = array_map(function ($activity) {
|
||||
if ($activity instanceof Activity) {
|
||||
// make sure that this potential doctrine proxy is initialized and filled with all data
|
||||
$activity->getName();
|
||||
$activityIds = array_filter(array_unique(array_map(function (Activity $activity) {
|
||||
// make sure that this potential doctrine proxy is initialized and filled with all data
|
||||
$activity->getName();
|
||||
// using reporting controller tests will show that error
|
||||
$activity->getProject()?->getName();
|
||||
|
||||
return $activity->getId();
|
||||
}
|
||||
|
||||
return $activity;
|
||||
}, $results);
|
||||
return $activity->getId();
|
||||
}, $results)), function ($value) { return $value !== null; });
|
||||
|
||||
$em = $this->entityManager;
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
/** @var Activity[] $activities */
|
||||
$activities = $qb->select('PARTIAL a.{id}', 'project')
|
||||
->from(Activity::class, 'a')
|
||||
->leftJoin('a.project', 'project')
|
||||
->andWhere($qb->expr()->isNotNull('a.project'))
|
||||
->andWhere($qb->expr()->in('a.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL a.{id}', 'meta')
|
||||
->from(Activity::class, 'a')
|
||||
->leftJoin('a.meta', 'meta')
|
||||
->andWhere($qb->expr()->in('a.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
$projectIds = array_filter(array_unique(array_map(function (Activity $activity) {
|
||||
return $activity->getProject()?->getId();
|
||||
}, $results)), function ($value) { return $value !== null; });
|
||||
|
||||
// global activities don't have projects
|
||||
if (!empty($activities)) {
|
||||
$projectIds = array_unique(array_map(function (Activity $activity) {
|
||||
if (null === $activity->getProject()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $activity->getProject()->getId();
|
||||
}, $activities));
|
||||
|
||||
if (\count($projectIds) > 0) {
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL project.{id}', 'customer')
|
||||
->from(Project::class, 'project')
|
||||
@@ -79,14 +58,6 @@ final class ActivityLoader implements LoaderInterface
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$customerIds = array_unique(array_map(function (Activity $activity) {
|
||||
if (null === $activity->getProject()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $activity->getProject()->getCustomer()->getId();
|
||||
}, $activities));
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL project.{id}', 'teams')
|
||||
->from(Project::class, 'project')
|
||||
@@ -95,44 +66,28 @@ final class ActivityLoader implements LoaderInterface
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL customer.{id}', 'teams')
|
||||
->from(Customer::class, 'customer')
|
||||
->leftJoin('customer.teams', 'teams')
|
||||
->andWhere($qb->expr()->in('customer.id', $customerIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
$customerIds = array_filter(array_unique(array_map(function (Activity $activity) {
|
||||
return $activity->getProject()?->getCustomer()?->getId();
|
||||
}, $results)), function ($value) { return $value !== null; });
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL a.{id}', 'teams')
|
||||
->from(Activity::class, 'a')
|
||||
->leftJoin('a.teams', 'teams')
|
||||
->andWhere($qb->expr()->in('a.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
// do not load team members or leads by default, because they will only be used on detail pages
|
||||
// and there is no benefit in adding multiple queries for most requests when they are only needed in one place
|
||||
if ($this->fullyHydrated) {
|
||||
$teamIds = [];
|
||||
foreach ($activities as $activity) {
|
||||
foreach ($activity->getTeams() as $team) {
|
||||
$teamIds[] = $team->getId();
|
||||
}
|
||||
}
|
||||
$teamIds = array_unique($teamIds);
|
||||
|
||||
if (\count($teamIds) > 0) {
|
||||
if (\count($customerIds) > 0) {
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL team.{id}', 'members', 'user')
|
||||
->from(Team::class, 'team')
|
||||
->leftJoin('team.members', 'members')
|
||||
->leftJoin('members.user', 'user')
|
||||
->andWhere($qb->expr()->in('team.id', $teamIds))
|
||||
$qb->select('PARTIAL customer.{id}', 'teams')
|
||||
->from(Customer::class, 'customer')
|
||||
->leftJoin('customer.teams', 'teams')
|
||||
->andWhere($qb->expr()->in('customer.id', $customerIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
// required on "Activity listing" page for non super-admins
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL a.{id}', 'teams')
|
||||
->from(Activity::class, 'a')
|
||||
->leftJoin('a.teams', 'teams')
|
||||
->andWhere($qb->expr()->in('a.id', $activityIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,58 +11,73 @@ namespace App\Repository\Loader;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Team;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\CustomerQueryHydrate;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @implements LoaderInterface<Customer>
|
||||
*/
|
||||
final class CustomerLoader implements LoaderInterface
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $entityManager, private bool $fullyHydrated = false)
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly CustomerQuery $query
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|Customer> $results
|
||||
* @param array<Customer> $results
|
||||
*/
|
||||
public function loadResults(array $results): void
|
||||
{
|
||||
if (empty($results)) {
|
||||
if (\count($results) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ids = array_map(function ($customer) {
|
||||
if ($customer instanceof Customer) {
|
||||
// make sure that this potential doctrine proxy is initialized and filled with all data
|
||||
$customer->getName();
|
||||
$customerIds = array_filter(array_unique(array_map(function (Customer $customer) {
|
||||
// make sure that this potential doctrine proxy is initialized and filled with all data
|
||||
$customer->getName();
|
||||
|
||||
return $customer->getId();
|
||||
return $customer->getId();
|
||||
}, $results)), function ($value) { return $value !== null; });
|
||||
|
||||
$hydrateTeams = false;
|
||||
$hydrateTeamMembers = false;
|
||||
|
||||
foreach ($this->query->getHydrate() as $hydrate) {
|
||||
switch ($hydrate) {
|
||||
case CustomerQueryHydrate::TEAMS:
|
||||
$hydrateTeams = true;
|
||||
break;
|
||||
case CustomerQueryHydrate::TEAM_MEMBER:
|
||||
$hydrateTeams = true;
|
||||
$hydrateTeamMembers = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $customer;
|
||||
}, $results);
|
||||
if (!$hydrateTeams) {
|
||||
return;
|
||||
}
|
||||
|
||||
$em = $this->entityManager;
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
/** @var Customer[] $customers */
|
||||
$customers = $qb->select('PARTIAL c.{id}', 'meta')
|
||||
->from(Customer::class, 'c')
|
||||
->leftJoin('c.meta', 'meta')
|
||||
->andWhere($qb->expr()->in('c.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
// required where we need to check team permissions, e.g. "Customer listing"
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL c.{id}', 'teams')
|
||||
->from(Customer::class, 'c')
|
||||
->leftJoin('c.teams', 'teams')
|
||||
->andWhere($qb->expr()->in('c.id', $ids))
|
||||
->andWhere($qb->expr()->in('c.id', $customerIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
// do not load team members or leads by default, because they will only be used on detail pages
|
||||
// and there is no benefit in adding multiple queries for most requests when they are only needed in one place
|
||||
if ($this->fullyHydrated) {
|
||||
if ($hydrateTeamMembers) {
|
||||
$teamIds = [];
|
||||
foreach ($customers as $customer) {
|
||||
foreach ($results as $customer) {
|
||||
foreach ($customer->getTeams() as $team) {
|
||||
$teamIds[] = $team->getId();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
namespace App\Repository\Loader;
|
||||
|
||||
/**
|
||||
* @deprecated use QueryBuilderPaginator instead
|
||||
* @implements LoaderInterface<mixed>
|
||||
*/
|
||||
final class DefaultLoader implements LoaderInterface
|
||||
{
|
||||
public function loadResults(array $results): void
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
<?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\Loader;
|
||||
|
||||
use App\Entity\Invoice;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
final class InvoiceLoader implements LoaderInterface
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $entityManager)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|Invoice> $results
|
||||
*/
|
||||
public function loadResults(array $results): void
|
||||
{
|
||||
if (empty($results)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ids = array_map(function ($invoice) {
|
||||
if ($invoice instanceof Invoice) {
|
||||
// make sure that this potential doctrine proxy is initialized and filled with all data
|
||||
$invoice->getInvoiceNumber();
|
||||
|
||||
return $invoice->getId();
|
||||
}
|
||||
|
||||
return $invoice;
|
||||
}, $results);
|
||||
|
||||
$em = $this->entityManager;
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL i.{id}', 'customer')
|
||||
->from(Invoice::class, 'i')
|
||||
->leftJoin('i.customer', 'customer')
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL i.{id}', 'user')
|
||||
->from(Invoice::class, 'i')
|
||||
->leftJoin('i.user', 'user')
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL i.{id}', 'meta')
|
||||
->from(Invoice::class, 'i')
|
||||
->leftJoin('i.meta', 'meta')
|
||||
->andWhere($qb->expr()->in('i.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,15 @@
|
||||
|
||||
namespace App\Repository\Loader;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
interface LoaderInterface
|
||||
{
|
||||
/**
|
||||
* Prepares the given database results, to prevent lazy loading.
|
||||
*
|
||||
* @param array $results
|
||||
* @param array<array-key, T> $results
|
||||
*/
|
||||
public function loadResults(array $results): void;
|
||||
}
|
||||
|
||||
@@ -14,63 +14,50 @@ use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @implements LoaderInterface<Project>
|
||||
*/
|
||||
final class ProjectLoader implements LoaderInterface
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $entityManager, private bool $hydrateTeamMembers = false, private bool $hydrateTeams = true, private bool $hydrateMeta = true)
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly bool $hydrateTeamMembers = false,
|
||||
private readonly bool $hydrateTeams = true
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|Project> $results
|
||||
* @param array<Project> $results
|
||||
*/
|
||||
public function loadResults(array $results): void
|
||||
{
|
||||
if (empty($results)) {
|
||||
if (\count($results) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ids = array_map(function ($project) {
|
||||
if ($project instanceof Project) {
|
||||
// make sure that this potential doctrine proxy is initialized and filled with all data
|
||||
$project->getName();
|
||||
$projectIds = array_filter(array_unique(array_map(function (Project $project) {
|
||||
// make sure that this potential doctrine proxy is initialized and filled with all data
|
||||
$project->getName();
|
||||
// using reporting controller tests will show that error
|
||||
$project->getCustomer()?->getName();
|
||||
|
||||
return $project->getId();
|
||||
}
|
||||
|
||||
return $project;
|
||||
}, $results);
|
||||
return $project->getId();
|
||||
}, $results)), function ($value) { return $value !== null; });
|
||||
|
||||
$em = $this->entityManager;
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
/** @var Project[] $projects */
|
||||
$projects = $qb->select('PARTIAL project.{id}', 'customer')
|
||||
->from(Project::class, 'project')
|
||||
->leftJoin('project.customer', 'customer')
|
||||
->andWhere($qb->expr()->in('project.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$customerIds = array_unique(array_map(function (Project $project) {
|
||||
return $project->getCustomer()->getId();
|
||||
}, $projects));
|
||||
|
||||
if ($this->hydrateMeta) {
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL project.{id}', 'meta')
|
||||
->from(Project::class, 'project')
|
||||
->leftJoin('project.meta', 'meta')
|
||||
->andWhere($qb->expr()->in('project.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
|
||||
if ($this->hydrateTeams) {
|
||||
$customerIds = array_filter(array_unique(array_map(function (Project $project) {
|
||||
return $project->getCustomer()->getId();
|
||||
}, $results)), function ($value) { return $value !== null; });
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL project.{id}', 'teams')
|
||||
->from(Project::class, 'project')
|
||||
->leftJoin('project.teams', 'teams')
|
||||
->andWhere($qb->expr()->in('project.id', $ids))
|
||||
->andWhere($qb->expr()->in('project.id', $projectIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
@@ -87,7 +74,7 @@ final class ProjectLoader implements LoaderInterface
|
||||
// and there is no benefit in adding multiple queries for most requests when they are only needed in one place
|
||||
if ($this->hydrateTeamMembers) {
|
||||
$teamIds = [];
|
||||
foreach ($projects as $project) {
|
||||
foreach ($results as $project) {
|
||||
foreach ($project->getTeams() as $team) {
|
||||
$teamIds[] = $team->getId();
|
||||
}
|
||||
|
||||
@@ -9,52 +9,77 @@
|
||||
|
||||
namespace App\Repository\Loader;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @implements LoaderInterface<Team>
|
||||
*/
|
||||
final class TeamLoader implements LoaderInterface
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $entityManager)
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly bool $loadCustomer = false
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|Team> $results
|
||||
* @param array<Team> $results
|
||||
*/
|
||||
public function loadResults(array $results): void
|
||||
{
|
||||
if (empty($results)) {
|
||||
if (\count($results) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ids = array_map(function ($team) {
|
||||
if ($team instanceof Team) {
|
||||
// make sure that this potential doctrine proxy is initialized and filled with all data
|
||||
$team->getName();
|
||||
$teamIds = array_filter(array_unique(array_map(function (Team $team) {
|
||||
// make sure that this potential doctrine proxy is initialized and filled with all data
|
||||
$team->getName();
|
||||
|
||||
return $team->getId();
|
||||
}
|
||||
|
||||
return $team;
|
||||
}, $results);
|
||||
return $team->getId();
|
||||
}, $results)), function ($value) { return $value !== null; });
|
||||
|
||||
$em = $this->entityManager;
|
||||
|
||||
// required wherever users are shown, e.g. on "Custom details" page
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL team.{id}', 'members', 'user')
|
||||
->from(Team::class, 'team')
|
||||
->leftJoin('team.members', 'members')
|
||||
->leftJoin('members.user', 'user')
|
||||
->andWhere($qb->expr()->in('team.id', $ids))
|
||||
->andWhere($qb->expr()->in('team.id', $teamIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
// used in UserTeamProjects widget
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL team.{id}', 'projects')
|
||||
/** @var array<Team> $teams */
|
||||
$teams = $qb->select('PARTIAL team.{id}', 'projects')
|
||||
->from(Team::class, 'team')
|
||||
->leftJoin('team.projects', 'projects')
|
||||
->andWhere($qb->expr()->in('team.id', $ids))
|
||||
->andWhere($qb->expr()->in('team.id', $teamIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$projectIds = [];
|
||||
foreach ($results as $team) {
|
||||
foreach ($team->getProjects() as $project) {
|
||||
$projectIds[] = $project->getId();
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->loadCustomer) {
|
||||
// used in UserTeamProjects widget
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL project.{id}', 'customer')
|
||||
->from(Project::class, 'project')
|
||||
->leftJoin('project.customer', 'customer')
|
||||
->andWhere($qb->expr()->in('project.id', $projectIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,46 +15,40 @@ use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @implements LoaderInterface<Timesheet>
|
||||
*/
|
||||
final class TimesheetLoader implements LoaderInterface
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $entityManager, private bool $fullyHydrated = false, private bool $basicHydrated = true)
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly bool $fullyHydrated = false
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|Timesheet> $results
|
||||
* @param array<Timesheet> $results
|
||||
*/
|
||||
public function loadResults(array $results): void
|
||||
{
|
||||
if (empty($results)) {
|
||||
if (\count($results) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ids = array_map(function ($timesheet) {
|
||||
if ($timesheet instanceof Timesheet) {
|
||||
// make sure that this potential doctrine proxy is initialized and filled with all data
|
||||
$timesheet->getType();
|
||||
$ids = array_filter(array_unique(array_map(function (Timesheet $timesheet) {
|
||||
// make sure that this potential doctrine proxy is initialized and filled with all data
|
||||
$timesheet->getType();
|
||||
|
||||
return $timesheet->getId();
|
||||
}
|
||||
|
||||
return $timesheet;
|
||||
}, $results);
|
||||
return $timesheet->getId();
|
||||
}, $results)), function ($value) { return $value !== null; });
|
||||
|
||||
$em = $this->entityManager;
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
/** @var array<Timesheet> $timesheets */
|
||||
$timesheets = $qb->select('PARTIAL t.{id}', 'project')
|
||||
->from(Timesheet::class, 't')
|
||||
->leftJoin('t.project', 'project')
|
||||
->andWhere($qb->expr()->in('t.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$projectIds = array_map(function ($timesheet) {
|
||||
return $timesheet->getProject()->getId();
|
||||
}, $timesheets);
|
||||
$projectIds = array_filter(array_unique(array_map(function (Timesheet $timesheet) {
|
||||
return $timesheet->getProject()?->getId();
|
||||
}, $results)), function ($value) { return $value !== null; });
|
||||
|
||||
if ($this->fullyHydrated) {
|
||||
$qb = $em->createQueryBuilder();
|
||||
@@ -76,9 +70,9 @@ final class TimesheetLoader implements LoaderInterface
|
||||
->execute();
|
||||
|
||||
if ($this->fullyHydrated) {
|
||||
$customerIds = array_map(function ($project) {
|
||||
$customerIds = array_filter(array_unique(array_map(function (Project $project) {
|
||||
return $project->getCustomer()->getId();
|
||||
}, $projects);
|
||||
}, $projects)), function ($value) { return $value !== null; });
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL c.{id}', 'meta')
|
||||
@@ -89,18 +83,10 @@ final class TimesheetLoader implements LoaderInterface
|
||||
->execute();
|
||||
}
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL t.{id}', 'activity')
|
||||
->from(Timesheet::class, 't')
|
||||
->leftJoin('t.activity', 'activity')
|
||||
->andWhere($qb->expr()->in('t.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
if ($this->fullyHydrated) {
|
||||
$activityIds = array_filter(array_map(function (Timesheet $timesheet) {
|
||||
return $timesheet->getActivity()?->getId();
|
||||
}, $timesheets), function ($id): bool {
|
||||
}, $results), function ($id): bool {
|
||||
return $id !== null;
|
||||
});
|
||||
|
||||
@@ -113,30 +99,12 @@ final class TimesheetLoader implements LoaderInterface
|
||||
->execute();
|
||||
}
|
||||
|
||||
if ($this->basicHydrated) {
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL t.{id}', 'user')
|
||||
->from(Timesheet::class, 't')
|
||||
->leftJoin('t.user', 'user')
|
||||
->andWhere($qb->expr()->in('t.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL t.{id}', 'tags')
|
||||
->from(Timesheet::class, 't')
|
||||
->leftJoin('t.tags', 'tags')
|
||||
->andWhere($qb->expr()->in('t.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL t.{id}', 'meta')
|
||||
->from(Timesheet::class, 't')
|
||||
->leftJoin('t.meta', 'meta')
|
||||
->andWhere($qb->expr()->in('t.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL t.{id}', 'tags')
|
||||
->from(Timesheet::class, 't')
|
||||
->leftJoin('t.tags', 'tags')
|
||||
->andWhere($qb->expr()->in('t.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,49 +13,54 @@ use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @implements LoaderInterface<User>
|
||||
*/
|
||||
final class UserLoader implements LoaderInterface
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $entityManager, private bool $fullyHydrated = false)
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly bool $fullyHydrated = false
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|User> $results
|
||||
* @param array<User> $results
|
||||
*/
|
||||
public function loadResults(array $results): void
|
||||
{
|
||||
if (empty($results)) {
|
||||
if (\count($results) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ids = array_map(function ($user) {
|
||||
if ($user instanceof User) {
|
||||
// make sure that this potential doctrine proxy is initialized and filled with all data
|
||||
$user->getDisplayName();
|
||||
$userIds = array_filter(array_unique(array_map(function (User $user) {
|
||||
// make sure that this potential doctrine proxy is initialized and filled with all data
|
||||
$user->getDisplayName();
|
||||
|
||||
return $user->getId();
|
||||
}
|
||||
|
||||
return $user;
|
||||
}, $results);
|
||||
return $user->getId();
|
||||
}, $results)), function ($value) { return $value !== null; });
|
||||
|
||||
$em = $this->entityManager;
|
||||
|
||||
// this is currently needed, as it does not work via the Doctrine eager fetch method
|
||||
// on user listing pages, if users are already in the unit of work from another load
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL user.{id}', 'preferences')
|
||||
->from(User::class, 'user')
|
||||
->leftJoin('user.preferences', 'preferences')
|
||||
->andWhere($qb->expr()->in('user.id', $userIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
/** @var User[] $users */
|
||||
$users = $qb->select('PARTIAL user.{id}', 'memberships', 'team')
|
||||
->from(User::class, 'user')
|
||||
->leftJoin('user.memberships', 'memberships')
|
||||
->leftJoin('memberships.team', 'team')
|
||||
->andWhere($qb->expr()->in('user.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL user.{id}', 'preferences')
|
||||
->from(User::class, 'user')
|
||||
->leftJoin('user.preferences', 'preferences')
|
||||
->andWhere($qb->expr()->in('user.id', $ids))
|
||||
->andWhere($qb->expr()->in('user.id', $userIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
|
||||
@@ -13,9 +13,21 @@ use App\Repository\Loader\LoaderInterface;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @implements PaginatorInterface<T>
|
||||
*/
|
||||
final class LoaderPaginator implements PaginatorInterface
|
||||
{
|
||||
public function __construct(private LoaderInterface $loader, private QueryBuilder $query, private int $results)
|
||||
/**
|
||||
* @param LoaderInterface<T> $loader
|
||||
* @param int<0, max> $results
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly LoaderInterface $loader,
|
||||
private readonly QueryBuilder $queryBuilder,
|
||||
private readonly int $results
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -25,12 +37,20 @@ final class LoaderPaginator implements PaginatorInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array-key, iterable<mixed>>
|
||||
* @return Query<null, T>
|
||||
*/
|
||||
private function getQuery(): Query
|
||||
{
|
||||
return $this->queryBuilder->getQuery(); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array-key, T>
|
||||
*/
|
||||
public function getSlice(int $offset, int $length): iterable
|
||||
{
|
||||
$query = $this->query
|
||||
->getQuery()
|
||||
/** @var Query<null, T> $query */
|
||||
$query = $this->getQuery()
|
||||
->setFirstResult($offset)
|
||||
->setMaxResults($length);
|
||||
|
||||
@@ -38,20 +58,24 @@ final class LoaderPaginator implements PaginatorInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Query<null, mixed> $query
|
||||
* @return iterable<array-key, iterable<mixed>>
|
||||
* @param Query<null, T> $query
|
||||
* @return array<array-key, T>
|
||||
*/
|
||||
private function getResults(Query $query)
|
||||
private function getResults(Query $query): array
|
||||
{
|
||||
/** @var array<array-key, T> $results */
|
||||
$results = $query->execute();
|
||||
|
||||
$this->loader->loadResults($results);
|
||||
|
||||
return $results; // @phpstan-ignore-line
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array-key, T>
|
||||
*/
|
||||
public function getAll(): iterable
|
||||
{
|
||||
return $this->getResults($this->query->getQuery());
|
||||
return $this->getResults($this->getQuery());
|
||||
}
|
||||
}
|
||||
|
||||
73
src/Repository/Paginator/LoaderQueryPaginator.php
Normal file
73
src/Repository/Paginator/LoaderQueryPaginator.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?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\Paginator;
|
||||
|
||||
use App\Repository\Loader\LoaderInterface;
|
||||
use Doctrine\ORM\Query;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @implements PaginatorInterface<T>
|
||||
*/
|
||||
final class LoaderQueryPaginator implements PaginatorInterface
|
||||
{
|
||||
/**
|
||||
* @param LoaderInterface<T> $loader
|
||||
* @param Query<T> $query
|
||||
* @param int<0, max> $results
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly LoaderInterface $loader,
|
||||
private readonly Query $query,
|
||||
private readonly int $results
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getNbResults(): int
|
||||
{
|
||||
return $this->results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array-key, T>
|
||||
*/
|
||||
public function getSlice(int $offset, int $length): iterable
|
||||
{
|
||||
/** @var Query<null, T> $query */
|
||||
$query = $this->query
|
||||
->setFirstResult($offset)
|
||||
->setMaxResults($length);
|
||||
|
||||
return $this->getResults($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Query<null, T> $query
|
||||
* @return iterable<array-key, T>
|
||||
*/
|
||||
private function getResults(Query $query): iterable
|
||||
{
|
||||
/** @var array<T> $results */
|
||||
$results = $query->execute();
|
||||
|
||||
$this->loader->loadResults($results);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array-key, T>
|
||||
*/
|
||||
public function getAll(): iterable
|
||||
{
|
||||
return $this->getResults($this->query);
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,15 @@ namespace App\Repository\Paginator;
|
||||
|
||||
use Pagerfanta\Adapter\AdapterInterface;
|
||||
|
||||
/**
|
||||
* @template-covariant T
|
||||
*/
|
||||
interface PaginatorInterface extends AdapterInterface
|
||||
{
|
||||
/**
|
||||
* Returns all available results without pagination.
|
||||
*
|
||||
* @return iterable
|
||||
* @return iterable<array-key, T>
|
||||
*/
|
||||
public function getAll(): iterable;
|
||||
}
|
||||
|
||||
@@ -12,9 +12,19 @@ namespace App\Repository\Paginator;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
* @deprecated use QueryPaginator instead
|
||||
* @implements PaginatorInterface<mixed>
|
||||
*/
|
||||
final class QueryBuilderPaginator implements PaginatorInterface
|
||||
{
|
||||
public function __construct(private QueryBuilder $query, private int $results)
|
||||
/**
|
||||
* @param int<0, max> $results
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly QueryBuilder $queryBuilder,
|
||||
private readonly int $results
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -24,11 +34,12 @@ final class QueryBuilderPaginator implements PaginatorInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array-key, iterable<mixed>>
|
||||
* @return iterable<array-key, mixed>
|
||||
*/
|
||||
public function getSlice(int $offset, int $length): iterable
|
||||
{
|
||||
$query = $this->query
|
||||
/** @var Query<null, mixed> $query */
|
||||
$query = $this->queryBuilder
|
||||
->getQuery()
|
||||
->setFirstResult($offset)
|
||||
->setMaxResults($length);
|
||||
@@ -38,15 +49,18 @@ final class QueryBuilderPaginator implements PaginatorInterface
|
||||
|
||||
/**
|
||||
* @param Query<null, mixed> $query
|
||||
* @return iterable<array-key, iterable<mixed>>
|
||||
* @return iterable<array-key, mixed>
|
||||
*/
|
||||
private function getResults(Query $query)
|
||||
private function getResults(Query $query): iterable
|
||||
{
|
||||
return $query->execute(); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array-key, mixed>
|
||||
*/
|
||||
public function getAll(): iterable
|
||||
{
|
||||
return $this->getResults($this->query->getQuery());
|
||||
return $this->getResults($this->queryBuilder->getQuery());
|
||||
}
|
||||
}
|
||||
|
||||
64
src/Repository/Paginator/QueryPaginator.php
Normal file
64
src/Repository/Paginator/QueryPaginator.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?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\Paginator;
|
||||
|
||||
use Doctrine\ORM\Query;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @implements PaginatorInterface<T>
|
||||
*/
|
||||
final class QueryPaginator implements PaginatorInterface
|
||||
{
|
||||
/**
|
||||
* @param Query<null, T> $query
|
||||
* @param int<0, max> $results
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly Query $query,
|
||||
private readonly int $results
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getNbResults(): int
|
||||
{
|
||||
return $this->results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array-key, T>
|
||||
*/
|
||||
public function getSlice(int $offset, int $length): iterable
|
||||
{
|
||||
$query = $this->query
|
||||
->setFirstResult($offset)
|
||||
->setMaxResults($length);
|
||||
|
||||
return $this->getResults($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Query<null, T> $query
|
||||
* @return iterable<array-key, T>
|
||||
*/
|
||||
private function getResults(Query $query): iterable
|
||||
{
|
||||
return $query->execute(); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array-key, T>
|
||||
*/
|
||||
public function getAll(): iterable
|
||||
{
|
||||
return $this->getResults($this->query);
|
||||
}
|
||||
}
|
||||
@@ -17,16 +17,18 @@ use App\Entity\Team;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Loader\ProjectLoader;
|
||||
use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\LoaderQueryPaginator;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\ProjectQueryHydrate;
|
||||
use App\Utils\Pagination;
|
||||
use DateTime;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Expr\Andx;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@@ -40,30 +42,31 @@ class ProjectRepository extends EntityRepository
|
||||
|
||||
/**
|
||||
* @param int[] $projectIds
|
||||
* @return Project[]
|
||||
* @return array<Project>
|
||||
*/
|
||||
public function findByIds(array $projectIds): array
|
||||
{
|
||||
$ids = array_filter(
|
||||
array_unique($projectIds),
|
||||
function ($value) {
|
||||
return $value > 0;
|
||||
}
|
||||
);
|
||||
|
||||
if (\count($ids) === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$qb = $this->createQueryBuilder('p');
|
||||
$qb
|
||||
->where($qb->expr()->in('p.id', ':id'))
|
||||
->setParameter('id', $projectIds)
|
||||
->setParameter('id', $ids)
|
||||
;
|
||||
|
||||
$projects = $qb->getQuery()->getResult();
|
||||
|
||||
$loader = new ProjectLoader($qb->getEntityManager(), true);
|
||||
$loader->loadResults($projects);
|
||||
|
||||
return $projects;
|
||||
return $this->getProjects($this->prepareProjectQuery($qb->getQuery()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function saveProject(Project $project)
|
||||
public function saveProject(Project $project): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($project);
|
||||
@@ -71,13 +74,12 @@ class ProjectRepository extends EntityRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|bool $visible
|
||||
* @return int
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function countProject($visible = null): int
|
||||
public function countProject(?bool $visible = null): int
|
||||
{
|
||||
if (null !== $visible) {
|
||||
return $this->count(['visible' => (bool) $visible]);
|
||||
return $this->count(['visible' => $visible]);
|
||||
}
|
||||
|
||||
return $this->count([]);
|
||||
@@ -91,7 +93,7 @@ class ProjectRepository extends EntityRepository
|
||||
}
|
||||
}
|
||||
|
||||
public function getPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): Andx
|
||||
private function getPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): Andx
|
||||
{
|
||||
$andX = $qb->expr()->andX();
|
||||
|
||||
@@ -140,8 +142,7 @@ class ProjectRepository extends EntityRepository
|
||||
/**
|
||||
* Returns a query builder that is used for ProjectType and your own 'query_builder' option.
|
||||
*
|
||||
* @param ProjectFormTypeQuery $query
|
||||
* @return QueryBuilder
|
||||
* @internal
|
||||
*/
|
||||
public function getQueryBuilderForFormType(ProjectFormTypeQuery $query): QueryBuilder
|
||||
{
|
||||
@@ -213,6 +214,10 @@ class ProjectRepository extends EntityRepository
|
||||
->leftJoin('p.customer', 'c')
|
||||
;
|
||||
|
||||
if (\count($query->getProjectIds()) > 0) {
|
||||
$qb->andWhere($qb->expr()->in('p.id', ':id'))->setParameter('id', $query->getProjectIds());
|
||||
}
|
||||
|
||||
foreach ($query->getOrderGroups() as $orderBy => $order) {
|
||||
switch ($orderBy) {
|
||||
case 'customer':
|
||||
@@ -332,6 +337,9 @@ class ProjectRepository extends EntityRepository
|
||||
return $and;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function countProjectsForQuery(ProjectQuery $query): int
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
@@ -342,7 +350,7 @@ class ProjectRepository extends EntityRepository
|
||||
->select($qb->expr()->countDistinct('p.id'))
|
||||
;
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
return (int) $qb->getQuery()->getSingleScalarResult(); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
public function getPagerfantaForQuery(ProjectQuery $query): Pagination
|
||||
@@ -350,34 +358,41 @@ class ProjectRepository extends EntityRepository
|
||||
return new Pagination($this->getPaginatorForQuery($query), $query);
|
||||
}
|
||||
|
||||
private function getPaginatorForQuery(ProjectQuery $query): PaginatorInterface
|
||||
/**
|
||||
* @return PaginatorInterface<Project>
|
||||
*/
|
||||
private function getPaginatorForQuery(ProjectQuery $projectQuery): PaginatorInterface
|
||||
{
|
||||
$counter = $this->countProjectsForQuery($query);
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
$counter = $this->countProjectsForQuery($projectQuery);
|
||||
$query = $this->createProjectQuery($projectQuery);
|
||||
|
||||
return new LoaderPaginator(new ProjectLoader($qb->getEntityManager()), $qb, $counter);
|
||||
return new LoaderQueryPaginator(new ProjectLoader($this->getEntityManager(), false, true), $query, $counter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProjectQuery $query
|
||||
* @return Project[]
|
||||
*/
|
||||
public function getProjectsForQuery(ProjectQuery $query): iterable
|
||||
public function getProjectsForQuery(ProjectQuery $query): array
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
$results = $qb->getQuery()->execute();
|
||||
$loader = new ProjectLoader($qb->getEntityManager());
|
||||
$loader->loadResults($results);
|
||||
|
||||
return $results;
|
||||
return $this->getProjects($this->createProjectQuery($query));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $delete
|
||||
* @param Project|null $replace
|
||||
* @throws \Doctrine\ORM\Exception\ORMException
|
||||
* @param Query<Project> $query
|
||||
* @return Project[]
|
||||
*/
|
||||
public function deleteProject(Project $delete, ?Project $replace = null)
|
||||
public function getProjects(Query $query): array
|
||||
{
|
||||
/** @var array<Project> $projects */
|
||||
$projects = $query->execute();
|
||||
|
||||
$loader = new ProjectLoader($this->getEntityManager(), false, true);
|
||||
$loader->loadResults($projects);
|
||||
|
||||
return $projects;
|
||||
}
|
||||
|
||||
public function deleteProject(Project $delete, ?Project $replace = null): void
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
@@ -429,17 +444,58 @@ class ProjectRepository extends EntityRepository
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function saveComment(ProjectComment $comment)
|
||||
public function saveComment(ProjectComment $comment): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($comment);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function deleteComment(ProjectComment $comment)
|
||||
public function deleteComment(ProjectComment $comment): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->remove($comment);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Query<Project>
|
||||
*/
|
||||
private function createProjectQuery(ProjectQuery $projectQuery): Query
|
||||
{
|
||||
$query = $this->getQueryBuilderForQuery($projectQuery)->getQuery();
|
||||
$query = $this->prepareProjectQuery($query);
|
||||
|
||||
foreach ($projectQuery->getHydrate() as $hydrate) {
|
||||
switch ($hydrate) {
|
||||
case ProjectQueryHydrate::TEAMS:
|
||||
// does not yet work, see https://github.com/doctrine/orm/pull/8391
|
||||
// $query->setFetchMode(Project::class, 'teams', ClassMetadata::FETCH_EAGER);
|
||||
break;
|
||||
|
||||
case ProjectQueryHydrate::TEAM_MEMBER:
|
||||
// does not yet work, see https://github.com/doctrine/orm/issues/11254
|
||||
// $query->setFetchMode(Project::class, 'teams', ClassMetadata::FETCH_EAGER);
|
||||
// $query->setFetchMode(Team::class, 'members', ClassMetadata::FETCH_EAGER);
|
||||
// $query->setFetchMode(TeamMember::class, 'user', ClassMetadata::FETCH_EAGER);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Query<Project> $query
|
||||
* @return Query<Project>
|
||||
*/
|
||||
public function prepareProjectQuery(Query $query): Query
|
||||
{
|
||||
$this->getEntityManager()->getConfiguration()->setEagerFetchBatchSize(300);
|
||||
|
||||
$query->setFetchMode(Project::class, 'meta', ClassMetadata::FETCH_EAGER);
|
||||
$query->setFetchMode(Project::class, 'customer', ClassMetadata::FETCH_EAGER);
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,11 @@ use App\Entity\Project;
|
||||
/**
|
||||
* Can be used for advanced queries with the: ActivityRepository
|
||||
*/
|
||||
class ActivityQuery extends ProjectQuery
|
||||
class ActivityQuery extends BaseQuery implements VisibilityInterface
|
||||
{
|
||||
use VisibilityTrait;
|
||||
use CustomerTrait;
|
||||
|
||||
public const ACTIVITY_ORDER_ALLOWED = [
|
||||
'name',
|
||||
'description' => 'comment',
|
||||
@@ -33,18 +36,45 @@ class ActivityQuery extends ProjectQuery
|
||||
private array $projects = [];
|
||||
private bool $globalsOnly = false;
|
||||
private bool $excludeGlobals = false;
|
||||
/**
|
||||
* @var array<int>
|
||||
*/
|
||||
private array $activityIds = [];
|
||||
/**
|
||||
* @var array<ActivityQueryHydrate>
|
||||
*/
|
||||
private array $hydrate = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setDefaults([
|
||||
'orderBy' => 'name',
|
||||
'customers' => [],
|
||||
'projects' => [],
|
||||
'globalsOnly' => false,
|
||||
'excludeGlobals' => false,
|
||||
'activityIds' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function copyFrom(BaseQuery $query): void
|
||||
{
|
||||
parent::copyFrom($query);
|
||||
|
||||
if (method_exists($query, 'getCustomers')) {
|
||||
$this->setCustomers($query->getCustomers());
|
||||
}
|
||||
|
||||
if ($query instanceof ActivityQuery) {
|
||||
$this->setActivityIds($query->getActivityIds());
|
||||
$this->setGlobalsOnly($query->isGlobalsOnly());
|
||||
$this->setExcludeGlobals($query->isExcludeGlobals());
|
||||
foreach ($query->getHydrate() as $hydrate) {
|
||||
$this->addHydrate($hydrate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function isGlobalsOnly(): bool
|
||||
{
|
||||
return $this->globalsOnly;
|
||||
@@ -115,4 +145,40 @@ class ActivityQuery extends ProjectQuery
|
||||
{
|
||||
return !empty($this->projects);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $ids
|
||||
*/
|
||||
public function setActivityIds(array $ids): void
|
||||
{
|
||||
$this->activityIds = $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function getActivityIds(): array
|
||||
{
|
||||
return $this->activityIds;
|
||||
}
|
||||
|
||||
private function addHydrate(ActivityQueryHydrate $hydrate): void
|
||||
{
|
||||
if (!\in_array($hydrate, $this->hydrate, true)) {
|
||||
$this->hydrate[] = $hydrate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ActivityQueryHydrate[]
|
||||
*/
|
||||
public function getHydrate(): array
|
||||
{
|
||||
return $this->hydrate;
|
||||
}
|
||||
|
||||
public function loadTeams(): void
|
||||
{
|
||||
$this->addHydrate(ActivityQueryHydrate::TEAMS);
|
||||
}
|
||||
}
|
||||
|
||||
16
src/Repository/Query/ActivityQueryHydrate.php
Normal file
16
src/Repository/Query/ActivityQueryHydrate.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?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\Query;
|
||||
|
||||
enum ActivityQueryHydrate
|
||||
{
|
||||
case TEAMS;
|
||||
case TEAM_MEMBER;
|
||||
}
|
||||
@@ -316,7 +316,13 @@ class BaseQuery
|
||||
return array_pop($shortClass);
|
||||
}
|
||||
|
||||
public function copyTo(BaseQuery $query): BaseQuery
|
||||
/**
|
||||
* @template T of BaseQuery
|
||||
* @param T $query
|
||||
* @return T
|
||||
* @internal
|
||||
*/
|
||||
final public function copyTo(BaseQuery $query): BaseQuery
|
||||
{
|
||||
$query->setDefaults($this->defaults);
|
||||
if (null !== $this->getCurrentUser()) {
|
||||
@@ -336,9 +342,15 @@ class BaseQuery
|
||||
$query->setVisibility($this->getVisibility());
|
||||
}
|
||||
|
||||
$query->copyFrom($this);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function copyFrom(BaseQuery $query): void
|
||||
{
|
||||
}
|
||||
|
||||
public function isDefaultFilter(string $filter): bool
|
||||
{
|
||||
if (!\array_key_exists($filter, $this->defaults)) {
|
||||
|
||||
@@ -14,11 +14,33 @@ class CustomerQuery extends BaseQuery implements VisibilityInterface
|
||||
use VisibilityTrait;
|
||||
|
||||
public const CUSTOMER_ORDER_ALLOWED = [
|
||||
'name', 'description' => 'comment', 'country', 'number', 'homepage', 'email', 'mobile', 'fax',
|
||||
'phone', 'currency', 'address', 'contact', 'company', 'vat_id', 'budget', 'timeBudget', 'visible'
|
||||
'name',
|
||||
'description' => 'comment',
|
||||
'country', 'number',
|
||||
'homepage',
|
||||
'email',
|
||||
'mobile',
|
||||
'fax',
|
||||
'phone',
|
||||
'currency',
|
||||
'address',
|
||||
'contact',
|
||||
'company',
|
||||
'vat_id',
|
||||
'budget',
|
||||
'timeBudget',
|
||||
'visible'
|
||||
];
|
||||
|
||||
private ?string $country = null;
|
||||
/**
|
||||
* @var array<int>
|
||||
*/
|
||||
private array $customerIds = [];
|
||||
/**
|
||||
* @var array<CustomerQueryHydrate>
|
||||
*/
|
||||
private array $hydrate = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -26,9 +48,23 @@ class CustomerQuery extends BaseQuery implements VisibilityInterface
|
||||
'orderBy' => 'name',
|
||||
'visibility' => VisibilityInterface::SHOW_VISIBLE,
|
||||
'country' => null,
|
||||
'customerIds' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function copyFrom(BaseQuery $query): void
|
||||
{
|
||||
parent::copyFrom($query);
|
||||
|
||||
if ($query instanceof CustomerQuery) {
|
||||
$this->setCustomerIds($query->getCustomerIds());
|
||||
$this->setCountry($query->getCountry());
|
||||
foreach ($query->getHydrate() as $hydrate) {
|
||||
$this->addHydrate($hydrate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getCountry(): ?string
|
||||
{
|
||||
return $this->country;
|
||||
@@ -38,4 +74,40 @@ class CustomerQuery extends BaseQuery implements VisibilityInterface
|
||||
{
|
||||
$this->country = $country;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $ids
|
||||
*/
|
||||
public function setCustomerIds(array $ids): void
|
||||
{
|
||||
$this->customerIds = $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function getCustomerIds(): array
|
||||
{
|
||||
return $this->customerIds;
|
||||
}
|
||||
|
||||
private function addHydrate(CustomerQueryHydrate $hydrate): void
|
||||
{
|
||||
if (!\in_array($hydrate, $this->hydrate, true)) {
|
||||
$this->hydrate[] = $hydrate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CustomerQueryHydrate[]
|
||||
*/
|
||||
public function getHydrate(): array
|
||||
{
|
||||
return $this->hydrate;
|
||||
}
|
||||
|
||||
public function loadTeams(): void
|
||||
{
|
||||
$this->addHydrate(CustomerQueryHydrate::TEAMS);
|
||||
}
|
||||
}
|
||||
|
||||
16
src/Repository/Query/CustomerQueryHydrate.php
Normal file
16
src/Repository/Query/CustomerQueryHydrate.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?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\Query;
|
||||
|
||||
enum CustomerQueryHydrate
|
||||
{
|
||||
case TEAMS;
|
||||
case TEAM_MEMBER;
|
||||
}
|
||||
63
src/Repository/Query/CustomerTrait.php
Normal file
63
src/Repository/Query/CustomerTrait.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?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\Query;
|
||||
|
||||
use App\Entity\Customer;
|
||||
|
||||
trait CustomerTrait
|
||||
{
|
||||
/**
|
||||
* @var array<Customer>
|
||||
*/
|
||||
private array $customers = [];
|
||||
|
||||
public function addCustomer(Customer $customer): self
|
||||
{
|
||||
$this->customers[] = $customer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Customer> $customers
|
||||
* @return $this
|
||||
*/
|
||||
public function setCustomers(array $customers): self
|
||||
{
|
||||
$this->customers = $customers;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<Customer>
|
||||
*/
|
||||
public function getCustomers(): array
|
||||
{
|
||||
return $this->customers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
public function getCustomerIds(): array
|
||||
{
|
||||
return array_filter(array_values(array_unique(array_map(function (Customer $customer) {
|
||||
return $customer->getId();
|
||||
}, $this->customers))), function ($id) {
|
||||
return $id !== null;
|
||||
});
|
||||
}
|
||||
|
||||
public function hasCustomers(): bool
|
||||
{
|
||||
return !empty($this->customers);
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,10 @@
|
||||
|
||||
namespace App\Repository\Query;
|
||||
|
||||
use App\Entity\Customer;
|
||||
|
||||
class ProjectQuery extends BaseQuery implements VisibilityInterface
|
||||
{
|
||||
use VisibilityTrait;
|
||||
use CustomerTrait;
|
||||
|
||||
public const PROJECT_ORDER_ALLOWED = [
|
||||
'name',
|
||||
@@ -29,13 +28,17 @@ class ProjectQuery extends BaseQuery implements VisibilityInterface
|
||||
'visible'
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<Customer>
|
||||
*/
|
||||
private array $customers = [];
|
||||
private ?\DateTime $projectStart = null;
|
||||
private ?\DateTime $projectEnd = null;
|
||||
private ?bool $globalActivities = null;
|
||||
/**
|
||||
* @var array<int>
|
||||
*/
|
||||
private array $projectIds = [];
|
||||
/**
|
||||
* @var array<ProjectQueryHydrate>
|
||||
*/
|
||||
private array $hydrate = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -46,50 +49,27 @@ class ProjectQuery extends BaseQuery implements VisibilityInterface
|
||||
'projectEnd' => null,
|
||||
'visibility' => VisibilityInterface::SHOW_VISIBLE,
|
||||
'globalActivities' => null,
|
||||
'projectIds' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function addCustomer(Customer $customer): self
|
||||
protected function copyFrom(BaseQuery $query): void
|
||||
{
|
||||
$this->customers[] = $customer;
|
||||
parent::copyFrom($query);
|
||||
|
||||
return $this;
|
||||
}
|
||||
if (method_exists($query, 'getCustomers')) {
|
||||
$this->setCustomers($query->getCustomers());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Customer> $customers
|
||||
* @return $this
|
||||
*/
|
||||
public function setCustomers(array $customers): self
|
||||
{
|
||||
$this->customers = $customers;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<Customer>
|
||||
*/
|
||||
public function getCustomers(): array
|
||||
{
|
||||
return $this->customers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
public function getCustomerIds(): array
|
||||
{
|
||||
return array_filter(array_values(array_unique(array_map(function (Customer $customer) {
|
||||
return $customer->getId();
|
||||
}, $this->customers))), function ($id) {
|
||||
return $id !== null;
|
||||
});
|
||||
}
|
||||
|
||||
public function hasCustomers(): bool
|
||||
{
|
||||
return !empty($this->customers);
|
||||
if ($query instanceof ProjectQuery) {
|
||||
$this->setProjectIds($query->getProjectIds());
|
||||
$this->setProjectStart($query->getProjectStart());
|
||||
$this->setProjectEnd($query->getProjectEnd());
|
||||
$this->setGlobalActivities($query->getGlobalActivities());
|
||||
foreach ($query->getHydrate() as $hydrate) {
|
||||
$this->addHydrate($hydrate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getProjectStart(): ?\DateTime
|
||||
@@ -125,4 +105,40 @@ class ProjectQuery extends BaseQuery implements VisibilityInterface
|
||||
{
|
||||
$this->globalActivities = $globalActivities;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $ids
|
||||
*/
|
||||
public function setProjectIds(array $ids): void
|
||||
{
|
||||
$this->projectIds = $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function getProjectIds(): array
|
||||
{
|
||||
return $this->projectIds;
|
||||
}
|
||||
|
||||
private function addHydrate(ProjectQueryHydrate $hydrate): void
|
||||
{
|
||||
if (!\in_array($hydrate, $this->hydrate, true)) {
|
||||
$this->hydrate[] = $hydrate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ProjectQueryHydrate[]
|
||||
*/
|
||||
public function getHydrate(): array
|
||||
{
|
||||
return $this->hydrate;
|
||||
}
|
||||
|
||||
public function loadTeams(): void
|
||||
{
|
||||
$this->addHydrate(ProjectQueryHydrate::TEAMS);
|
||||
}
|
||||
}
|
||||
|
||||
16
src/Repository/Query/ProjectQueryHydrate.php
Normal file
16
src/Repository/Query/ProjectQueryHydrate.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?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\Query;
|
||||
|
||||
enum ProjectQueryHydrate
|
||||
{
|
||||
case TEAMS;
|
||||
case TEAM_MEMBER;
|
||||
}
|
||||
@@ -59,6 +59,17 @@ class TimesheetQuery extends ActivityQuery implements BillableInterface, DateRan
|
||||
]);
|
||||
}
|
||||
|
||||
protected function copyFrom(BaseQuery $query): void
|
||||
{
|
||||
parent::copyFrom($query);
|
||||
|
||||
if ($query instanceof TimesheetQuery) {
|
||||
foreach ($this->getUsers() as $user) {
|
||||
$this->addUser($user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getMaxResults(): ?int
|
||||
{
|
||||
return $this->maxResults;
|
||||
|
||||
@@ -26,6 +26,10 @@ class UserQuery extends BaseQuery implements VisibilityInterface
|
||||
*/
|
||||
private array $searchTeams = [];
|
||||
private ?bool $systemAccount = null;
|
||||
/**
|
||||
* @var array<int>
|
||||
*/
|
||||
private array $userIds = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -34,6 +38,7 @@ class UserQuery extends BaseQuery implements VisibilityInterface
|
||||
'searchTeams' => [],
|
||||
'visibility' => VisibilityInterface::SHOW_VISIBLE,
|
||||
'systemAccount' => null,
|
||||
'userIds' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -74,4 +79,20 @@ class UserQuery extends BaseQuery implements VisibilityInterface
|
||||
{
|
||||
$this->systemAccount = $systemAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $ids
|
||||
*/
|
||||
public function setUserIds(array $ids): void
|
||||
{
|
||||
$this->userIds = $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function getUserIds(): array
|
||||
{
|
||||
return $this->userIds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,15 +11,19 @@ namespace App\Repository\Result;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Repository\Loader\TimesheetLoader;
|
||||
use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\LoaderQueryPaginator;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Utils\Pagination;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class TimesheetResult
|
||||
{
|
||||
private ?TimesheetResultStatistic $statisticCache = null;
|
||||
private bool $cachedFullyHydrated = false;
|
||||
/**
|
||||
* @var array<Timesheet>|null
|
||||
*/
|
||||
@@ -27,16 +31,22 @@ final class TimesheetResult
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param Query<null, Timesheet> $query
|
||||
*/
|
||||
public function __construct(private TimesheetQuery $query, private QueryBuilder $queryBuilder)
|
||||
public function __construct(
|
||||
private readonly TimesheetQuery $timesheetQuery,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly QueryBuilder $statisticQb,
|
||||
private readonly Query $query
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getStatistic(): TimesheetResultStatistic
|
||||
{
|
||||
if ($this->statisticCache === null) {
|
||||
$withDuration = $this->query->countFilter() > 0;
|
||||
$qb = clone $this->queryBuilder;
|
||||
$withDuration = $this->timesheetQuery->countFilter() > 0;
|
||||
$qb = clone $this->statisticQb;
|
||||
$qb
|
||||
->resetDQLPart('select')
|
||||
->resetDQLPart('orderBy')
|
||||
@@ -47,6 +57,7 @@ final class TimesheetResult
|
||||
$qb->addSelect('COALESCE(SUM(t.duration), 0) as duration');
|
||||
}
|
||||
|
||||
/** @var array{'duration': int<0, max>, 'counter': int<0, max>} $result */
|
||||
$result = $qb->getQuery()->getArrayResult()[0];
|
||||
$duration = $withDuration ? $result['duration'] : 0;
|
||||
|
||||
@@ -58,39 +69,34 @@ final class TimesheetResult
|
||||
|
||||
public function toIterable(): iterable
|
||||
{
|
||||
$query = $this->queryBuilder->getQuery();
|
||||
|
||||
return $query->toIterable();
|
||||
return $this->query->toIterable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $fullyHydrated
|
||||
* @return array<Timesheet>
|
||||
*/
|
||||
public function getResults(bool $fullyHydrated = false): array
|
||||
public function getResults(): array
|
||||
{
|
||||
if ($this->resultCache === null || ($fullyHydrated && $this->cachedFullyHydrated === false)) {
|
||||
$query = $this->queryBuilder->getQuery();
|
||||
$results = $query->getResult();
|
||||
if ($this->resultCache === null) {
|
||||
/** @var array<Timesheet> $results */
|
||||
$results = $this->query->getResult();
|
||||
|
||||
$loader = new TimesheetLoader($this->queryBuilder->getEntityManager(), $fullyHydrated);
|
||||
$loader = new TimesheetLoader($this->entityManager, true);
|
||||
$loader->loadResults($results);
|
||||
|
||||
$this->cachedFullyHydrated = $fullyHydrated;
|
||||
$this->resultCache = $results;
|
||||
}
|
||||
|
||||
return $this->resultCache;
|
||||
}
|
||||
|
||||
public function getPagerfanta(bool $fullyHydrated = false): Pagination
|
||||
public function getPagerfanta(): Pagination
|
||||
{
|
||||
$qb = clone $this->queryBuilder;
|
||||
$loader = new LoaderQueryPaginator(new TimesheetLoader($this->entityManager), $this->query, $this->getStatistic()->getCount());
|
||||
|
||||
$loader = new LoaderPaginator(new TimesheetLoader($qb->getEntityManager(), $fullyHydrated), $qb, $this->getStatistic()->getCount());
|
||||
$paginator = new Pagination($loader);
|
||||
$paginator->setMaxPerPage($this->query->getPageSize());
|
||||
$paginator->setCurrentPage($this->query->getPage());
|
||||
$paginator->setMaxPerPage($this->timesheetQuery->getPageSize());
|
||||
$paginator->setCurrentPage($this->timesheetQuery->getPage());
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
@@ -11,10 +11,17 @@ namespace App\Repository\Result;
|
||||
|
||||
final class TimesheetResultStatistic
|
||||
{
|
||||
public function __construct(private int $count, private int $duration)
|
||||
/**
|
||||
* @param int<0, max> $count
|
||||
* @param int<0, max> $duration
|
||||
*/
|
||||
public function __construct(private readonly int $count, private readonly int $duration)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function getCount(): int
|
||||
{
|
||||
return $this->count;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use App\Repository\Paginator\QueryBuilderPaginator;
|
||||
use App\Repository\Paginator\QueryPaginator;
|
||||
use App\Repository\Query\TagFormTypeQuery;
|
||||
use App\Repository\Query\TagQuery;
|
||||
use App\Utils\Pagination;
|
||||
@@ -115,9 +115,10 @@ class TagRepository extends EntityRepository
|
||||
->resetDQLPart('orderBy')
|
||||
->select($qb->expr()->count('tag.id'))
|
||||
;
|
||||
/** @var int<0, max> $counter */
|
||||
$counter = (int) $qb->getQuery()->getSingleScalarResult();
|
||||
|
||||
$paginator = new QueryBuilderPaginator($qb1, $counter);
|
||||
$paginator = new QueryPaginator($qb1->getQuery(), $counter);
|
||||
|
||||
$pager = new Pagination($paginator);
|
||||
$pager->setMaxPerPage($query->getPageSize());
|
||||
|
||||
@@ -11,83 +11,62 @@ namespace App\Repository;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Entity\TeamMember;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Loader\TeamLoader;
|
||||
use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\LoaderQueryPaginator;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Utils\Pagination;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
* @extends \Doctrine\ORM\EntityRepository<Team>
|
||||
* @extends EntityRepository<Team>
|
||||
*/
|
||||
class TeamRepository extends EntityRepository
|
||||
{
|
||||
/**
|
||||
* @return Team[]
|
||||
*/
|
||||
public function findAll(): array
|
||||
{
|
||||
$result = parent::findAll();
|
||||
|
||||
$loader = new TeamLoader($this->getEntityManager());
|
||||
$loader->loadResults($result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $teamIds
|
||||
* @return Team[]
|
||||
*/
|
||||
public function findByIds(array $teamIds): array
|
||||
{
|
||||
$ids = array_filter(
|
||||
array_unique($teamIds),
|
||||
function ($value) {
|
||||
return $value > 0;
|
||||
}
|
||||
);
|
||||
|
||||
if (\count($ids) === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$qb = $this->createQueryBuilder('t');
|
||||
$qb
|
||||
->where($qb->expr()->in('t.id', ':id'))
|
||||
->setParameter('id', $teamIds)
|
||||
->setParameter('id', $ids)
|
||||
;
|
||||
|
||||
$teams = $qb->getQuery()->getResult();
|
||||
|
||||
$loader = new TeamLoader($qb->getEntityManager());
|
||||
$loader->loadResults($teams);
|
||||
|
||||
return $teams;
|
||||
return $this->getTeams($this->prepareTeamQuery($qb->getQuery()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Team $team
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function saveTeam(Team $team)
|
||||
public function saveTeam(Team $team): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($team);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TeamMember $member
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function removeTeamMember(TeamMember $member)
|
||||
public function removeTeamMember(TeamMember $member): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->remove($member);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Team $team
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function deleteTeam(Team $team)
|
||||
public function deleteTeam(Team $team): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->remove($team);
|
||||
@@ -96,9 +75,6 @@ class TeamRepository extends EntityRepository
|
||||
|
||||
/**
|
||||
* Returns a query builder that is used for TeamType and your own 'query_builder' option.
|
||||
*
|
||||
* @param TeamQuery $query
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function getQueryBuilderForFormType(TeamQuery $query): QueryBuilder
|
||||
{
|
||||
@@ -118,32 +94,46 @@ class TeamRepository extends EntityRepository
|
||||
return new Pagination($this->getPaginatorForQuery($query), $query);
|
||||
}
|
||||
|
||||
protected function getPaginatorForQuery(TeamQuery $query): PaginatorInterface
|
||||
/**
|
||||
* @return PaginatorInterface<Team>
|
||||
*/
|
||||
private function getPaginatorForQuery(TeamQuery $teamQuery): PaginatorInterface
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
$qb = $this->getQueryBuilderForQuery($teamQuery);
|
||||
$qb
|
||||
->resetDQLPart('select')
|
||||
->resetDQLPart('orderBy')
|
||||
->select($qb->expr()->countDistinct('t.id'))
|
||||
;
|
||||
/** @var int<0, max> $counter */
|
||||
$counter = (int) $qb->getQuery()->getSingleScalarResult();
|
||||
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
$query = $this->createTeamQuery($teamQuery);
|
||||
|
||||
return new LoaderPaginator(new TeamLoader($qb->getEntityManager()), $qb, $counter);
|
||||
return new LoaderQueryPaginator(new TeamLoader($qb->getEntityManager()), $query, $counter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TeamQuery $query
|
||||
* @return Timesheet[]
|
||||
* @return Team[]
|
||||
*/
|
||||
public function getTeamsForQuery(TeamQuery $query): iterable
|
||||
{
|
||||
// this is using the paginator internally, as it will load all joined entities into the working unit
|
||||
// do not "optimize" to use the query directly, as it would results in hundreds of additional lazy queries
|
||||
$paginator = $this->getPaginatorForQuery($query);
|
||||
return $this->getTeams($this->createTeamQuery($query));
|
||||
}
|
||||
|
||||
return $paginator->getAll();
|
||||
/**
|
||||
* @param Query<Team> $query
|
||||
* @return Team[]
|
||||
*/
|
||||
public function getTeams(Query $query): array
|
||||
{
|
||||
/** @var array<Team> $teams */
|
||||
$teams = $query->execute();
|
||||
|
||||
$loader = new TeamLoader($this->getEntityManager());
|
||||
$loader->loadResults($teams);
|
||||
|
||||
return $teams;
|
||||
}
|
||||
|
||||
private function getQueryBuilderForQuery(TeamQuery $query): QueryBuilder
|
||||
@@ -208,11 +198,9 @@ class TeamRepository extends EntityRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryBuilder $qb
|
||||
* @param User|null $user
|
||||
* @param Team[] $teams
|
||||
*/
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): void
|
||||
{
|
||||
// make sure that all queries without a user see all user
|
||||
if (null === $user && empty($teams)) {
|
||||
@@ -228,7 +216,7 @@ class TeamRepository extends EntityRepository
|
||||
// OR we query for all teams where the user is a member - in later case $teams is not empty
|
||||
$or = $qb->expr()->orX();
|
||||
|
||||
// this query should limit to teams where the user is a teamlead (eg. in dropdowns or listing page)
|
||||
// this query should limit to teams where the user is a teamlead (e.g. in dropdowns or listing page)
|
||||
if (null !== $user) {
|
||||
$qb->leftJoin('t.members', 'members');
|
||||
$or->add(
|
||||
@@ -255,4 +243,31 @@ class TeamRepository extends EntityRepository
|
||||
$qb->andWhere($or);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Query<Team>
|
||||
*/
|
||||
private function createTeamQuery(TeamQuery $teamQuery): Query
|
||||
{
|
||||
$query = $this->getQueryBuilderForQuery($teamQuery)->getQuery();
|
||||
$query = $this->prepareTeamQuery($query);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Query<Team> $query
|
||||
* @return Query<Team>
|
||||
*/
|
||||
public function prepareTeamQuery(Query $query): Query
|
||||
{
|
||||
$this->getEntityManager()->getConfiguration()->setEagerFetchBatchSize(300);
|
||||
|
||||
// $query->setFetchMode(Team::class, 'members', ClassMetadata::FETCH_EAGER);
|
||||
// $query->setFetchMode(Team::class, 'customers', ClassMetadata::FETCH_EAGER);
|
||||
// $query->setFetchMode(Team::class, 'projects', ClassMetadata::FETCH_EAGER);
|
||||
// $query->setFetchMode(Team::class, 'activities', ClassMetadata::FETCH_EAGER);
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ use App\Entity\User;
|
||||
use App\Model\Revenue;
|
||||
use App\Model\TimesheetStatistic;
|
||||
use App\Repository\Loader\TimesheetLoader;
|
||||
use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\LoaderQueryPaginator;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\Result\TimesheetResult;
|
||||
@@ -30,13 +30,15 @@ use DateInterval;
|
||||
use DateTime;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @extends \Doctrine\ORM\EntityRepository<Timesheet>
|
||||
* @extends EntityRepository<Timesheet>
|
||||
*/
|
||||
class TimesheetRepository extends EntityRepository
|
||||
{
|
||||
@@ -230,7 +232,6 @@ class TimesheetRepository extends EntityRepository
|
||||
/**
|
||||
* @param string|string[] $select
|
||||
* @return int|mixed
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
private function queryTimeRange(string|array $select, ?\DateTimeInterface $begin, ?\DateTimeInterface $end, ?User $user, ?bool $billable = null): mixed
|
||||
{
|
||||
@@ -346,9 +347,12 @@ class TimesheetRepository extends EntityRepository
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
return $this->getHydratedResultsByQuery($qb, false);
|
||||
return $this->getHydratedResultsByQuery($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function countActiveEntries(?User $user = null): int
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
@@ -366,9 +370,12 @@ class TimesheetRepository extends EntityRepository
|
||||
;
|
||||
}
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
return (int) $qb->getQuery()->getSingleScalarResult(); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function countActiveUsers(?\DateTimeInterface $begin, ?\DateTimeInterface $end, ?bool $billable = null): int
|
||||
{
|
||||
$tmp = $this->queryTimeRange('COUNT(DISTINCT(t.user))', $begin, $end, null, $billable);
|
||||
@@ -377,7 +384,7 @@ class TimesheetRepository extends EntityRepository
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) $tmp;
|
||||
return (int) $tmp; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,7 +449,10 @@ class TimesheetRepository extends EntityRepository
|
||||
return new Pagination($this->getPaginatorForQuery($query), $query);
|
||||
}
|
||||
|
||||
private function getPaginatorForQuery(TimesheetQuery $query): PaginatorInterface
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
private function countTimesheetsForQuery(TimesheetQuery $query): int
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
$qb
|
||||
@@ -450,50 +460,60 @@ class TimesheetRepository extends EntityRepository
|
||||
->resetDQLPart('orderBy')
|
||||
->select($qb->expr()->count('t.id'))
|
||||
;
|
||||
$counter = (int) $qb->getQuery()->getSingleScalarResult();
|
||||
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
return (int) $qb->getQuery()->getSingleScalarResult(); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
return new LoaderPaginator(new TimesheetLoader($qb->getEntityManager()), $qb, $counter);
|
||||
/**
|
||||
* @return PaginatorInterface<Timesheet>
|
||||
*/
|
||||
private function getPaginatorForQuery(TimesheetQuery $timesheetQuery): PaginatorInterface
|
||||
{
|
||||
$counter = $this->countTimesheetsForQuery($timesheetQuery);
|
||||
$query = $this->createTimesheetQuery($timesheetQuery);
|
||||
|
||||
return new LoaderQueryPaginator(new TimesheetLoader($this->getEntityManager()), $query, $counter);
|
||||
}
|
||||
|
||||
/**
|
||||
* When switching $fullyHydrated to true, the call gets even more expensive.
|
||||
* You normally don't need this, unless you want to access deeply nested attributes for many entries.
|
||||
*
|
||||
* @param TimesheetQuery $query
|
||||
* @param bool $fullyHydrated
|
||||
* @param bool $basicHydrated
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
public function getTimesheetsForQuery(TimesheetQuery $query, bool $fullyHydrated = false, bool $basicHydrated = true): iterable
|
||||
public function getTimesheetsForQuery(TimesheetQuery $query, bool $fullyHydrated = false): array
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
|
||||
return $this->getHydratedResultsByQuery($qb, $fullyHydrated, $basicHydrated);
|
||||
return $this->getHydratedResultsByQuery($qb, $fullyHydrated);
|
||||
}
|
||||
|
||||
public function getTimesheetResult(TimesheetQuery $query): TimesheetResult
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
|
||||
return new TimesheetResult($query, $qb);
|
||||
return new TimesheetResult(
|
||||
$query,
|
||||
$this->getEntityManager(),
|
||||
$this->getQueryBuilderForQuery($query),
|
||||
$this->createTimesheetQuery($query)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryBuilder $qb
|
||||
* @param bool $fullyHydrated
|
||||
* @param bool $basicHydrated
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
private function getHydratedResultsByQuery(QueryBuilder $qb, bool $fullyHydrated = false, bool $basicHydrated = true): iterable
|
||||
private function getHydratedResultsByQuery(QueryBuilder $qb, bool $fullyHydrated = false): array
|
||||
{
|
||||
$results = $qb->getQuery()->getResult();
|
||||
/** @var Query<Timesheet> $query */
|
||||
$query = $qb->getQuery();
|
||||
$query = $this->prepareTimesheetQuery($query);
|
||||
|
||||
$loader = new TimesheetLoader($qb->getEntityManager(), $fullyHydrated, $basicHydrated);
|
||||
$loader->loadResults($results);
|
||||
/** @var array<Timesheet> $timesheets */
|
||||
$timesheets = $query->getResult();
|
||||
|
||||
return $results;
|
||||
$loader = new TimesheetLoader($qb->getEntityManager(), $fullyHydrated);
|
||||
$loader->loadResults($timesheets);
|
||||
|
||||
return $timesheets;
|
||||
}
|
||||
|
||||
private function getQueryBuilderForQuery(TimesheetQuery $query): QueryBuilder
|
||||
@@ -660,12 +680,9 @@ class TimesheetRepository extends EntityRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param DateTime|null $startFrom
|
||||
* @param int $limit
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
public function getRecentActivities(User $user, DateTime $startFrom = null, int $limit = 10): array
|
||||
public function getRecentActivities(User $user, ?\DateTimeInterface $startFrom = null, int $limit = 10): array
|
||||
{
|
||||
return $this->findTimesheetsById(
|
||||
$user,
|
||||
@@ -674,12 +691,9 @@ class TimesheetRepository extends EntityRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param DateTime|null $startFrom
|
||||
* @param int $limit
|
||||
* @return array<int>
|
||||
*/
|
||||
public function getRecentActivityIds(User $user, DateTime $startFrom = null, int $limit = 10): array
|
||||
public function getRecentActivityIds(User $user, ?\DateTimeInterface $startFrom = null, int $limit = 10): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
@@ -707,7 +721,7 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
if (null !== $startFrom) {
|
||||
$qb->andWhere($qb->expr()->gte('t.begin', ':begin'))
|
||||
->setParameter('begin', $startFrom);
|
||||
->setParameter('begin', \DateTimeImmutable::createFromInterface($startFrom), Types::DATETIME_IMMUTABLE);
|
||||
}
|
||||
|
||||
$qb->join('t.project', 'p');
|
||||
@@ -725,13 +739,10 @@ class TimesheetRepository extends EntityRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param array<int> $ids
|
||||
* @param bool $fullyHydrated
|
||||
* @param bool $basicHydrated
|
||||
* @return array<Timesheet>
|
||||
*/
|
||||
public function findTimesheetsById(User $user, array $ids, bool $fullyHydrated = false, bool $basicHydrated = true): array
|
||||
public function findTimesheetsById(User $user, array $ids): array
|
||||
{
|
||||
if (\count($ids) === 0) {
|
||||
return [];
|
||||
@@ -749,13 +760,13 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
$this->addPermissionCriteria($qb, $user);
|
||||
|
||||
return $this->getHydratedResultsByQuery($qb, $fullyHydrated, $basicHydrated);
|
||||
return $this->getHydratedResultsByQuery($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[]|int[] $timesheets
|
||||
*/
|
||||
public function setExported(array $timesheets)
|
||||
public function setExported(array $timesheets): void
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
@@ -896,4 +907,40 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
return $result > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Query<Timesheet>
|
||||
*/
|
||||
private function createTimesheetQuery(TimesheetQuery $timesheetQuery): Query
|
||||
{
|
||||
$query = $this->getQueryBuilderForQuery($timesheetQuery)->getQuery();
|
||||
$query = $this->prepareTimesheetQuery($query);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Query<Timesheet> $query
|
||||
* @return Query<Timesheet>
|
||||
*/
|
||||
public function prepareTimesheetQuery(Query $query): Query
|
||||
{
|
||||
$this->getEntityManager()->getConfiguration()->setEagerFetchBatchSize(300);
|
||||
|
||||
$query->setFetchMode(Timesheet::class, 'meta', ClassMetadata::FETCH_EAGER);
|
||||
$query->setFetchMode(Timesheet::class, 'activity', ClassMetadata::FETCH_EAGER);
|
||||
$query->setFetchMode(Timesheet::class, 'project', ClassMetadata::FETCH_EAGER);
|
||||
$query->setFetchMode(Timesheet::class, 'user', ClassMetadata::FETCH_EAGER);
|
||||
|
||||
// not yet supported by Doctrine
|
||||
// $query->setFetchMode(Activity::class, 'meta', ClassMetadata::FETCH_EAGER);
|
||||
// $query->setFetchMode(Project::class, 'customer', ClassMetadata::FETCH_EAGER);
|
||||
// $query->setFetchMode(Project::class, 'meta', ClassMetadata::FETCH_EAGER);
|
||||
// $query->setFetchMode(Customer::class, 'meta', ClassMetadata::FETCH_EAGER);
|
||||
|
||||
// ManyToMany not supported by Doctrine yet
|
||||
// $query->setFetchMode(Timesheet::class, 'tags', ClassMetadata::FETCH_EAGER);
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,15 +15,16 @@ use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Repository\Loader\UserLoader;
|
||||
use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\LoaderQueryPaginator;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\UserFormTypeQuery;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\Query\VisibilityInterface;
|
||||
use App\Utils\Pagination;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
@@ -33,7 +34,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* @extends \Doctrine\ORM\EntityRepository<User>
|
||||
* @extends EntityRepository<User>
|
||||
* @template-implements PasswordUpgraderInterface<User>
|
||||
* @template-implements UserProviderInterface<User>
|
||||
*/
|
||||
@@ -48,11 +49,6 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function saveUser(User $user): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
@@ -78,23 +74,15 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to fetch a user by its ID.
|
||||
*
|
||||
* @param int $id
|
||||
* @return null|User
|
||||
*/
|
||||
public function getUserById($id): ?User
|
||||
public function getUserById(int $id): ?User
|
||||
{
|
||||
/** @var User|null $user */
|
||||
$user = $this->find($id);
|
||||
$users = $this->findByIds([$id]);
|
||||
|
||||
if ($user !== null) {
|
||||
$loader = new UserLoader($this->getEntityManager(), true);
|
||||
$loader->loadResults([$user]);
|
||||
if (\count($users) === 1) {
|
||||
return $users[0];
|
||||
}
|
||||
|
||||
return $user;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,18 +91,24 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
|
||||
*/
|
||||
public function findByIds(array $userIds): array
|
||||
{
|
||||
$ids = array_filter(
|
||||
array_unique($userIds),
|
||||
function ($value) {
|
||||
return $value > 0;
|
||||
}
|
||||
);
|
||||
|
||||
if (\count($ids) === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$qb = $this->createQueryBuilder('u');
|
||||
$qb
|
||||
->where($qb->expr()->in('u.id', ':id'))
|
||||
->setParameter('id', $userIds)
|
||||
->setParameter('id', $ids)
|
||||
;
|
||||
|
||||
$users = $qb->getQuery()->getResult();
|
||||
|
||||
$loader = new UserLoader($qb->getEntityManager(), true);
|
||||
$loader->loadResults($users);
|
||||
|
||||
return $users;
|
||||
return $this->getUsers($this->prepareUserQuery($qb->getQuery()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,6 +129,9 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
|
||||
return parent::findOneBy(['username' => $username]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function countUser(?bool $enabled = null): int
|
||||
{
|
||||
if (null !== $enabled) {
|
||||
@@ -200,11 +197,9 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryBuilder $qb
|
||||
* @param User|null $user
|
||||
* @param Team[] $teams
|
||||
*/
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): void
|
||||
{
|
||||
// make sure that all queries without a user see all user
|
||||
if (null === $user && empty($teams)) {
|
||||
@@ -259,25 +254,16 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $role
|
||||
* @return User[]
|
||||
* @internal
|
||||
*/
|
||||
public function findUsersWithRole(string $role): array
|
||||
{
|
||||
if ($role === User::ROLE_USER) {
|
||||
return $this->findAll();
|
||||
}
|
||||
$query = new UserQuery();
|
||||
$query->setRole($role);
|
||||
$query->setVisibility(VisibilityInterface::SHOW_BOTH);
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb
|
||||
->select('u')
|
||||
->from(User::class, 'u')
|
||||
->andWhere('u.roles LIKE :role');
|
||||
$qb->setParameter('role', '%' . $role . '%');
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
return $this->getUsersForQuery($query);
|
||||
}
|
||||
|
||||
private function getQueryBuilderForQuery(UserQuery $query): QueryBuilder
|
||||
@@ -377,6 +363,9 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
|
||||
return new Pagination($this->getPaginatorForQuery($query), $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function countUsersForQuery(UserQuery $query): int
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
@@ -386,15 +375,18 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
|
||||
->select($qb->expr()->countDistinct('u.id'))
|
||||
;
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
return (int) $qb->getQuery()->getSingleScalarResult(); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
protected function getPaginatorForQuery(UserQuery $query): PaginatorInterface
|
||||
/**
|
||||
* @return PaginatorInterface<User>
|
||||
*/
|
||||
private function getPaginatorForQuery(UserQuery $userQuery): PaginatorInterface
|
||||
{
|
||||
$counter = $this->countUsersForQuery($query);
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
$counter = $this->countUsersForQuery($userQuery);
|
||||
$query = $this->createUserQuery($userQuery);
|
||||
|
||||
return new LoaderPaginator(new UserLoader($qb->getEntityManager()), $qb, $counter);
|
||||
return new LoaderQueryPaginator(new UserLoader($this->getEntityManager()), $query, $counter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -403,27 +395,25 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
|
||||
*/
|
||||
public function getUsersForQuery(UserQuery $query): array
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
|
||||
return $this->getHydratedResultsByQuery($qb);
|
||||
return $this->getUsers($this->createUserQuery($query));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryBuilder $qb
|
||||
* @param Query<User> $query
|
||||
* @return User[]
|
||||
*/
|
||||
protected function getHydratedResultsByQuery(QueryBuilder $qb): array
|
||||
public function getUsers(Query $query): array
|
||||
{
|
||||
/** @var array<User> $results */
|
||||
$results = $qb->getQuery()->getResult();
|
||||
/** @var array<User> $users */
|
||||
$users = $query->execute();
|
||||
|
||||
$loader = new UserLoader($qb->getEntityManager());
|
||||
$loader->loadResults($results);
|
||||
$loader = new UserLoader($this->getEntityManager());
|
||||
$loader->loadResults($users);
|
||||
|
||||
return $results;
|
||||
return $users;
|
||||
}
|
||||
|
||||
public function deleteUser(User $delete, ?User $replace = null)
|
||||
public function deleteUser(User $delete, ?User $replace = null): void
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
@@ -454,9 +444,35 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
|
||||
$em->remove($delete);
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Query<User>
|
||||
*/
|
||||
private function createUserQuery(UserQuery $userQuery): Query
|
||||
{
|
||||
$query = $this->getQueryBuilderForQuery($userQuery)->getQuery();
|
||||
$query = $this->prepareUserQuery($query);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Query<User> $query
|
||||
* @return Query<User>
|
||||
*/
|
||||
public function prepareUserQuery(Query $query): Query
|
||||
{
|
||||
$this->getEntityManager()->getConfiguration()->setEagerFetchBatchSize(300);
|
||||
|
||||
// $query->setFetchMode(User::class, 'preferences', ClassMetadata::FETCH_EAGER);
|
||||
// $query->setFetchMode(User::class, 'supervisor', ClassMetadata::FETCH_EAGER);
|
||||
// $query->setFetchMode(User::class, 'memberships', ClassMetadata::FETCH_EAGER);
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user