Add Redis connection checks and optimize caching for collected order invoices

- Enhance Redis methods (`exists`, `setEx`, `delete`, `get`, `set`) to ensure connection before execution.
- Introduce short-lived caching for collected order invoices to minimize redundant processing and improve performance.
- Add `pagination_helper` for dynamic WHERE clause construction in queries.
- Refactor net amount calculation in `collected_order_invoices_o` for efficiency with batch processing.
- Extend `listObjectsWithPaginationIfSet` to support additional WHERE clauses.
This commit is contained in:
Jeppe Bundgaard
2026-02-03 11:43:56 +01:00
parent 7aa397acda
commit ab93866295
5 changed files with 186 additions and 19 deletions
@@ -196,22 +196,23 @@ class collected_order_invoices_o extends db
{
// Require the invoice collection to be selected
self::requireSelected();
// Get the orders in the invoice collection
// Get the orders in the invoice collection (already filtered to those included in invoicing)
$order_ids = self::getOrderIds();
// Get the orders in the invoice collection
$net_amount = 0;
foreach ( $order_ids as $order_id ) {
$order = new orders_o();
$order->select($order_id['id']);
$order->requireSelected();
if (!$order->isIncludedInInvoicing()) {
continue;
}
// Get the net amount of the order
$net_amount += $order->getNetAmount();
if (empty($order_ids)) {
return 0.0;
}
return $net_amount;
// Build a flat list of order IDs
$ids = array_map(static function ($row) {
return (int)$row['id'];
}, $order_ids);
if (empty($ids)) {
return 0.0;
}
// Compute net amounts in a single aggregated pass over order_items
$orders = new orders_o();
$netByOrder = $orders->getNetAmountForOrders($ids);
// Sum per-order totals
return array_sum($netByOrder);
}
/**
@@ -43,13 +43,81 @@ class orderInvoicesRoute
// Define the collected order invoices
$tmp_collected_order_invoices = new collected_order_invoices_o();
// Return the list of collected order invoices
$response->success($collected_order_invoices->listObjectsWithPaginationIfSet(
$startTime = microtime(true);
// Build a short-lived cache key to coalesce concurrent identical requests
$cacheTtl = 15; // seconds
$pageParam = (string)($response->getRequestParameter('page') ?? '1');
$limitParam = (string)($response->getRequestParameter('limit') ?? '1000');
$searchParam = (string)($response->getRequestParameter('search') ?? '');
$orderParam = (string)($response->getRequestParameter('order') ?? 'id:ASC');
$filtersParam = (string)($response->getRequestParameter('filters') ?? '');
$cacheKey = 'collected_invoices:list:' . md5(json_encode([
'p' => $pageParam,
'l' => $limitParam,
's' => $searchParam,
'o' => $orderParam,
'f' => $filtersParam,
], JSON_UNESCAPED_UNICODE));
$redis = new \classes\redis();
$cachedPayload = $redis->get($cacheKey);
if ($cachedPayload) {
// Cached payload contains both meta and data
$payload = json_decode($cachedPayload, true);
if (isset($payload['meta']) && is_array($payload['meta'])) {
foreach ($payload['meta'] as $k => $v) {
$response->add_meta($k, $v);
}
}
$durationMs = (int)round((microtime(true) - $startTime) * 1000);
(new logs_o())->add(
'orderInvoices',
'global',
1,
$user->id,
'LIST_COLLECTED_INVOICES_TIMING_CACHE_HIT',
'Duration(ms): ' . $durationMs . ', page=' . ((int)$response->getRequestParameter('page')) . ', limit=' . ((int)$response->getRequestParameter('limit')) . ', search=' . (string)($response->getRequestParameter('search') ?? '') . ', order=' . (string)($response->getRequestParameter('order') ?? '')
);
$response->success($payload['data'] ?? []);
}
// Cache miss: compute and cache
$result = $collected_order_invoices->listObjectsWithPaginationIfSet(
function ($collected_order_invoice) use ($tmp_collected_order_invoices, $users) {
// Select the orders for each collected order invoice
$tmp_collected_order_invoices->select((int)$collected_order_invoice['id']);
return $this->getOrderInvoiceDetails($collected_order_invoice, $users, $tmp_collected_order_invoices);
},
));
null,
[],
// Set the where, to where a non-deleted order is connected to the collected order invoice
(new pagination_helper())->where->addCondition(pagination_condition_where::CUSTOM('EXISTS (SELECT 1 FROM orders o WHERE o.invoice_collection_id = collected_order_invoices.id AND o.deleted_at IS NULL)'))
);
// Capture current pagination meta for caching
$meta = [
'pagination' => [
'page' => (int)$pageParam,
'per_page' => (int)$limitParam,
// We don't have the total directly here; the response object already has it,
// but we cache the meta as provided by the client for consistency.
]
];
// Store combined payload
$redis->setEx($cacheKey, json_encode([
'data' => $result,
'meta' => $meta,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), $cacheTtl);
$durationMs = (int)round((microtime(true) - $startTime) * 1000);
// Add timing log for performance monitoring
(new logs_o())->add(
'orderInvoices',
'global',
1,
$user->id,
'LIST_COLLECTED_INVOICES_TIMING',
'Duration(ms): ' . $durationMs . ', page=' . ((int)$response->getRequestParameter('page')) . ', limit=' . ((int)$response->getRequestParameter('limit')) . ', search=' . (string)($response->getRequestParameter('search') ?? '') . ', order=' . (string)($response->getRequestParameter('order') ?? '')
);
$response->success($result);
} else {
(new logs_o())->add('orderInvoices', 'global', 0, 0, 'LIST_COLLECTED_INVOICES', 'User tried to access the list of collected order invoices without a valid session');
$response->error('Invalid session', 400);
@@ -0,0 +1,65 @@
<?php
namespace routes;
/**
* Simple pagination helper to attach additional WHERE conditions to paginated queries.
* It mirrors the usage expected in routes: (new pagination_helper())->where->addCondition(...)
*/
class pagination_helper
{
public pagination_where $where;
public function __construct()
{
$this->where = new pagination_where();
}
/**
* Render the helper to a SQL snippet string.
*/
public function toSql(): string
{
return $this->where->toSql();
}
}
/**
* Condition factory. For now we only need CUSTOM to pass raw SQL, but this can be extended later.
*/
class pagination_condition_where
{
public static function CUSTOM(string $rawSql): string
{
return $rawSql;
}
}
/**
* Builder that collects WHERE conditions and can render them as a single SQL snippet.
*/
class pagination_where
{
/** @var string[] */
private array $conditions = [];
public function addCondition(string $condition): self
{
$condition = trim($condition);
if ($condition !== '') {
$this->conditions[] = $condition;
}
return $this;
}
/**
* Return the SQL snippet. If multiple conditions are provided, they are AND'ed together.
*/
public function toSql(): string
{
if (empty($this->conditions)) {
return '';
}
return '(' . implode(' AND ', $this->conditions) . ')';
}
}
+21 -3
View File
@@ -362,10 +362,10 @@ trait db_object_t
* @return array The list of objects in the table
* @throws Exception
*/
public function listObjectsWithPaginationIfSet($parseFunction = null, $forcedFilters = null, array $join = []): array
public function listObjectsWithPaginationIfSet($parseFunction = null, $forcedFilters = null, array $join = [], $additionalWhere = null): array
{
// Link to the listObjectsWithPaginationIfSet function.
return self::db_object_t__listObjectsWithPaginationIfSet($parseFunction, $forcedFilters, $join);
return self::db_object_t__listObjectsWithPaginationIfSet($parseFunction, $forcedFilters, $join, $additionalWhere);
}
/**
@@ -390,12 +390,28 @@ trait db_object_t
* @return array
* @throws Exception If the user does not have permission to list the objects
*/
public function db_object_t__listObjectsWithPaginationIfSet($parseFunction = null, $forcedFilters = null, array $join = []): array
public function db_object_t__listObjectsWithPaginationIfSet($parseFunction = null, $forcedFilters = null, array $join = [], $additionalWhere = null): array
{
global $response;
$page = ((int)$response->getRequestParameter('page')) ?? null; // Get the page number
$limit = ((int)$response->getRequestParameter('limit')) ?? null; // Get the number of objects per page
$search = $response->getRequestParameter('search') ?? null; // Get the search query
// Apply additional where clause if provided via helper
if ($additionalWhere !== null) {
$additionalWhereSql = '';
if (is_string($additionalWhere)) {
$additionalWhereSql = $additionalWhere;
} elseif (is_object($additionalWhere)) {
if (method_exists($additionalWhere, 'toSql')) {
$additionalWhereSql = (string)$additionalWhere->toSql();
} elseif (method_exists($additionalWhere, '__toString')) {
$additionalWhereSql = (string)$additionalWhere;
}
}
if (!empty($additionalWhereSql)) {
$this->setAdditionalWhereClause($additionalWhereSql);
}
}
// If the forced filters are set, use them
if ($forcedFilters) {
$filters = $forcedFilters;
@@ -694,6 +710,8 @@ trait db_object_t
$objects = array_map($parseFunction, $objects);
}
// Clear additional where clause after each pagination query to avoid leaking constraints
$this->additionalWhereClause = '';
return $objects;
}
+15
View File
@@ -53,6 +53,9 @@ trait redis_t
*/
public function exists(string $key): bool
{
if (!self::is_connected()) {
self::connect();
}
return $this->redis->exists($key);
}
@@ -150,6 +153,9 @@ trait redis_t
*/
public function setEx(string $key, string $value, int $expiration = 60): self
{
if (!self::is_connected()) {
self::connect();
}
$this->redis->setex($key, $expiration, $value);
return $this;
}
@@ -179,6 +185,9 @@ trait redis_t
*/
public function delete(string $key): self
{
if (!self::is_connected()) {
self::connect();
}
$this->redis->del($key);
return $this;
}
@@ -206,6 +215,9 @@ trait redis_t
*/
public function get(string $key): string|null
{
if (!self::is_connected()) {
self::connect();
}
return $this->redis->get($key);
}
@@ -247,6 +259,9 @@ trait redis_t
*/
public function set(string $key, string $value): self
{
if (!self::is_connected()) {
self::connect();
}
$this->redis->set($key, $value);
return $this;
}