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:
@@ -68,6 +68,7 @@ final class ActivityController extends BaseApiController
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new ActivityQuery();
|
||||
$query->loadTeams();
|
||||
$query->setCurrentUser($user);
|
||||
|
||||
$order = $paramFetcher->get('order');
|
||||
|
||||
@@ -65,6 +65,7 @@ final class CustomerController extends BaseApiController
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new CustomerQuery();
|
||||
$query->loadTeams();
|
||||
$query->setCurrentUser($user);
|
||||
|
||||
$order = $paramFetcher->get('order');
|
||||
|
||||
@@ -74,6 +74,7 @@ final class ProjectController extends BaseApiController
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new ProjectQuery();
|
||||
$query->loadTeams();
|
||||
$query->setCurrentUser($user);
|
||||
|
||||
$order = $paramFetcher->get('order');
|
||||
|
||||
@@ -18,6 +18,7 @@ use App\Form\API\TeamApiEditForm;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
@@ -53,7 +54,10 @@ final class TeamController extends BaseApiController
|
||||
#[Route(methods: ['GET'], path: '', name: 'get_teams')]
|
||||
public function cgetAction(): Response
|
||||
{
|
||||
$data = $this->repository->findAll();
|
||||
$query = new TeamQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
$data = $this->repository->getTeamsForQuery($query);
|
||||
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
@@ -106,25 +106,25 @@ final class ExportCreateCommand extends Command
|
||||
$timezone = new \DateTimeZone($timezone);
|
||||
$dateFactory = new DateTimeFactory($timezone);
|
||||
|
||||
$customerIDs = $input->getOption('customer');
|
||||
$customerIDs = $this->optionToIntArray($input, 'customer');
|
||||
$customers = [];
|
||||
if (\count($customerIDs) > 0) {
|
||||
$customers = $this->customerRepository->findByIds($customerIDs);
|
||||
}
|
||||
|
||||
$projectIDs = $input->getOption('project');
|
||||
$projectIDs = $this->optionToIntArray($input, 'project');
|
||||
$projects = [];
|
||||
if (\count($projectIDs) > 0) {
|
||||
$projects = $this->projectRepository->findByIds($projectIDs);
|
||||
}
|
||||
|
||||
$teamIDs = $input->getOption('team');
|
||||
$teamIDs = $this->optionToIntArray($input, 'team');
|
||||
$teams = [];
|
||||
if (\count($teamIDs) > 0) {
|
||||
$teams = $this->teamRepository->findByIds($teamIDs);
|
||||
}
|
||||
|
||||
$userIDs = $input->getOption('user');
|
||||
$userIDs = $this->optionToIntArray($input, 'user');
|
||||
$users = [];
|
||||
if (\count($userIDs) > 0) {
|
||||
$users = $this->userRepository->findByIds($userIDs);
|
||||
@@ -287,6 +287,25 @@ final class ExportCreateCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
private function optionToIntArray(InputInterface $input, string $name): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
$options = $input->getOption($name);
|
||||
if (\is_array($options) && \count($options) > 0) {
|
||||
foreach ($options as $option) {
|
||||
if (is_numeric($option)) {
|
||||
$results[] = (int) $option;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function savePreview(Response $response, string $directory): string
|
||||
{
|
||||
$filename = uniqid('invoice_');
|
||||
|
||||
@@ -51,7 +51,12 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[Route(path: '/admin/activity')]
|
||||
final class ActivityController extends AbstractController
|
||||
{
|
||||
public function __construct(private ActivityRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ActivityService $activityService)
|
||||
public function __construct(
|
||||
private readonly ActivityRepository $repository,
|
||||
private readonly SystemConfiguration $configuration,
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
private readonly ActivityService $activityService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -61,6 +66,7 @@ final class ActivityController extends AbstractController
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query->loadTeams();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
@@ -443,7 +449,6 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ActivityQuery $query
|
||||
* @return FormInterface<ActivityQuery>
|
||||
*/
|
||||
private function getToolbarForm(ActivityQuery $query): FormInterface
|
||||
@@ -456,7 +461,7 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FormInterface<ActivityEditForm>
|
||||
* @return FormInterface<mixed>
|
||||
*/
|
||||
private function createEditForm(Activity $activity): FormInterface
|
||||
{
|
||||
|
||||
@@ -54,7 +54,10 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[Route(path: '/admin/customer')]
|
||||
final class CustomerController extends AbstractController
|
||||
{
|
||||
public function __construct(private CustomerRepository $repository, private EventDispatcherInterface $dispatcher)
|
||||
public function __construct(
|
||||
private readonly CustomerRepository $repository,
|
||||
private readonly EventDispatcherInterface $dispatcher
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -64,6 +67,7 @@ final class CustomerController extends AbstractController
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
$query->loadTeams();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
@@ -126,7 +130,6 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CustomerQuery $query
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
private function findMetaColumns(CustomerQuery $query): array
|
||||
@@ -539,7 +542,7 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FormInterface<CustomerComment>
|
||||
* @return FormInterface<mixed>
|
||||
*/
|
||||
private function getCommentForm(CustomerComment $comment): FormInterface
|
||||
{
|
||||
|
||||
@@ -73,6 +73,7 @@ final class ProjectController extends AbstractController
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query->loadTeams();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ final class QuickEntryController extends AbstractController
|
||||
$result = $this->repository->getTimesheetResult($query);
|
||||
|
||||
$rows = [];
|
||||
foreach ($result->getResults(true) as $timesheet) {
|
||||
foreach ($result->getResults() as $timesheet) {
|
||||
$i = 0;
|
||||
$id = $timesheet->getProject()->getId() . '_' . $timesheet->getActivity()->getId();
|
||||
$day = $timesheet->getBegin()->format('Y-m-d');
|
||||
|
||||
@@ -19,7 +19,6 @@ use App\Reporting\YearByUser\YearByUser;
|
||||
use App\Reporting\YearByUser\YearByUserForm;
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -31,11 +30,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[IsGranted('report:user')]
|
||||
final class UserYearController extends AbstractUserReportController
|
||||
{
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Route(path: '/year', name: 'report_user_year', methods: ['GET', 'POST'])]
|
||||
public function yearByUser(Request $request, SystemConfiguration $systemConfiguration): Response
|
||||
{
|
||||
|
||||
@@ -36,6 +36,7 @@ use App\Utils\PageSetup;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormTypeInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
@@ -144,7 +145,9 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$event = new TimesheetMetaDefinitionEvent($entry);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$editForm = $this->getEditForm($entry, $request->get('page'));
|
||||
$page = $request->get('page');
|
||||
$page = is_numeric($page) ? (int) $page : 1;
|
||||
$editForm = $this->getEditForm($entry, $page);
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
@@ -267,7 +270,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
if (null === $exporter) {
|
||||
$form->addError(new FormError('Invalid timesheet exporter given'));
|
||||
} else {
|
||||
return $exporter->render($entries->getResults(true), $query);
|
||||
return $exporter->render($entries->getResults(), $query);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -484,6 +487,9 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<FormTypeInterface> $formClass
|
||||
*/
|
||||
protected function generateCreateForm(Timesheet $entry, string $formClass, string $action): FormInterface
|
||||
{
|
||||
$mode = $this->getTrackingMode();
|
||||
@@ -504,12 +510,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $entry
|
||||
* @param int $page
|
||||
* @return FormInterface
|
||||
*/
|
||||
protected function getEditForm(Timesheet $entry, $page): FormInterface
|
||||
private function getEditForm(Timesheet $entry, int $page): FormInterface
|
||||
{
|
||||
$mode = $this->getTrackingMode();
|
||||
|
||||
@@ -568,6 +569,9 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
return 'edit_rate_own_timesheet';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<FormTypeInterface>
|
||||
*/
|
||||
protected function getEditFormClassName(): string
|
||||
{
|
||||
return TimesheetEditForm::class;
|
||||
|
||||
@@ -23,7 +23,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\CustomerRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
#[Exporter\Order(['id', 'name', 'company', 'number', 'vatId', 'address', 'contact', 'email', 'phone', 'mobile', 'fax', 'homepage', 'country', 'currency', 'timezone', 'budget', 'timeBudget', 'budgetType', 'color', 'visible', 'teams', 'comment', 'billable'])]
|
||||
#[Exporter\Order(['id', 'name', 'company', 'number', 'vatId', 'address', 'contact', 'email', 'phone', 'mobile', 'fax', 'homepage', 'country', 'currency', 'timezone', 'budget', 'timeBudget', 'budgetType', 'color', 'visible', 'comment', 'billable'])]
|
||||
#[Constraints\Customer]
|
||||
class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
{
|
||||
|
||||
@@ -26,7 +26,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
#[Serializer\VirtualProperty('CustomerName', exp: 'object.getCustomer() === null ? null : object.getCustomer().getName()', options: [new Serializer\SerializedName('parentTitle'), new Serializer\Type(name: 'string'), new Serializer\Groups(['Project'])])]
|
||||
#[Serializer\VirtualProperty('CustomerAsId', exp: 'object.getCustomer() === null ? null : object.getCustomer().getId()', options: [new Serializer\SerializedName('customer'), new Serializer\Type(name: 'integer'), new Serializer\Groups(['Project', 'Team', 'Not_Expanded'])])]
|
||||
#[Exporter\Order(['id', 'name', 'customer', 'orderNumber', 'orderDate', 'start', 'end', 'budget', 'timeBudget', 'budgetType', 'color', 'visible', 'teams', 'comment', 'billable', 'number'])]
|
||||
#[Exporter\Order(['id', 'name', 'customer', 'orderNumber', 'orderDate', 'start', 'end', 'budget', 'timeBudget', 'budgetType', 'color', 'visible', 'comment', 'billable', 'number'])]
|
||||
#[Exporter\Expose(name: 'customer', label: 'customer', exp: 'object.getCustomer() === null ? null : object.getCustomer().getName()')]
|
||||
#[Constraints\Project]
|
||||
class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
|
||||
@@ -56,6 +56,10 @@ class Tag
|
||||
/**
|
||||
* This is ONLY here, so we can count the amount of timesheets.
|
||||
*
|
||||
* See TagRepository "SIZE(t.timesheets)"
|
||||
* Removing this makes the count more complicated.
|
||||
* Should be refactored at some point in the future.
|
||||
*
|
||||
* @var Collection<Timesheet>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Timesheet::class, mappedBy: 'tags', fetch: 'EXTRA_LAZY')]
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Validator\Constraints as Constraints;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -20,7 +21,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Table(name: 'kimai2_teams')]
|
||||
#[ORM\UniqueConstraint(columns: ['name'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\TeamRepository')]
|
||||
#[ORM\Entity(repositoryClass: TeamRepository::class)]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity('name')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
@@ -310,6 +311,7 @@ class Team
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @return Collection<Customer>
|
||||
*/
|
||||
public function getCustomers(): iterable
|
||||
@@ -343,6 +345,7 @@ class Team
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @return Collection<Project>
|
||||
*/
|
||||
public function getProjects(): iterable
|
||||
@@ -376,6 +379,7 @@ class Team
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @return Collection<Activity>
|
||||
*/
|
||||
public function getActivities(): iterable
|
||||
|
||||
@@ -128,7 +128,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
* @var Collection<UserPreference>|null
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'user', targetEntity: UserPreference::class, cascade: ['persist'])]
|
||||
private ?Collection $preferences = null;
|
||||
private ?Collection $preferences;
|
||||
/**
|
||||
* List of all team memberships.
|
||||
*
|
||||
@@ -663,11 +663,13 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
#[Serializer\SerializedName('teams')]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
||||
public function getTeams(): iterable
|
||||
public function getTeams(): array
|
||||
{
|
||||
$teams = [];
|
||||
foreach ($this->memberships as $membership) {
|
||||
$teams[] = $membership->getTeam();
|
||||
if ($membership->getTeam() !== null) {
|
||||
$teams[] = $membership->getTeam();
|
||||
}
|
||||
}
|
||||
|
||||
return $teams;
|
||||
|
||||
@@ -13,8 +13,6 @@ use App\Repository\Query\CustomerQuery;
|
||||
|
||||
/**
|
||||
* Dynamically find possible meta fields for a customer query.
|
||||
*
|
||||
* @method CustomerQuery getQuery()
|
||||
*/
|
||||
final class CustomerMetaDisplayEvent extends AbstractMetaDisplayEvent
|
||||
{
|
||||
|
||||
@@ -32,13 +32,12 @@ final class InvoiceSubscriber extends AbstractActionsSubscriber
|
||||
|
||||
$allowCreate = $this->isGranted('create_invoice');
|
||||
$allowView = $this->isGranted('view_invoice');
|
||||
$allowCustomer = $this->isGranted('access', $invoice->getCustomer());
|
||||
|
||||
if ($allowCustomer && $allowCreate) {
|
||||
if ($allowCreate) {
|
||||
$event->addEdit($this->path('admin_invoice_edit', ['id' => $invoice->getId()]));
|
||||
}
|
||||
|
||||
if ($allowCustomer && $allowView) {
|
||||
if ($allowView) {
|
||||
$event->addAction('download', ['url' => $this->path('admin_invoice_download', ['id' => $invoice->getId()]), 'target' => '_blank']);
|
||||
}
|
||||
|
||||
@@ -46,7 +45,7 @@ final class InvoiceSubscriber extends AbstractActionsSubscriber
|
||||
$event->addDivider();
|
||||
}
|
||||
|
||||
if ($allowCustomer && $allowCreate) {
|
||||
if ($allowCreate) {
|
||||
if (!$invoice->isPending()) {
|
||||
$event->addAction('invoice.pending', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'pending', 'token' => $payload['token']])]);
|
||||
} else {
|
||||
|
||||
@@ -18,7 +18,9 @@ use App\Event\ProjectMetaDisplayEvent;
|
||||
use App\Event\TimesheetMetaDisplayEvent;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Export\ExportFilename;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Twig\LocaleFormatExtensions;
|
||||
use App\Utils\StringHelper;
|
||||
@@ -461,9 +463,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
}
|
||||
|
||||
if (isset($columns['customer-meta'])) {
|
||||
/** @var CustomerQuery $customerQuery */
|
||||
$customerQuery = $query->copyTo(new CustomerQuery());
|
||||
$customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($customerQuery, CustomerMetaDisplayEvent::EXPORT));
|
||||
$customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($query->copyTo(new CustomerQuery()), CustomerMetaDisplayEvent::EXPORT));
|
||||
|
||||
$columns['customer-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($customerMetaFields): int {
|
||||
@@ -491,7 +491,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
}
|
||||
|
||||
if (isset($columns['project-meta'])) {
|
||||
$projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query, ProjectMetaDisplayEvent::EXPORT));
|
||||
$projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query->copyTo(new ProjectQuery()), ProjectMetaDisplayEvent::EXPORT));
|
||||
$columns['project-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($projectMetaFields): int {
|
||||
foreach ($projectMetaFields as $metaField) {
|
||||
@@ -518,7 +518,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
}
|
||||
|
||||
if (isset($columns['activity-meta'])) {
|
||||
$activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query, ActivityMetaDisplayEvent::EXPORT));
|
||||
$activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query->copyTo(new ActivityQuery()), ActivityMetaDisplayEvent::EXPORT));
|
||||
$columns['activity-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($activityMetaFields): int {
|
||||
foreach ($activityMetaFields as $metaField) {
|
||||
|
||||
@@ -19,7 +19,9 @@ use App\Event\ProjectMetaDisplayEvent;
|
||||
use App\Event\TimesheetMetaDisplayEvent;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Twig\SecurityPolicy\ExportPolicy;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
@@ -81,13 +83,10 @@ class HtmlRenderer
|
||||
*/
|
||||
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||
{
|
||||
/** @var CustomerQuery $customerQuery */
|
||||
$customerQuery = $query->copyTo(new CustomerQuery());
|
||||
|
||||
$timesheetMetaFields = $this->findMetaColumns(new TimesheetMetaDisplayEvent($query, TimesheetMetaDisplayEvent::EXPORT));
|
||||
$customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($customerQuery, CustomerMetaDisplayEvent::EXPORT));
|
||||
$projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query, ProjectMetaDisplayEvent::EXPORT));
|
||||
$activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query, ActivityMetaDisplayEvent::EXPORT));
|
||||
$customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($query->copyTo(new CustomerQuery()), CustomerMetaDisplayEvent::EXPORT));
|
||||
$projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query->copyTo(new ProjectQuery()), ProjectMetaDisplayEvent::EXPORT));
|
||||
$activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query->copyTo(new ActivityQuery()), ActivityMetaDisplayEvent::EXPORT));
|
||||
|
||||
$event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::EXPORT);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
@@ -44,7 +44,7 @@ final class ActivityHelper
|
||||
public function getChoiceLabel(Activity $activity): string
|
||||
{
|
||||
$name = $this->getChoicePattern();
|
||||
$name = str_replace(self::PATTERN_NAME, $activity->getName(), $name);
|
||||
$name = str_replace(self::PATTERN_NAME, $activity->getName() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_NUMBER, $activity->getNumber() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_COMMENT, $activity->getComment() ?? '', $name);
|
||||
|
||||
@@ -57,7 +57,7 @@ final class ActivityHelper
|
||||
}
|
||||
|
||||
if ($name === '' || $name === self::SPACER) {
|
||||
$name = $activity->getName();
|
||||
$name = $activity->getName() ?? '';
|
||||
}
|
||||
|
||||
return substr($name, 0, 110);
|
||||
|
||||
@@ -45,7 +45,7 @@ final class CustomerHelper
|
||||
public function getChoiceLabel(Customer $customer): string
|
||||
{
|
||||
$name = $this->getChoicePattern();
|
||||
$name = str_replace(self::PATTERN_NAME, $customer->getName(), $name);
|
||||
$name = str_replace(self::PATTERN_NAME, $customer->getName() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_COMMENT, $customer->getComment() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_NUMBER, $customer->getNumber() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_COMPANY, $customer->getCompany() ?? '', $name);
|
||||
@@ -59,7 +59,7 @@ final class CustomerHelper
|
||||
}
|
||||
|
||||
if ($name === '' || $name === self::SPACER) {
|
||||
$name = $customer->getName();
|
||||
$name = $customer->getName() ?? '';
|
||||
}
|
||||
|
||||
return substr($name, 0, 110);
|
||||
|
||||
@@ -70,7 +70,7 @@ final class ProjectHelper
|
||||
public function getChoiceLabel(Project $project): string
|
||||
{
|
||||
$name = $this->getChoicePattern();
|
||||
$name = str_replace(self::PATTERN_NAME, $project->getName(), $name);
|
||||
$name = str_replace(self::PATTERN_NAME, $project->getName() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_NUMBER, $project->getNumber() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_COMMENT, $project->getComment() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_CUSTOMER, $project->getCustomer()?->getName() ?? '', $name);
|
||||
@@ -115,7 +115,7 @@ final class ProjectHelper
|
||||
}
|
||||
|
||||
if ($name === '' || $name === self::SPACER) {
|
||||
$name = $project->getName();
|
||||
$name = $project->getName() ?? '';
|
||||
}
|
||||
|
||||
return substr($name, 0, 110);
|
||||
|
||||
@@ -27,7 +27,10 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
*/
|
||||
final class ProjectType extends AbstractType
|
||||
{
|
||||
public function __construct(private ProjectHelper $projectHelper, private CustomerHelper $customerHelper)
|
||||
public function __construct(
|
||||
private readonly ProjectHelper $projectHelper,
|
||||
private readonly CustomerHelper $customerHelper
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class BudgetStatisticModel implements BudgetStatisticModelInterface
|
||||
private ?BudgetStatistic $statistic = null;
|
||||
private ?BudgetStatistic $statisticTotal = null;
|
||||
|
||||
public function __construct(private EntityWithBudget $entity)
|
||||
public function __construct(private readonly EntityWithBudget $entity)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class BudgetStatisticModel implements BudgetStatisticModelInterface
|
||||
return $this->statistic;
|
||||
}
|
||||
|
||||
public function setStatistic(BudgetStatistic $statistic)
|
||||
public function setStatistic(BudgetStatistic $statistic): void
|
||||
{
|
||||
$this->statistic = $statistic;
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class BudgetStatisticModel implements BudgetStatisticModelInterface
|
||||
return $this->statisticTotal;
|
||||
}
|
||||
|
||||
public function setStatisticTotal(BudgetStatistic $statistic)
|
||||
public function setStatisticTotal(BudgetStatistic $statistic): void
|
||||
{
|
||||
$this->statisticTotal = $statistic;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ use App\Reporting\ProjectInactive\ProjectInactiveQuery;
|
||||
use App\Reporting\ProjectView\ProjectViewModel;
|
||||
use App\Reporting\ProjectView\ProjectViewQuery;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Loader\ProjectLoader;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\UserRepository;
|
||||
@@ -85,7 +84,7 @@ class ProjectStatisticService
|
||||
|
||||
$qb = $this->projectRepository->createQueryBuilder('p');
|
||||
$qb
|
||||
->select('p, c')
|
||||
->select('p')
|
||||
->leftJoin('p.customer', 'c')
|
||||
->andWhere($qb->expr()->eq('p.visible', true))
|
||||
->andWhere($qb->expr()->eq('c.visible', true))
|
||||
@@ -108,15 +107,9 @@ class ProjectStatisticService
|
||||
;
|
||||
|
||||
$this->projectRepository->addPermissionCriteria($qb, $user);
|
||||
$query = $this->projectRepository->prepareProjectQuery($qb->getQuery());
|
||||
|
||||
/** @var Project[] $projects */
|
||||
$projects = $qb->getQuery()->getResult();
|
||||
|
||||
// pre-cache customer objects instead of joining them
|
||||
$loader = new ProjectLoader($this->projectRepository->createQueryBuilder('p')->getEntityManager(), false, false, false);
|
||||
$loader->loadResults($projects);
|
||||
|
||||
return $projects;
|
||||
return $this->projectRepository->getProjects($query);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,6 +121,10 @@ class ProjectStatisticService
|
||||
$begin = $dateRange->getBegin();
|
||||
$end = $dateRange->getEnd();
|
||||
|
||||
if ($begin === null || $end === null) {
|
||||
throw new \InvalidArgumentException('Missing date in date-range for project statistics');
|
||||
}
|
||||
|
||||
$qb = $this->projectRepository->createQueryBuilder('p');
|
||||
$qb
|
||||
->select('p')
|
||||
@@ -191,15 +188,9 @@ class ProjectStatisticService
|
||||
}
|
||||
|
||||
$this->projectRepository->addPermissionCriteria($qb, $user);
|
||||
$query = $this->projectRepository->prepareProjectQuery($qb->getQuery());
|
||||
|
||||
/** @var Project[] $projects */
|
||||
$projects = $qb->getQuery()->getResult();
|
||||
|
||||
// pre-cache customer objects instead of joining them
|
||||
$loader = new ProjectLoader($this->projectRepository->createQueryBuilder('p')->getEntityManager(), false, false, false);
|
||||
$loader->loadResults($projects);
|
||||
|
||||
return $projects;
|
||||
return $this->projectRepository->getProjects($query);
|
||||
}
|
||||
|
||||
public function getBudgetStatisticModel(Project $project, DateTimeInterface $today): ProjectBudgetStatisticModel
|
||||
@@ -687,15 +678,9 @@ class ProjectStatisticService
|
||||
}
|
||||
|
||||
$this->projectRepository->addPermissionCriteria($qb, $user);
|
||||
$query = $this->projectRepository->prepareProjectQuery($qb->getQuery());
|
||||
|
||||
/** @var Project[] $projects */
|
||||
$projects = $qb->getQuery()->getResult();
|
||||
|
||||
// pre-cache customer objects instead of joining them
|
||||
$loader = new ProjectLoader($this->projectRepository->createQueryBuilder('p')->getEntityManager(), false, false, false);
|
||||
$loader->loadResults($projects);
|
||||
|
||||
return $projects;
|
||||
return $this->projectRepository->getProjects($query);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ final class FavoriteRecordService
|
||||
|
||||
$all = [];
|
||||
if (\count($ids) > 0) {
|
||||
$timesheets = $this->repository->findTimesheetsById($user, $ids, false, false);
|
||||
$timesheets = $this->repository->findTimesheetsById($user, $ids);
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$id = $timesheet->getId();
|
||||
if ($id === null) {
|
||||
|
||||
@@ -29,7 +29,10 @@ final class DataTable implements \Countable, \IteratorAggregate
|
||||
private bool $sticky = true;
|
||||
private ?string $paginationRoute = null;
|
||||
|
||||
public function __construct(private string $tableName, private BaseQuery $query)
|
||||
public function __construct(
|
||||
private readonly string $tableName,
|
||||
private readonly BaseQuery $query
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -9,17 +9,17 @@
|
||||
|
||||
namespace App\Widget\Type;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Repository\Loader\ProjectLoader;
|
||||
use App\Repository\Loader\TeamLoader;
|
||||
use App\Widget\WidgetInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
final class UserTeamProjects extends AbstractWidget
|
||||
{
|
||||
public function __construct(private ProjectStatisticService $statisticService, private EntityManagerInterface $entityManager)
|
||||
public function __construct(
|
||||
private readonly ProjectStatisticService $statisticService,
|
||||
private readonly EntityManagerInterface $entityManager
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -65,34 +65,23 @@ final class UserTeamProjects extends AbstractWidget
|
||||
public function getData(array $options = []): mixed
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$teams = $user->getTeams();
|
||||
$now = new \DateTime('now', new \DateTimeZone($user->getTimezone()));
|
||||
|
||||
$loader = new TeamLoader($this->entityManager);
|
||||
$loader->loadResults($user->getTeams());
|
||||
$loader = new TeamLoader($this->entityManager, true);
|
||||
$loader->loadResults($teams);
|
||||
|
||||
$teamProjects = [];
|
||||
$projects = [];
|
||||
|
||||
/** @var Team $team */
|
||||
foreach ($user->getTeams() as $team) {
|
||||
/** @var Project $project */
|
||||
foreach ($teams as $team) {
|
||||
foreach ($team->getProjects() as $project) {
|
||||
if (!isset($teamProjects[$project->getId()])) {
|
||||
$teamProjects[$project->getId()] = $project;
|
||||
if (!$project->isVisibleAtDate($now) || !$project->hasBudgets()) {
|
||||
continue;
|
||||
}
|
||||
$projects[$project->getId()] = $project;
|
||||
}
|
||||
}
|
||||
|
||||
$loader = new ProjectLoader($this->entityManager, false, false, false);
|
||||
$loader->loadResults($teamProjects);
|
||||
|
||||
foreach ($teamProjects as $id => $project) {
|
||||
if (!$project->isVisibleAtDate($now) || !$project->hasBudgets()) {
|
||||
continue;
|
||||
}
|
||||
$projects[$project->getId()] = $project;
|
||||
}
|
||||
|
||||
return $this->statisticService->getBudgetStatisticModelForProjects($projects, $now);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,16 +9,10 @@
|
||||
|
||||
namespace App\Widget\Type;
|
||||
|
||||
use App\Repository\Loader\UserLoader;
|
||||
use App\Widget\WidgetInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
final class UserTeams extends AbstractWidget
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $entityManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function getWidth(): int
|
||||
{
|
||||
return WidgetInterface::WIDTH_HALF;
|
||||
@@ -57,12 +51,6 @@ final class UserTeams extends AbstractWidget
|
||||
*/
|
||||
public function getData(array $options = []): mixed
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
// without this, every user would be lazy loaded
|
||||
$loader = new UserLoader($this->entityManager, true);
|
||||
$loader->loadResults([$user->getId()]);
|
||||
|
||||
return $user->getTeams();
|
||||
return $this->getUser()->getTeams();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user