From 7525a891545242204ae2666008a716058b46db3d Mon Sep 17 00:00:00 2001 From: Jepp9350 <2jepp9350@gmail.com> Date: Wed, 12 Feb 2025 16:39:52 +0100 Subject: [PATCH] Refactor database operations and enhance routes for consistency Refactored database methods to improve code readability, reusability, and error handling. Introduced input validation helper functions and parsing capabilities for objects with optional callbacks. Added new routes for handling categories and improved product-related functionality to align with the new architecture. --- services/nginx/app/classes/db.php | 8 -- services/nginx/app/classes/response.php | 19 +++ services/nginx/app/objects/bookings_o.php | 2 +- services/nginx/app/objects/categories_o.php | 83 +++++++++++ services/nginx/app/objects/products_o.php | 8 +- services/nginx/app/routes/bookingsRoute.php | 7 +- services/nginx/app/routes/categoriesRoute.php | 132 ++++++++++++++++++ .../nginx/app/routes/economicInvoiceRoute.php | 7 +- services/nginx/app/routes/productsRoute.php | 116 ++++++++------- services/nginx/app/traits/db_object_t.php | 32 ++++- services/nginx/app/traits/route_t.php | 27 ++++ 11 files changed, 375 insertions(+), 66 deletions(-) create mode 100644 services/nginx/app/objects/categories_o.php create mode 100644 services/nginx/app/routes/categoriesRoute.php diff --git a/services/nginx/app/classes/db.php b/services/nginx/app/classes/db.php index c4f0a3c3..8e550847 100644 --- a/services/nginx/app/classes/db.php +++ b/services/nginx/app/classes/db.php @@ -86,14 +86,6 @@ class db return $result->fetch_row()[0]; } - public function add_object(string $table, array $data): void - { - $columns = implode(', ', array_keys($data)); - $values = implode("', '", array_values($data)); - $sql = "INSERT INTO $table ($columns) VALUES ('$values')"; - $this->query($sql); - } - public function insert_id(): int { return $this->conn->insert_id; diff --git a/services/nginx/app/classes/response.php b/services/nginx/app/classes/response.php index 469bddc4..4945bf0b 100644 --- a/services/nginx/app/classes/response.php +++ b/services/nginx/app/classes/response.php @@ -142,6 +142,25 @@ class response implements response_i return $data[$key] ?? null; } + /** + * This function checks if a request parameter is set. + * This is different from getRequestParameter because this function allows for empty & null values. + * @param string $key + * @return bool + */ + public function isRequestParameterSet(string $key): bool + { + // Get the request data if the method is POST, PUT or PATCH + if ($_SERVER['REQUEST_METHOD'] === 'POST' || $_SERVER['REQUEST_METHOD'] === 'PUT' || $_SERVER['REQUEST_METHOD'] === 'PATCH') { + $data = json_decode(file_get_contents('php://input'), true); + } + // Get the request data if the method is GET, DELETE or OPTIONS + if ($_SERVER['REQUEST_METHOD'] === 'GET' || $_SERVER['REQUEST_METHOD'] === 'DELETE' || $_SERVER['REQUEST_METHOD'] === 'OPTIONS') { + $data = $_GET; + } + return array_key_exists($key, $data); + } + public function parseFilters(?string $filters): array|null { if ($filters) { diff --git a/services/nginx/app/objects/bookings_o.php b/services/nginx/app/objects/bookings_o.php index 2080800d..0bdd4547 100644 --- a/services/nginx/app/objects/bookings_o.php +++ b/services/nginx/app/objects/bookings_o.php @@ -91,7 +91,7 @@ class bookings_o extends db $this->regNrTrailer = new object_property($this->table, $this->id, 'regNrTrailer', 'string', true); $this->washCertificateEmail = new object_property($this->table, $this->id, 'washCertificateEmail', 'string', true); $this->date = new object_property($this->table, $this->id, 'date', 'string', true); - $this->department = new object_property($this->table, $this->id, 'department', 'string', true); + $this->department = new object_property($this->table, $this->id, 'department', 'int', true); $this->pickup_bool = new object_property($this->table, $this->id, 'pickup_bool', 'int', true); $this->notes = new object_property($this->table, $this->id, 'notes', 'string', true); $this->washCertificateStatus = new object_property($this->table, $this->id, 'washCertificateStatus', 'string', true); diff --git a/services/nginx/app/objects/categories_o.php b/services/nginx/app/objects/categories_o.php new file mode 100644 index 00000000..ce5809c6 --- /dev/null +++ b/services/nginx/app/objects/categories_o.php @@ -0,0 +1,83 @@ +setTable('categories'); + } + + /** + * @param string $name + * @param string $description + * @throws Exception If the object is not selected, it throws an exception + */ + public function add(string $name, string $description): void + { + global /** @var db $db */ + $db; + // Avoid SQL injection + $name = $db->escape_string($name); + $description = $db->escape_string($description); + $tmp_id = self::add_object([ + 'name' => $name, + 'description' => $description + ]); + + // Transfer the temporary id to the current object + $this->id = $tmp_id; + + // Set the values of the object properties + $this->getObjectProperties(); + + // Trigger the object changed event + self::objectChanged(); + } + + public function getObjectProperties(): void + { + $this->name = new object_property($this->table, $this->id, 'name', 'string', true); + $this->description = new object_property($this->table, $this->id, 'description', 'string', false); + } + + public function objectChanged(): void + { + //TODO: Add cache invalidation + } + + /** + * Set the description of the category + * @throws Exception If the object is not selected, it throws an exception + */ + public function set_description(string $description): self + { + self::requireSelected(); + $this->description->set($description); + self::objectChanged(); + return $this; + } + + /** + * Set the name of the category + * @throws Exception If the object is not selected, it throws an exception + */ + public function set_name(string $name): self + { + self::requireSelected(); + $this->name->set($name); + self::objectChanged(); + return $this; + } +} \ No newline at end of file diff --git a/services/nginx/app/objects/products_o.php b/services/nginx/app/objects/products_o.php index ec2b5768..55045261 100644 --- a/services/nginx/app/objects/products_o.php +++ b/services/nginx/app/objects/products_o.php @@ -139,13 +139,17 @@ class products_o extends db } } - public function listObjectsByCategory(string $category): array + public function listObjectsByCategory(string $category, $parseFunction = null): array { global $db; $category = $db->escape_string($category); $sql = "SELECT * FROM $this->table WHERE category = '$category'"; $result = $db->query($sql); - return $db->fetch_all($result); + $objects = $db->fetch_all($result); + if ($parseFunction) { + $objects = array_map($parseFunction, $objects); + } + return $objects; } public function asArray(): array diff --git a/services/nginx/app/routes/bookingsRoute.php b/services/nginx/app/routes/bookingsRoute.php index 194963b6..01d08a18 100644 --- a/services/nginx/app/routes/bookingsRoute.php +++ b/services/nginx/app/routes/bookingsRoute.php @@ -37,7 +37,12 @@ class bookingsRoute $bookings_o = new bookings_o(); // Return the list of bookings $response->success( - $bookings_o->parseBookings($bookings_o->listObjectsWithPaginationIfSet()) + $bookings_o->parseBookings($bookings_o->listObjectsWithPaginationIfSet( + function ($booking) { + $booking['department'] = (int)$booking['department']; + return $booking; + } + )) ); } else { // Log the incident diff --git a/services/nginx/app/routes/categoriesRoute.php b/services/nginx/app/routes/categoriesRoute.php new file mode 100644 index 00000000..06aed687 --- /dev/null +++ b/services/nginx/app/routes/categoriesRoute.php @@ -0,0 +1,132 @@ +get('/categories', function () { + + // Require the user to be logged in + global $response; + $this->requirePermission('list_categories'); + // Get the user object + $user = (new authentication())->get_user(); + // Check if the request was successful + if ($user) { + // Log the incident + (new logs_o())->add('categories', 'global', 1, $user->id, 'LIST_CATEGORIES', 'User listed categories'); + // Return the list of departments + $response->success( + (new categories_o()) + ->setSearchableFields([ + // The fields that can be searched. This would otherwise make it possible to get secret information from the database, simply by searching for it and getting the result count back + 'id', + 'name', + 'description', + ]) + ->listObjectsWithPaginationIfSet( + function ($category) use ($user) { + // Return the object as an array + return [ + 'id' => (int)$category['id'], + 'name' => (string)$category['name'], + 'description' => (string)$category['description'], + 'created_at' => (string)$category['created_at'], + 'updated_at' => (string)$category['updated_at'], + ]; + } + ) + ); + } else { + // Log the incident + (new logs_o())->add('categories', 'global', 1, 0, 'LIST_CATEGORIES', 'User tried to list categories without being logged in'); + // Return an error + $response->error('Invalid session', 400); + } + }); + + $this->post('/categories', function () { + + // Require the user to be logged in + global $response; + $this->requirePermission('add_category'); + // Get the user object + $user = (new authentication())->get_user(); + // Check if the request was successful + if ($user) { + // Log the incident + (new logs_o())->add('categories', 'global', 1, $user->id, 'ADD_CATEGORY', 'User added a category'); + // Get the request data + $name = $response->getRequestParameter('name') ?? null; + $description = $response->getRequestParameter('description') ?? ''; + // Check if the required fields are set + if ($name) { + // Add the category + (new categories_o())->add($name, $description ?? ''); + // Return a success message + $response->success('Category added'); + } else { + // Return an error + $response->error('Missing required fields', 400); + } + + } else { + // Log the incident + (new logs_o())->add('categories', 'global', 1, 0, 'ADD_CATEGORY', 'User tried to add a category without being logged in'); + // Return an error + $response->error('Invalid session', 400); + } + }); + + $this->put('/categories', function () { + + // Require the user to be logged in + global $response; + $this->requirePermission('edit_category'); + // Get the user object + $user = (new authentication())->get_user(); + // Check if the request was successful + if ($user) { + // Log the incident + (new logs_o())->add('categories', 'global', 1, $user->id, 'EDIT_CATEGORY', 'User edited a category'); + // Get the request data + $id = $response->getRequestParameter('id') ?? null; + $name = $response->getRequestParameter('name') ?? null; + $description = $response->getRequestParameter('description') ?? ''; + + // Check what fields are set + self::requireParameters(['id']); + // Get the category object + $category = (new categories_o())->select((int)$id); + // Check if the category exists + if (!$category->exists()) { + // Return an error + $response->error('Category not found', 404); + } + // Update the category fields that are set + if (self::isParametersSet(['name'])) { + $category->name->set($name); + } + if (self::isParametersSet(['description'])) { + $category->description->set($description); + } + // Return a success message + $response->success('Category updated'); + } else { + // Log the incident + (new logs_o())->add('categories', 'global', 1, 0, 'EDIT_CATEGORY', 'User tried to edit a category without being logged in'); + // Return an error + $response->error('Invalid session', 400); + } + }); + } +} \ No newline at end of file diff --git a/services/nginx/app/routes/economicInvoiceRoute.php b/services/nginx/app/routes/economicInvoiceRoute.php index 49d08b0a..29b3c84a 100644 --- a/services/nginx/app/routes/economicInvoiceRoute.php +++ b/services/nginx/app/routes/economicInvoiceRoute.php @@ -22,7 +22,7 @@ class economicInvoiceRoute global /** @var response $response */ /** @var router $router */ $router, $response; - + $this->post('/economic/invoice/draft/export', function () { global $response; @@ -252,11 +252,12 @@ class economicInvoiceRoute // Truck Wash - [department name], [date], (?)Ref(erence): [reference], Reg 1: [reg_1], (?)Reg 2: [reg_2], (?)Reg 3: [reg_3] // (?)[note] // Example: - // 2021-09-01, Truck Wash - Administration, Ref: 123456, Reg 1: ABC123, Reg 2: DEF456, Reg 3: GHI789 + // 12-02-2025 13:27, Truck Wash - Administration, Ref: 123456, Reg 1: ABC123, Reg 2: DEF456, Reg 3: GHI789 // This is a note for the invoice // // (?) = Optional - $economic_invoice_draft->addLineTEXT("[ " . $order->created_at->value() . ' ' . $department_name . ' #' . $order->id . " ]"); + $parsed_date = date('d/m/Y H:i', strtotime($order->created_at->value())); + $economic_invoice_draft->addLineTEXT("[ " . $parsed_date . ' ' . $department_name . ' #' . $order->id . " ]"); // If there's a reference, add it to the invoice if ($order->reference->value() !== '') { $economic_invoice_draft->addLineTEXT('Reference:'); diff --git a/services/nginx/app/routes/productsRoute.php b/services/nginx/app/routes/productsRoute.php index 993b0816..8091a6ea 100644 --- a/services/nginx/app/routes/productsRoute.php +++ b/services/nginx/app/routes/productsRoute.php @@ -14,6 +14,22 @@ class productsRoute public function run(): void { $this->get('/products', function () { + function parseProduct($product): array + { + return [ + 'id' => (int)$product['id'], + 'name' => (string)$product['name'], + 'description' => (string)$product['description'], + 'price' => (int)$product['price'], + 'category' => (int)$product['category'], + 'piktogram' => (string)$product['piktogram'], + 'economic_product_id' => (int)$product['economic_product_id'], + 'apply_category_discount' => (int)$product['apply_category_discount'], + 'created_at' => (string)$product['created_at'], + 'updated_at' => (string)$product['updated_at'] + ]; + } + // Require the user to be logged in global $response; $this->requirePermission('list_products'); @@ -44,12 +60,18 @@ class productsRoute if (isset($data['department_id'])) { // Return the list of products $response->success( - (new products_o())->applyDepartmentPricing((array)(new products_o())->listObjectsWithPaginationIfSet(), (int)$data['department_id']) + (new products_o())->applyDepartmentPricing((array)(new products_o())->listObjectsWithPaginationIfSet( + function ($product) { + return parseProduct($product); + } + ), (int)$data['department_id']) ); } // Return the list of products $response->success( - (new products_o())->listObjectsWithPaginationIfSet() + (new products_o())->listObjectsWithPaginationIfSet(function ($product) { + return parseProduct($product); + }) ); } else { // Log the incident @@ -67,31 +89,31 @@ class productsRoute $user = (new authentication())->get_user(); // Check if the request was successful if ($user) { - // Get the post data - $data = json_decode(file_get_contents('php://input'), true); + // Get the request data + $name = $response->getRequestParameter('name'); + $description = $response->getRequestParameter('description') ?? null; + $price = $response->getRequestParameter('price'); + $category = $response->getRequestParameter('category') ?? null; + $piktogram = $response->getRequestParameter('piktogram') ?? null; + $economicProductId = $response->getRequestParameter('economicProductId') ?? null; // Check if the required fields are set - if (!isset($data['name'])) { - $response->error('Name is required', 400); - } - if (!isset($data['description'])) { - $response->error('Description is required', 400); - } - if (!isset($data['price'])) { - $response->error('Price is required', 400); - } - if (!isset($data['category'])) { - $response->error('Category is required', 400); - } - if (!isset($data['piktogram'])) { - $response->error('Piktogram is required', 400); - } - if (!isset($data['economicProductId'])) { - $response->error('Economic product ID is required', 400); - } + self::requireParameters( + [ + 'name', + 'price' + ] + ); // Add the product - (new products_o())->add($data['name'], $data['description'], $data['price'], $data['category'], $data['piktogram'], $data['economicProductId']); + (new products_o())->add( + (string)$name, + (string)$description ?? '', + (int)$price, + (int)$category ?? '', + (int)$piktogram ?? null, + (int)$economicProductId ?? null + ); // Log the incident - (new logs_o())->add('products', 'global', 1, $user->id, 'ADD_PRODUCT', 'Product name: ' . $data['name']); + (new logs_o())->add('products', 'global', 1, $user->id, 'ADD_PRODUCT', 'Product name: ' . $name); // Return a success message $response->success(['message' => 'Product added successfully']); } else { @@ -110,39 +132,37 @@ class productsRoute $user = (new authentication())->get_user(); // Check if the request was successful if ($user) { - // Get the post data - $data = json_decode(file_get_contents('php://input'), true); // Check if the required fields are set - if (!isset($data['id'])) { - $response->error('ID is required', 400); + self::requireParameters(['id']); + $id = $response->getRequestParameter('id'); + $product = (new products_o())->select((int)$id); + // Check if the product exists + if (!$product->exists()) { + $response->error('Product not found', 404); } - if (!isset($data['name'])) { - $response->error('Name is required', 400); + // Update the fields that are set + if (self::isParametersSet(['name'])) { + $product->name->set($response->getRequestParameter('name')); } - if (!isset($data['description'])) { - $response->error('Description is required', 400); + if (self::isParametersSet(['description'])) { + $product->description->set($response->getRequestParameter('description') ?? ''); } - if (!isset($data['price'])) { - $response->error('Price is required', 400); + if (self::isParametersSet(['price'])) { + $product->price->set($response->getRequestParameter('price')); } - if (!isset($data['category'])) { - $response->error('Category is required', 400); + if (self::isParametersSet(['category'])) { + $product->category->set($response->getRequestParameter('category') ?? ''); } - if (!isset($data['piktogram'])) { - $response->error('Piktogram is required', 400); + if (self::isParametersSet(['piktogram'])) { + $product->piktogram->set($response->getRequestParameter('piktogram') ?? ''); } - if (!isset($data['economicProductId'])) { - $response->error('Economic product ID is required', 400); + if (self::isParametersSet(['economic_product_id'])) { + $product->economic_product_id->set($response->getRequestParameter('economic_product_id') ?? ''); } - if (!isset($data['applyCategoryDiscount'])) { - $data['applyCategoryDiscount'] = true; - } else { - $data['applyCategoryDiscount'] = (bool)$data['applyCategoryDiscount']; + if (self::isParametersSet(['apply_category_discount'])) { + $product->apply_category_discount->set($response->getRequestParameter('apply_category_discount') ? 1 : 0); } - // Edit the product - (new products_o())->edit($data['id'], $data['name'], $data['description'], $data['price'], $data['category'], $data['piktogram'], $data['economicProductId'], $data['applyCategoryDiscount']); - // Log the incident - (new logs_o())->add('products', 'global', 1, $user->id, 'EDIT_PRODUCT', 'Product id: ' . $data['id']); + (new logs_o())->add('products', 'global', 1, $user->id, 'EDIT_PRODUCT', 'Product id: ' . $id); // Return a success message $response->success(['message' => 'Product edited successfully']); } else { diff --git a/services/nginx/app/traits/db_object_t.php b/services/nginx/app/traits/db_object_t.php index cc5652bf..8986dbe7 100644 --- a/services/nginx/app/traits/db_object_t.php +++ b/services/nginx/app/traits/db_object_t.php @@ -116,7 +116,7 @@ trait db_object_t if ($page && $limit) { return $this->listObjectsWithPagination($page, $limit, $search, $filters, $order, $parseFunction); } - return $this->listObjects(); + return $this->listObjects($parseFunction); } /** @@ -247,12 +247,17 @@ trait db_object_t * List ALL objects in the table (THIS INCLUDES DELETED OBJECTS) * @return array The list of objects in the table */ - public function listObjects(): array + public function listObjects($parseFunction = null): array { global $db; $sql = "SELECT * FROM $this->table"; $result = $db->query($sql); - return $db->fetch_all($result); + $objects = $db->fetch_all($result); + // Parse the objects, if a parse function is provided + if ($parseFunction) { + $objects = array_map($parseFunction, $objects); + } + return $objects; } /** @@ -523,6 +528,27 @@ trait db_object_t return $this; } + /** + * Add an object to the database, using the data provided + * @param array $data The data to add the object with (E.g. ['name' => 'John', 'email' => 'email@example.com']) + * @return int The id of the new object + * @throws Exception If the creation of the object fails, it throws an exception + */ + public function add_object(array $data): int + { + global /** @var db $db */ + $db; + try { + $columns = implode(', ', array_keys($data)); + $values = implode("', '", array_values($data)); + $sql = "INSERT INTO $this->table ($columns) VALUES ('$values')"; + $db->query($sql); + return $db->insert_id(); + } catch (Exception $e) { + throw new Exception($e->getMessage()); + } + } + /** * Does this object exist in the database? * @return bool True if the object exists in the database, false otherwise diff --git a/services/nginx/app/traits/route_t.php b/services/nginx/app/traits/route_t.php index 89923568..20769e15 100644 --- a/services/nginx/app/traits/route_t.php +++ b/services/nginx/app/traits/route_t.php @@ -20,6 +20,33 @@ trait route_t // Add the routes here } + public function requireParameters(array $parameters): void + { + global $response; + $missing_parameters = []; + // Check if all required parameters are set + foreach ( $parameters as $parameter ) { + if (!$response->getRequestParameter($parameter)) { + $missing_parameters[] = $parameter; + } + } + if (count($missing_parameters) > 0) { + $response->error('Missing required parameters: ' . implode(', ', $missing_parameters), 400); + } + } + + public function isParametersSet(array $parameters): bool + { + global $response; + // Check if all required parameters are set + foreach ( $parameters as $parameter ) { + if (!$response->isRequestParameterSet($parameter)) { + return false; + } + } + return true; + } + /** * GET route * @param string $route Example: /home, /home/{id}