Skip to content

Commit

Permalink
Cache the results of the statistics to reduce database calls, especia…
Browse files Browse the repository at this point in the history
…lly on large databases
  • Loading branch information
jbtronics committed Dec 1, 2024
1 parent 07cc3e0 commit 83e6206
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 77 deletions.
205 changes: 129 additions & 76 deletions src/Services/Statistics/PaymentOrderStatistics.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,42 @@
use App\Entity\PaymentOrder;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;

/**
* Helpers to retrieve various statistics about payment orders.
* The results get cached for 1 hour.
*/
final readonly class PaymentOrderStatistics
{
public function __construct(private EntityManagerInterface $entityManager)
//Cache time-to-live in seconds (1 hour)
private const CACHE_TTL = 3600;

public function __construct(private EntityManagerInterface $entityManager, private CacheInterface $cache)
{
}

/**
* Caches the result of a query for a given key (function name) and time range.
* @param string $key
* @param \DateTimeInterface|string|null $from
* @param \DateTimeInterface|string|null $to
* @param \Closure $callback
* @return mixed
*/
private function cached(string $key, null|\DateTimeInterface|string $from, null|\DateTimeInterface|string $to, \Closure $callback): mixed
{
//Do not cache if datetime objects were passed directly (as we cannot really find them again in the cache)
if ($from instanceof \DateTimeInterface || $to instanceof \DateTimeInterface) {
return $callback($from, $to);
}

$cacheKey = sprintf('po_statistics_%s_%s_%s', $key, $from ?? '0', $to ?? '0');
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($callback, $from, $to) {
$item->expiresAfter(3600);
return $callback($from, $to);
});
}

private function addFromToRange(QueryBuilder $qb, null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null, string $field = "po.creation_date"): void
Expand Down Expand Up @@ -40,13 +71,15 @@ private function addFromToRange(QueryBuilder $qb, null|\DateTimeInterface|string
*/
public function getSubmittedPaymentOrdersCount(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): int
{
$qb = $this->entityManager->createQueryBuilder();
$qb->select('COUNT(po.id)')
->from(PaymentOrder::class, 'po');
return $this->cached('submittedCount', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) {
$qb = $this->entityManager->createQueryBuilder();
$qb->select('COUNT(po.id)')
->from(PaymentOrder::class, 'po');

$this->addFromToRange($qb, $from, $to);
$this->addFromToRange($qb, $from, $to);

return (int) $qb->getQuery()->getSingleScalarResult();
return (int) $qb->getQuery()->getSingleScalarResult();
});
}

/**
Expand All @@ -58,14 +91,16 @@ public function getSubmittedPaymentOrdersCount(null|\DateTimeInterface|string $f
*/
public function getBookedPaymentOrdersCount(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): int
{
$qb = $this->entityManager->createQueryBuilder();
$qb->select('COUNT(po.id)')
->from(PaymentOrder::class, 'po')
->where('po.booking_date IS NOT NULL');
return $this->cached('bookedCount', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) {
$qb = $this->entityManager->createQueryBuilder();
$qb->select('COUNT(po.id)')
->from(PaymentOrder::class, 'po')
->where('po.booking_date IS NOT NULL');

$this->addFromToRange($qb, $from, $to, "po.booking_date");
$this->addFromToRange($qb, $from, $to, "po.booking_date");

return (int) $qb->getQuery()->getSingleScalarResult();
return (int) $qb->getQuery()->getSingleScalarResult();
});
}

/**
Expand All @@ -77,14 +112,16 @@ public function getBookedPaymentOrdersCount(null|\DateTimeInterface|string $from
*/
public function getMathematicallyCheckedPaymentOrdersCount(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): int
{
$qb = $this->entityManager->createQueryBuilder();
$qb->select('COUNT(po.id)')
->from(PaymentOrder::class, 'po')
->where('po.mathematically_correct.checked = true');
return $this->cached('mathematicallyCheckedCount', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) {
$qb = $this->entityManager->createQueryBuilder();
$qb->select('COUNT(po.id)')
->from(PaymentOrder::class, 'po')
->where('po.mathematically_correct.checked = true');

$this->addFromToRange($qb, $from, $to, "po.mathematically_correct.timestamp");
$this->addFromToRange($qb, $from, $to, "po.mathematically_correct.timestamp");

return (int) $qb->getQuery()->getSingleScalarResult();
return (int) $qb->getQuery()->getSingleScalarResult();
});
}

/**
Expand All @@ -96,14 +133,16 @@ public function getMathematicallyCheckedPaymentOrdersCount(null|\DateTimeInterfa
*/
public function getFactuallyCheckedPaymentOrdersCount(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): int
{
$qb = $this->entityManager->createQueryBuilder();
$qb->select('COUNT(po.id)')
->from(PaymentOrder::class, 'po')
->where('po.factually_correct.checked = true');
return $this->cached('factuallyCheckedCount', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) {
$qb = $this->entityManager->createQueryBuilder();
$qb->select('COUNT(po.id)')
->from(PaymentOrder::class, 'po')
->where('po.factually_correct.checked = true');

$this->addFromToRange($qb, $from, $to, "po.factually_correct.timestamp");
$this->addFromToRange($qb, $from, $to, "po.factually_correct.timestamp");

return (int) $qb->getQuery()->getSingleScalarResult();
return (int) $qb->getQuery()->getSingleScalarResult();
});
}

/**
Expand All @@ -112,62 +151,72 @@ public function getFactuallyCheckedPaymentOrdersCount(null|\DateTimeInterface|st
*/
public function getPaymentOrdersTotalValue(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): float
{
$qb = $this->entityManager->createQueryBuilder();
$qb->select('SUM(po.amount)')
->from(PaymentOrder::class, 'po');
return $this->cached('totalValue', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) {
$qb = $this->entityManager->createQueryBuilder();
$qb->select('SUM(po.amount)')
->from(PaymentOrder::class, 'po');

$this->addFromToRange($qb, $from, $to);
$this->addFromToRange($qb, $from, $to);

// We need to divide the result by 100 because the value is stored in cents.
return ((float) $qb->getQuery()->getSingleScalarResult()) / 100;
// We need to divide the result by 100 because the value is stored in cents.
return ((float) $qb->getQuery()->getSingleScalarResult()) / 100;
});
}

public function getPaymentOrdersAverageValue(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): float
{
$qb = $this->entityManager->createQueryBuilder();
$qb->select('AVG(po.amount)')
->from(PaymentOrder::class, 'po');
return $this->cached('averageValue', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) {
$qb = $this->entityManager->createQueryBuilder();
$qb->select('AVG(po.amount)')
->from(PaymentOrder::class, 'po');

$this->addFromToRange($qb, $from, $to);
$this->addFromToRange($qb, $from, $to);

// We need to divide the result by 100 because the value is stored in cents.
return ((float) $qb->getQuery()->getSingleScalarResult()) / 100;
// We need to divide the result by 100 because the value is stored in cents.
return ((float) $qb->getQuery()->getSingleScalarResult()) / 100;
});
}

public function getPaymentOrdersStdDevValue(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): float
{
$qb = $this->entityManager->createQueryBuilder();
$qb->select('STDDEV(po.amount)')
->from(PaymentOrder::class, 'po');
return $this->cached('stdDevValue', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) {
$qb = $this->entityManager->createQueryBuilder();
$qb->select('STDDEV(po.amount)')
->from(PaymentOrder::class, 'po');

$this->addFromToRange($qb, $from, $to);
$this->addFromToRange($qb, $from, $to);

// We need to divide the result by 100 because the value is stored in cents.
return ((float) $qb->getQuery()->getSingleScalarResult()) / 100;
// We need to divide the result by 100 because the value is stored in cents.
return ((float) $qb->getQuery()->getSingleScalarResult()) / 100;
});
}

public function getPaymentOrdersMaxValue(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): float
{
$qb = $this->entityManager->createQueryBuilder();
$qb->select('MAX(po.amount)')
->from(PaymentOrder::class, 'po');
return $this->cached('maxValue', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) {
$qb = $this->entityManager->createQueryBuilder();
$qb->select('MAX(po.amount)')
->from(PaymentOrder::class, 'po');

$this->addFromToRange($qb, $from, $to);
$this->addFromToRange($qb, $from, $to);

// We need to divide the result by 100 because the value is stored in cents.
return ((float) $qb->getQuery()->getSingleScalarResult()) / 100;
// We need to divide the result by 100 because the value is stored in cents.
return ((float) $qb->getQuery()->getSingleScalarResult()) / 100;
});
}

public function getPaymentOrdersMinValue(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): float
{
$qb = $this->entityManager->createQueryBuilder();
$qb->select('MIN(po.amount)')
->from(PaymentOrder::class, 'po');
return $this->cached('minValue', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) {
$qb = $this->entityManager->createQueryBuilder();
$qb->select('MIN(po.amount)')
->from(PaymentOrder::class, 'po');

$this->addFromToRange($qb, $from, $to);
$this->addFromToRange($qb, $from, $to);

// We need to divide the result by 100 because the value is stored in cents.
return ((float) $qb->getQuery()->getSingleScalarResult()) / 100;
// We need to divide the result by 100 because the value is stored in cents.
return ((float) $qb->getQuery()->getSingleScalarResult()) / 100;
});
}

/**
Expand All @@ -178,20 +227,22 @@ public function getPaymentOrdersMinValue(null|\DateTimeInterface|string $from =
*/
public function getAverageProcessingTime(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): ?float
{
$qb = $this->entityManager->createQueryBuilder();
$qb->select('AVG(UNIX_TIMESTAMP(po.booking_date) - UNIX_TIMESTAMP(po.creation_date))')
->from(PaymentOrder::class, 'po')
->where('po.booking_date IS NOT NULL');
return $this->cached('averageProcessingTime', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) {
$qb = $this->entityManager->createQueryBuilder();
$qb->select('AVG(UNIX_TIMESTAMP(po.booking_date) - UNIX_TIMESTAMP(po.creation_date))')
->from(PaymentOrder::class, 'po')
->where('po.booking_date IS NOT NULL');

$this->addFromToRange($qb, $from, $to, 'po.booking_date');
$this->addFromToRange($qb, $from, $to, 'po.booking_date');

$res = $qb->getQuery()->getSingleScalarResult();
if ($res === null) {
return null;
}
$res = $qb->getQuery()->getSingleScalarResult();
if ($res === null) {
return null;
}

//Res is in seconds, we need to convert it to days
return (float)$res / 86400;
//Res is in seconds, we need to convert it to days
return (float)$res / 86400;
});
}

/**
Expand All @@ -202,19 +253,21 @@ public function getAverageProcessingTime(null|\DateTimeInterface|string $from =
*/
public function getStddevProcessingTime(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): ?float
{
$qb = $this->entityManager->createQueryBuilder();
$qb->select('STDDEV(UNIX_TIMESTAMP(po.booking_date) - UNIX_TIMESTAMP(po.creation_date))')
->from(PaymentOrder::class, 'po')
->where('po.booking_date IS NOT NULL');
return $this->cached('stddevProcessingTime', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) {
$qb = $this->entityManager->createQueryBuilder();
$qb->select('STDDEV(UNIX_TIMESTAMP(po.booking_date) - UNIX_TIMESTAMP(po.creation_date))')
->from(PaymentOrder::class, 'po')
->where('po.booking_date IS NOT NULL');

$this->addFromToRange($qb, $from, $to, 'po.booking_date');
$this->addFromToRange($qb, $from, $to, 'po.booking_date');

$res = $qb->getQuery()->getSingleScalarResult();
if ($res === null) {
return null;
}
$res = $qb->getQuery()->getSingleScalarResult();
if ($res === null) {
return null;
}

//Res is in seconds, we need to convert it to days
return (float)$res / 86400;
//Res is in seconds, we need to convert it to days
return (float)$res / 86400;
});
}
}
2 changes: 1 addition & 1 deletion templates/admin/dashboard.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</tbody>
</table>

<h6>Geldbeträge</h6>
<h6>Geldbeträge (eingereicht)</h6>

<table class="table table-sm table-striped table-hover">
<thead>
Expand Down

0 comments on commit 83e6206

Please sign in to comment.