From 5b88ef9dc3e141cac378f57ac5e125a4bfa0d8d4 Mon Sep 17 00:00:00 2001 From: Jepp9350 <2jepp9350@gmail.com> Date: Thu, 26 Jun 2025 08:55:58 +0200 Subject: [PATCH] Refactor order and order item management: add cache invalidation, improve object change tracking, and enhance `asArray` conversion with caching and expiration support. --- .../objects/collected_order_invoices_o.php | 31 ++++++- services/nginx/app/objects/order_items_o.php | 49 ++++++++-- services/nginx/app/objects/orders_o.php | 93 ++++++++++++------- services/nginx/app/routes/ordersRoute.php | 5 +- services/nginx/app/traits/db_object_t.php | 43 ++++++++- 5 files changed, 172 insertions(+), 49 deletions(-) diff --git a/services/nginx/app/objects/collected_order_invoices_o.php b/services/nginx/app/objects/collected_order_invoices_o.php index 291031be..70f88757 100644 --- a/services/nginx/app/objects/collected_order_invoices_o.php +++ b/services/nginx/app/objects/collected_order_invoices_o.php @@ -90,10 +90,19 @@ class collected_order_invoices_o extends db } /** + * Get the invoice collection as an array * @throws ApiErrorException If the payment method is Stripe and the request fails + * @throws Exception If the request was not successful */ public function asArray(): array { + // Require the invoice collection to be selected + self::requireSelected(); + // Check if the object is cached + $cached = self::getCached('asArray', $this->id); + if ($cached !== null) { + return (array)$cached; + } $tmp = [ 'id' => (int)$this->id, 'customer_number' => (int)$this->customer_number->value(), @@ -130,6 +139,9 @@ class collected_order_invoices_o extends db ]); $tmp['stripe'] = $stripe_details; } + // Cache the result + self::cache('asArray', $tmp, $this->id); + self::setCachedExpiration('asArray', self::$asArrayCacheExpiration, $this->id); return $tmp; } @@ -141,6 +153,8 @@ class collected_order_invoices_o extends db */ public function getOrders(bool $count = false): array|int { + // Require the invoice collection to be selected + self::requireSelected(); $orders = new orders_o(); $order_ids = $orders->getFieldsWhere( [ @@ -542,7 +556,8 @@ class collected_order_invoices_o extends db public function objectChanged(): void { - //TODO: Add cache invalidation + // Invalidate the cache, so the next time the object is requested, it will be fetched from the database + self::deleteCached('asArray', $this->id); } /** @@ -609,6 +624,8 @@ class collected_order_invoices_o extends db foreach ( $orders as $order ) { self::addInvoiceToDraft($order['id'], true, $draft_id, $currency); } + // Object changed + self::objectChanged(); return $this; } @@ -837,6 +854,8 @@ class collected_order_invoices_o extends db // Set the order to the new invoice collection $order_object->invoice_collection_id->set($tmp->id); } + // Invalidate the cache for the invoice collection + $this->objectChanged(); } /** @@ -912,6 +931,7 @@ class collected_order_invoices_o extends db $transaction->invoice_collection_id->set($this->id); $transaction->created_at->set(self::getFirstDayOfMonth($this->created_at->value())); $transaction->objectChanged(); + $this->objectChanged(); // Add the vehicle subscriptions to the transaction $order_items_o = new order_items_o(); foreach ( $vehicle_array as $vehicle ) { @@ -974,6 +994,7 @@ class collected_order_invoices_o extends db $order_item_object->requireSelected(); $order_item_object->include_in_invoice->set(0); $order_item_object->price->set((int)self::getWashSubscriptionPrice((new products_o())->select((int)$order_item['product_id'])->price->value()) / 2); + $order_item_object->objectChanged(); } // Check if the order item product id is equal any of the vehicle subscription addons product id if (isset($vehicle_regs[$order['reg_1']]['addons'][$order_item['product_id']])) { @@ -999,10 +1020,13 @@ class collected_order_invoices_o extends db $order_item_object->requireSelected(); $order_item_object->include_in_invoice->set(0); $order_item_object->price->set($tmp_subscription_price); + $order_item_object->objectChanged(); } } + $order_object->objectChanged(); } } + $this->objectChanged(); //print_r($vehicle_array); } @@ -1034,6 +1058,8 @@ class collected_order_invoices_o extends db $order->requireSelected(); $order->delete(); } + // Clear the cache for the invoice collection + $this->objectChanged(); } private static function getFirstDayOfMonth(string $timestamp): string @@ -1137,7 +1163,10 @@ class collected_order_invoices_o extends db $order_item_object->requireSelected(); $order_item_object->include_in_invoice->set(0); $order_item_object->price->set(0); + $order_item_object->objectChanged(); } } + // Invalidate the cache for the invoice collection + $this->objectChanged(); } } \ No newline at end of file diff --git a/services/nginx/app/objects/order_items_o.php b/services/nginx/app/objects/order_items_o.php index 6db043a6..3cf3c746 100644 --- a/services/nginx/app/objects/order_items_o.php +++ b/services/nginx/app/objects/order_items_o.php @@ -4,6 +4,7 @@ namespace objects; use classes\db; use classes\object_property; +use Exception; use traits\db_object_t; class order_items_o extends db @@ -62,11 +63,6 @@ class order_items_o extends db $this->setTable('order_items'); } - public function objectChanged(): void - { - // No need to invalidate the cache, since the order_items object is not cached - } - public function getOrderItemById(int $id): order_items_o { global $db; @@ -113,11 +109,37 @@ class order_items_o extends db if ($related_item_id) { $this->related_item_id->set($related_item_id); } - } catch (\Exception $e) { + // Inform the order object that a new item has been added + $this->getOrder()->objectChanged(); + } catch (Exception $e) { $response->error($e->getMessage()); } } + /** + * @throws Exception + */ + public function objectChanged(): void + { + // This method is called when the object is changed, to inform the order object + // that an item has been added, edited or removed. + // This is used to update the order total and other related properties. + $order = $this->getOrder(); + $order->objectChanged(); + } + + /** + * Get the order object associated with this order item + * @return orders_o The order object associated with this order item + * @throws Exception If the order item is not selected + * @throws Exception If no order is selected + */ + public function getOrder(): orders_o + { + self::requireSelected(); + return (new orders_o())->select($this->id); + } + public function edit(int $id, int $order_id, int $product_id, string $reference, string $notes, int $cashier_id, int $price, int $quantity): void { global $db, $response; @@ -132,7 +154,9 @@ class order_items_o extends db // Set the values of the object properties $this->getObjectProperties(); - } catch (\Exception $e) { + // Inform the order object that an item has been edited + $this->objectChanged(); + } catch (Exception $e) { $response->error($e->getMessage()); } } @@ -173,13 +197,17 @@ class order_items_o extends db if ($notes) { $this->notes->set($notes); } + $this->objectChanged(); - } catch (\Exception $e) { + } catch (Exception $e) { $response->error($e->getMessage()); } } + /** + * @throws Exception + */ public function removeOrderItem(int $id): void { // TODO: Implement delete() method instead @@ -187,6 +215,8 @@ class order_items_o extends db $this->id = $id; $sql = "DELETE FROM $this->table WHERE id = $this->id or related_item_id = $this->id"; $db->query($sql); + // Inform the order object that an item has been removed + $this->getOrder()->objectChanged(); } public function getItemAsArray(): array @@ -236,7 +266,8 @@ class order_items_o extends db $db->query($sql); // Set the values of the object properties $this->getObjectProperties(); - } catch (\Exception $e) { + $this->objectChanged(); + } catch (Exception $e) { $response->error($e->getMessage()); } } diff --git a/services/nginx/app/objects/orders_o.php b/services/nginx/app/objects/orders_o.php index 7ec806e6..9f5c7d82 100644 --- a/services/nginx/app/objects/orders_o.php +++ b/services/nginx/app/objects/orders_o.php @@ -34,6 +34,7 @@ class orders_o extends db public object_property $wash_id; // The XL Vask Wash ID, if any public object_property $lane; // The lane used for the order, if any + public function structure(): void { $this->setTable('orders'); @@ -116,9 +117,49 @@ class orders_o extends db { // Set the deleted_at property to the current timestamp $this->deleted_at->set(date('Y-m-d H:i:s')); + $this->objectChanged(); // Save the object } + /** + * @throws Exception If the order is not selected + * This function is called when the order object is changed. + */ + public function objectChanged(): void + { + self::requireSelected(); + // Reset the cached object + self::deleteCached('asArray', $this->id); + // Inform the order collection that the order has changed + $this->getOrderCollection()->objectChanged(); + } + + /** + * Get the order collection for the order + * @return collected_order_invoices_o The order collection + * @throws Exception If the order is not selected + */ + public function getOrderCollection(): collected_order_invoices_o + { + self::requireSelected(); + $order_collection = new collected_order_invoices_o(); + $order_collection->select($this->invoice_collection_id->value()); + if (!$order_collection->exists()) { + throw new Exception('Order collection not found'); + } + return $order_collection; + } + + public function exists(): bool + { + // Check if the id is greater than 0, and that the deleted_at property is null + if ($this->id > 0) { + $this->getObjectProperties(); + return $this->deleted_at->value() === null; + } + return false; + } + public function restore(): void { // Set the deleted_at property to null @@ -192,16 +233,6 @@ class orders_o extends db $this->{$data['field']}->set($data['value']); } - public function exists(): bool - { - // Check if the id is greater than 0, and that the deleted_at property is null - if ($this->id > 0) { - $this->getObjectProperties(); - return $this->deleted_at->value() === null; - } - return false; - } - /** * Mark the order as completed * @throws Exception If the order is not selected @@ -223,11 +254,6 @@ class orders_o extends db $this->objectChanged(); } - public function objectChanged(): void - { - // Since the orders object is not cached, there is no need to invalidate the cache - } - /** * Get the order by invoice id * @param int $invoiceId @@ -427,22 +453,6 @@ class orders_o extends db return $count; } - /** - * Get the order collection for the order - * @return collected_order_invoices_o The order collection - * @throws Exception If the order is not selected - */ - public function getOrderCollection(): collected_order_invoices_o - { - self::requireSelected(); - $order_collection = new collected_order_invoices_o(); - $order_collection->select($this->invoice_collection_id->value()); - if (!$order_collection->exists()) { - throw new Exception('Order collection not found'); - } - return $order_collection; - } - /** * Get the wash subscription transactions for a customer * @throws Exception If something goes wrong @@ -473,9 +483,22 @@ class orders_o extends db ); } - public function asArray(): array + /** + * @return array The order as an array + * @throws Exception + * Convert the order object to an array + */ + public function asArray(bool $skipCache = false): array { - return [ + self::requireSelected(); + if (!$skipCache) { + // Check if the object is cached + $cached = self::getCached('asArray', $this->id); + if ($cached) { + return (array)$cached; + } + } + $tmp = [ 'id' => $this->id, 'customer_id' => (int)$this->customer_id->value(), 'cashier_id' => (int)$this->cashier_id->value(), @@ -495,6 +518,10 @@ class orders_o extends db 'lane' => $this->lane->value(), 'closed_at' => (int)$this->invoice_collection_id->value() ? (new collected_order_invoices_o())->select((int)$this->invoice_collection_id->value())->closed_at->value() : null, ]; + // Cache the object + self::cache('asArray', $tmp, $this->id); + self::setCachedExpiration('asArray', self::$asArrayCacheExpiration, $this->id); + return $tmp; } public function getNetAmount(): float diff --git a/services/nginx/app/routes/ordersRoute.php b/services/nginx/app/routes/ordersRoute.php index 3e737e33..f9a97d01 100644 --- a/services/nginx/app/routes/ordersRoute.php +++ b/services/nginx/app/routes/ordersRoute.php @@ -41,7 +41,7 @@ class ordersRoute // Log the incident (new logs_o())->add('orders', 'global', 1, $user->id, 'LIST_ORDERS', 'Successfully listed orders'); // Create economic_module_orders object - $economic_module_orders = new economic_module_orders(); + //$economic_module_orders = new economic_module_orders(); $orders = new orders_o(); if (!$restrict_only_own) { $department_ids = $user->getGroup()->getDepartments(); @@ -216,7 +216,8 @@ class ordersRoute if (isset($data['created_at'])) { $order->created_at->set($data['created_at']); } - // If the department ID is set, validate it + // Void any cached key for the order + $order->objectChanged(); // Log the incident (new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'EDIT_ORDER', 'Successfully updated an order (ID: ' . $data['id'] . ')'); // Return a success message diff --git a/services/nginx/app/traits/db_object_t.php b/services/nginx/app/traits/db_object_t.php index a71a31fa..f8185b4d 100644 --- a/services/nginx/app/traits/db_object_t.php +++ b/services/nginx/app/traits/db_object_t.php @@ -44,10 +44,11 @@ use objects\users_o; trait db_object_t { - public int $id; // The id of the object in the database - private string $table; // The table of the objects in the database (e.g. users) - private array $searchableFields = []; // The fields to search in the database (e.g. ['name', 'email']). If empty, all fields will be searched - private array $whereClauses = []; // The where clauses to add to the pagination query + public static int $asArrayCacheExpiration = 600; // The id of the object in the database + public int $id; // The table of the objects in the database (e.g. users) + private string $table; // The fields to search in the database (e.g. ['name', 'email']). If empty, all fields will be searched + private array $searchableFields = []; // The where clauses to add to the pagination query + private array $whereClauses = []; // The cache expiration time for the asArray function, in seconds. This is used to cache the result of the asArray function to improve performance. Default is 10 minutes (600 seconds). public function __construct() { @@ -744,6 +745,40 @@ trait db_object_t redis->set($this->table . '_' . $objectId . '_' . $key, $data); } + /** + * Set cached object expiration time + * @param string $key The key to set the cached object expiration time + * @param int $seconds The number of seconds to set the cached object expiration time + * @param null $objectId + * @return void + */ + public function setCachedExpiration(string $key, int $seconds, $objectId = null): void + { + // If the object id is not set, use the object id + if (!$objectId) { + $objectId = $this->id; + } + // Set the expiration time for the cached data + redis->expire($this->table . '_' . $objectId . '_' . $key, $seconds); + } + + /** + * Get a cached object key (redis key) + * @param string $key The key to get the cached object + * @return string The cached object key + * @throws Exception If the object is not selected, it throws an exception + */ + public function getCachedKey(string $key, $objectId = null): string + { + self::requireSelected(); + // If the object id is not set, use the object id + if (!$objectId) { + $objectId = $this->id; + } + // Return the cached data key + return $this->table . '_' . $objectId . '_' . $key; + } + /** * Get cached object * @param string $key The key to get the cached object