Introduce endpoints for managing product options and department categories, including listing, creating, editing, and deleting functionalities. Updated related objects and traits to support new operations, including added array serialization methods and improved query handling for better flexibility.
645 lines
22 KiB
PHP
645 lines
22 KiB
PHP
<?php
|
|
|
|
namespace traits;
|
|
|
|
use classes\db;
|
|
use classes\response;
|
|
use Exception;
|
|
use objects\bookings_o;
|
|
use objects\categories_o;
|
|
use objects\cron_o;
|
|
use objects\customer_codes_o;
|
|
use objects\customer_notes_o;
|
|
use objects\customer_vehicles_o;
|
|
use objects\department_categories_o;
|
|
use objects\departments_o;
|
|
use objects\economic_module_orders;
|
|
use objects\logs_o;
|
|
use objects\order_items_o;
|
|
use objects\orders_o;
|
|
use objects\plate_scanners_o;
|
|
use objects\plate_scans_o;
|
|
use objects\product_options_o;
|
|
use objects\products_o;
|
|
use objects\ratelimit_o;
|
|
use objects\tokens_o;
|
|
use objects\user_key_value_pairs_o;
|
|
use objects\user_price_overrides_o;
|
|
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 function __construct()
|
|
{
|
|
$this->structure();
|
|
}
|
|
|
|
/**
|
|
* Structure: Define the table and fields of the objects in the database
|
|
*/
|
|
public function structure(): void
|
|
{
|
|
// Define the table and fields of the objects in the database
|
|
}
|
|
|
|
public function countRowsWhere(array $fieldsAndValues): int
|
|
{
|
|
global $db;
|
|
$table = $this->table;
|
|
$where = [];
|
|
foreach ( $fieldsAndValues as $field => $value ) {
|
|
$where[] = "$field = '$value'";
|
|
}
|
|
$where = implode(' AND ', $where);
|
|
$sql = "SELECT COUNT(*) AS count FROM $table WHERE $where";
|
|
$result = $db->query($sql);
|
|
$row = $db->fetch_assoc($result);
|
|
return $row['count'];
|
|
}
|
|
|
|
public function getFieldsWhere(array $fieldsAndValues, array $fields): array
|
|
{
|
|
global $db;
|
|
$table = $this->table;
|
|
$where = [];
|
|
foreach ( $fieldsAndValues as $field => $value ) {
|
|
// If the value is null, add a where clause to check if the field is null
|
|
if ($value === null) {
|
|
$where[] = "$field IS NULL";
|
|
} else {
|
|
$where[] = "$field = '$value'";
|
|
}
|
|
}
|
|
$where = implode(' AND ', $where);
|
|
$sql = "SELECT " . implode(', ', $fields) . " FROM $table WHERE $where";
|
|
$result = $db->query($sql);
|
|
return $db->fetch_all($result);
|
|
}
|
|
|
|
/**
|
|
* Set the searchable fields
|
|
* @param array $fields The fields to search in the database (e.g. ['name', 'email'])
|
|
* @return users_o|bookings_o|cron_o|customer_codes_o|customer_notes_o|customer_vehicles_o|departments_o|economic_module_orders|logs_o|order_items_o|orders_o|plate_scanners_o|plate_scans_o|products_o|ratelimit_o|tokens_o|user_key_value_pairs_o|user_price_overrides_o|db_object_t
|
|
*/
|
|
public function setSearchableFields(array $fields): self
|
|
{
|
|
$this->searchableFields = $fields;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Add a where clause to the pagination query
|
|
* @param string $field
|
|
* @param mixed $value
|
|
* @return users_o|bookings_o|categories_o|cron_o|customer_codes_o|customer_notes_o|customer_vehicles_o|department_categories_o|departments_o|economic_module_orders|logs_o|order_items_o|orders_o|plate_scanners_o|plate_scans_o|product_options_o|products_o|ratelimit_o|tokens_o|user_key_value_pairs_o|user_price_overrides_o|db_object_t
|
|
*/
|
|
public function addWhereClause(string $field, mixed $value): self
|
|
{
|
|
$this->whereClauses[] = "$field = '$value'";
|
|
return $this;
|
|
}
|
|
|
|
public function __toString(): string
|
|
{
|
|
// Return the object as a string
|
|
return json_encode($this->getArray());
|
|
}
|
|
|
|
/**
|
|
* Get current object row as an array
|
|
* @return array The current object row as an array
|
|
* @throws Exception If object not found
|
|
*/
|
|
public function getArray(): array
|
|
{
|
|
// Get the object from the database
|
|
global $db;
|
|
// Check if the id is set, if not throw an exception
|
|
if (!isset($this->id)) {
|
|
throw new Exception('Object not found');
|
|
}
|
|
$id = $this->id;
|
|
$sql = "SELECT * FROM $this->table WHERE id = $id";
|
|
$result = $db->query($sql);
|
|
return $db->fetch_assoc($result);
|
|
}
|
|
|
|
/**
|
|
* List objects with pagination (if set)
|
|
* @return array The list of objects in the table
|
|
* @throws Exception
|
|
*/
|
|
public function listObjectsWithPaginationIfSet($parseFunction = 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
|
|
$filters = $response->getRequestParameter('filters') ?? null; // Get the filters ( Eg. department_id:1,role_id:2 OR department_id:1 )
|
|
$order = $response->getRequestParameter('order') ?? null; // Get the order ( Eg. name:ASC )
|
|
// Make the filters an array
|
|
if ($filters) {
|
|
$filters = explode(',', $filters);
|
|
$temp = [];
|
|
foreach ( $filters as $filter ) {
|
|
$filter = explode(':', $filter);
|
|
$temp[$filter[0]] = $filter[1];
|
|
}
|
|
$filters = $temp;
|
|
}
|
|
// Make the order an array
|
|
if ($order) {
|
|
$order = explode(':', $order);
|
|
$order = [$order[0] => $order[1]];
|
|
// Make the order direction uppercase
|
|
$order = array_map('strtoupper', $order);
|
|
// Check if the order direction is valid
|
|
if (!in_array($order[array_key_first($order)], ['ASC', 'DESC'])) {
|
|
throw new Exception('Invalid order direction, must be ASC or DESC');
|
|
}
|
|
}
|
|
// List the objects with pagination
|
|
if ($page && $limit) {
|
|
return $this->listObjectsWithPagination($page, $limit, $search, $filters, $order, $parseFunction);
|
|
}
|
|
return $this->listObjects($parseFunction);
|
|
}
|
|
|
|
/**
|
|
* List objects in the table with pagination
|
|
* @param int $page The page number
|
|
* @param int $limit The number of objects per page
|
|
* @return array The list of objects in the table
|
|
*/
|
|
public function listObjectsWithPagination(int $page, int $limit, ?string $search = null, ?array $filters = null, ?array $order = null, $parseFunction = null): array
|
|
{
|
|
global /** @var response $response */
|
|
/** @var db $db */
|
|
$db, $response;
|
|
$mysqli = $db->conn;
|
|
|
|
$offset = ($page - 1) * $limit;
|
|
|
|
// Fetch table fields to search
|
|
$fields = $this->searchableFields;
|
|
if (empty($fields)) {
|
|
$fields = [];
|
|
$result = $mysqli->query("SHOW COLUMNS FROM {$this->table}");
|
|
while ($row = $result->fetch_assoc()) {
|
|
$fields[] = $row['Field'];
|
|
}
|
|
$result->free();
|
|
}
|
|
|
|
$whereClauses = [];
|
|
$params = [];
|
|
|
|
// Add where clauses from the object, if set. This is done to allow for custom where clauses in routes, while still allowing for pagination
|
|
if (!empty($this->whereClauses)) {
|
|
$whereClauses = $this->whereClauses;
|
|
}
|
|
|
|
// Search clause
|
|
if (!empty($search)) {
|
|
// Use prepared statements to prevent SQL injection
|
|
$searchClauses = [];
|
|
foreach ( $fields as $field ) {
|
|
$searchClauses[] = "`$field` LIKE ?";
|
|
$params[] = "%$search%";
|
|
}
|
|
$whereClauses[] = '(' . implode(' OR ', $searchClauses) . ')';
|
|
}
|
|
|
|
// Filters clause
|
|
if (!empty($filters)) {
|
|
foreach ( $filters as $field => $value ) {
|
|
if (in_array($field, $fields)) {
|
|
$whereClauses[] = "`$field` = ?";
|
|
$params[] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for "deleted_at" column
|
|
if (in_array('deleted_at', $fields)) {
|
|
$whereClauses[] = "`deleted_at` IS NULL";
|
|
}
|
|
|
|
// Combine WHERE clauses
|
|
$whereQuery = !empty($whereClauses) ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
|
|
|
|
// Order clause
|
|
$orderQuery = '';
|
|
if (!empty($order)) {
|
|
$orderParts = [];
|
|
foreach ( $order as $field => $direction ) {
|
|
if (in_array($field, $fields)) {
|
|
$direction = strtoupper($direction) === 'DESC' ? 'DESC' : 'ASC';
|
|
$orderParts[] = "`$field` $direction";
|
|
}
|
|
}
|
|
$orderQuery = !empty($orderParts) ? 'ORDER BY ' . implode(', ', $orderParts) : '';
|
|
}
|
|
|
|
// Pagination clause
|
|
$limitQuery = 'LIMIT ? OFFSET ?';
|
|
$params[] = $limit;
|
|
$params[] = $offset;
|
|
|
|
// Final query
|
|
$sql = "SELECT * FROM {$this->table} $whereQuery $orderQuery $limitQuery";
|
|
|
|
// Prepare and bind
|
|
$stmt = $mysqli->prepare($sql);
|
|
if ($stmt === false) {
|
|
throw new Exception("Failed to prepare statement: " . $mysqli->error);
|
|
}
|
|
|
|
// Dynamically bind parameters
|
|
$types = str_repeat('s', count($params) - 2) . 'ii'; // 's' for strings, 'i' for limit and offset
|
|
$stmt->bind_param($types, ...$params);
|
|
|
|
$stmt->execute();
|
|
$result = $stmt->get_result();
|
|
$objects = $result->fetch_all(MYSQLI_ASSOC);
|
|
$stmt->close();
|
|
|
|
// Total count query
|
|
$countSql = "SELECT COUNT(*) AS count FROM {$this->table} $whereQuery";
|
|
$countStmt = $mysqli->prepare($countSql);
|
|
if ($countStmt === false) {
|
|
throw new Exception("Failed to prepare count statement: " . $mysqli->error);
|
|
}
|
|
|
|
// Reuse parameters (excluding LIMIT and OFFSET)
|
|
$countParams = array_slice($params, 0, -2);
|
|
$countTypes = substr($types, 0, count($countParams));
|
|
if (!empty($countParams)) {
|
|
$countStmt->bind_param($countTypes, ...$countParams);
|
|
}
|
|
|
|
$countStmt->execute();
|
|
$countResult = $countStmt->get_result();
|
|
$total = $countResult->fetch_assoc()['count'];
|
|
$countStmt->close();
|
|
|
|
// Provide pagination metadata
|
|
$response->paginate($page, $limit, $total, $search, $filters, $order);
|
|
|
|
// Parse the objects, if a parse function is provided
|
|
if ($parseFunction) {
|
|
$objects = array_map($parseFunction, $objects);
|
|
}
|
|
|
|
return $objects;
|
|
}
|
|
|
|
/**
|
|
* List ALL objects in the table (THIS INCLUDES DELETED OBJECTS)
|
|
* @return array The list of objects in the table
|
|
*/
|
|
public function listObjects($parseFunction = null): array
|
|
{
|
|
global $db;
|
|
$sql = "SELECT * FROM $this->table";
|
|
$result = $db->query($sql);
|
|
$objects = $db->fetch_all($result);
|
|
// Parse the objects, if a parse function is provided
|
|
if ($parseFunction) {
|
|
$objects = array_map($parseFunction, $objects);
|
|
}
|
|
return $objects;
|
|
}
|
|
|
|
/**
|
|
* Get the total number of objects
|
|
* @param string|null $search The search query
|
|
* @param array|null $filters The filters ( Eg. department_id:1,role_id:2 OR department_id:1 )
|
|
* @return int The total number of objects
|
|
*/
|
|
public function getTotalObjects(string $search = null, array $filters = null): int
|
|
{
|
|
global $db;
|
|
$mysqli = $db->conn;
|
|
|
|
// Fetch table fields
|
|
$fields = [];
|
|
$result = $mysqli->query("SHOW COLUMNS FROM {$this->table}");
|
|
while ($row = $result->fetch_assoc()) {
|
|
$fields[] = $row['Field'];
|
|
}
|
|
$result->free();
|
|
|
|
$whereClauses = [];
|
|
$params = [];
|
|
|
|
// Search clause
|
|
if (!empty($search)) {
|
|
// Use prepared statements to prevent SQL injection
|
|
$searchClauses = [];
|
|
foreach ( $fields as $field ) {
|
|
$searchClauses[] = "`$field` LIKE ?";
|
|
$params[] = "%$search%";
|
|
}
|
|
$whereClauses[] = '(' . implode(' OR ', $searchClauses) . ')';
|
|
}
|
|
|
|
// Filters clause
|
|
if (!empty($filters)) {
|
|
foreach ( $filters as $field => $value ) {
|
|
if (in_array($field, $fields)) {
|
|
$whereClauses[] = "`$field` = ?";
|
|
$params[] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Combine WHERE clauses
|
|
$whereQuery = !empty($whereClauses) ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
|
|
|
|
// Check for "deleted_at" column
|
|
if (in_array('deleted_at', $fields)) {
|
|
$whereClauses[] = "`deleted_at` IS NULL";
|
|
}
|
|
|
|
// Final query
|
|
$sql = "SELECT COUNT(*) AS count FROM {$this->table} $whereQuery";
|
|
|
|
// Prepare and bind
|
|
$stmt = $mysqli->prepare($sql);
|
|
if ($stmt === false) {
|
|
throw new Exception("Failed to prepare statement: " . $mysqli->error);
|
|
}
|
|
|
|
// Dynamically bind parameters
|
|
$types = str_repeat('s', count($params)); // 's' for strings
|
|
$stmt->bind_param($types, ...$params);
|
|
|
|
$stmt->execute();
|
|
$result = $stmt->get_result();
|
|
|
|
return $result->fetch_assoc()['count'];
|
|
}
|
|
|
|
/**
|
|
* Get the table of the objects in the database
|
|
* @returns string
|
|
*/
|
|
public function getTable(): string
|
|
{
|
|
return $this->table;
|
|
}
|
|
|
|
/**
|
|
* Set the table of the objects in the database
|
|
* @param string $table The table of the objects in the database
|
|
*/
|
|
public function setTable(string $table): void
|
|
{
|
|
$this->table = $table;
|
|
}
|
|
|
|
/**
|
|
* Cache object
|
|
* @param string $key The key to cache the object
|
|
* @param mixed $data The data to cache
|
|
*/
|
|
public function cache(string $key, mixed $data, $objectId = null): void
|
|
{
|
|
// If the object id is not set, use the object id
|
|
if (!$objectId) {
|
|
$objectId = $this->id;
|
|
}
|
|
// if the data is an array, object, or resource, convert it to a string
|
|
if (is_array($data) || is_object($data) || is_resource($data)) {
|
|
$data = json_encode($data);
|
|
}
|
|
// Cache the data
|
|
redis->set($this->table . '_' . $objectId . '_' . $key, $data);
|
|
}
|
|
|
|
/**
|
|
* Get cached object
|
|
* @param string $key The key to get the cached object
|
|
* @return mixed The cached object
|
|
*/
|
|
public function getCached(string $key, $objectId = null): mixed
|
|
{
|
|
// If the object id is not set, use the object id
|
|
if (!$objectId) {
|
|
$objectId = $this->id;
|
|
}
|
|
// Get the cached data
|
|
$data = redis->get($this->table . '_' . $objectId . '_' . $key) ?? null;
|
|
// if the data is a JSON string, convert it to an array
|
|
if (is_string($data) && json_decode($data)) {
|
|
$data = json_decode($data);
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Delete cached object
|
|
* @param string $key The key to delete the cached object
|
|
*/
|
|
public function deleteCached(string $key, $objectId = null): void
|
|
{
|
|
// If the object id is not set, use the object id
|
|
if (!$objectId) {
|
|
$objectId = $this->id;
|
|
}
|
|
// Delete the cached data
|
|
redis->delete($this->table . '_' . $objectId . '_' . $key);
|
|
}
|
|
|
|
/**
|
|
* Delete the object from the database, or if the deleted_at column exists, soft delete the object.
|
|
* @throws Exception If the object is not selected, it throws an exception
|
|
*/
|
|
public function delete(): void
|
|
{
|
|
self::requireSelected();
|
|
// Check if the deleted_at column exists
|
|
if (self::columnsExist(['deleted_at'])) {
|
|
// Soft delete the object
|
|
self::update(['deleted_at' => date('Y-m-d H:i:s')]);
|
|
} else {
|
|
// Delete the object from the database
|
|
self::delete_object($this->table, $this->id);
|
|
}
|
|
// Trigger the object changed event
|
|
self::objectChanged();
|
|
}
|
|
|
|
/**
|
|
* Require selected object.
|
|
* Will throw an exception if object is not selected.
|
|
*/
|
|
public function requireSelected(): void
|
|
{
|
|
if (!$this->id) {
|
|
throw new Exception('Object not selected or does not exist in ' . $this->table);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Columns exist
|
|
* Check if all the columns in the array exist in the table
|
|
* @param array $columns The columns to check if they exist in the table E.g. ['name', 'email']
|
|
* @return bool True if all the columns exist in the table, false otherwise
|
|
*/
|
|
public function columnsExist(array $columns): bool
|
|
{
|
|
global $db;
|
|
$table = $this->table;
|
|
$result = $db->query("SHOW COLUMNS FROM $table");
|
|
$fields = $db->fetch_all($result);
|
|
$fields = array_column($fields, 'Field');
|
|
return count(array_intersect($columns, $fields)) === count($columns);
|
|
}
|
|
|
|
/**
|
|
* Update object
|
|
* @param array $data The data to update the object with (E.g. ['name' => 'John', 'email' => 'email@example.com'])
|
|
* @throws Exception If the object is not selected, it throws an exception
|
|
*/
|
|
public function update(array $data): void
|
|
{
|
|
self::requireSelected();
|
|
// Update the object in the database
|
|
global $db;
|
|
$set = [];
|
|
foreach ( $data as $key => $value ) {
|
|
$value = $db->escape_string($value);
|
|
$set[] = "$key = '$value'";
|
|
}
|
|
$set = implode(', ', $set);
|
|
$sql = "UPDATE $this->table SET $set WHERE id = $this->id";
|
|
$db->query($sql);
|
|
// Trigger the object changed event
|
|
self::objectChanged();
|
|
}
|
|
|
|
/**
|
|
* Object changed
|
|
* This function should be called after an object property is changed
|
|
* It will trigger an event to notify the object has changed
|
|
* @return void
|
|
*/
|
|
abstract public function objectChanged(): void;
|
|
|
|
/**
|
|
* Delete object by table and id (Permanently, no soft delete)
|
|
* @param string $table The table of the object in the database
|
|
* @param int $id The id of the object in the database
|
|
*/
|
|
public static function delete_object(string $table, int $id): void
|
|
{
|
|
global $db;
|
|
$sql = "DELETE FROM $table WHERE id = $id";
|
|
$db->query($sql);
|
|
}
|
|
|
|
/**
|
|
* Delete object permanently
|
|
* @throws Exception If the object is not selected, it throws an exception
|
|
*/
|
|
public function deletePermanently(): void
|
|
{
|
|
self::requireSelected();
|
|
// Delete the object from the database
|
|
self::delete_object($this->table, $this->id);
|
|
// Trigger the object changed event
|
|
self::objectChanged();
|
|
}
|
|
|
|
/**
|
|
* Get all objects fields
|
|
* @param array $fields The fields to get from the objects (Eg. ['name', 'description'])
|
|
* @return array The list of fields from the objects (Eg. [['name' => 'John', 'description' => 'Doe'], ['name' => 'Jane', 'description' => 'Doe']])
|
|
*/
|
|
public function getFields(array $fields): array
|
|
{
|
|
global $db;
|
|
$sql = "SELECT " . implode(', ', $fields) . " FROM $this->table";
|
|
$result = $db->query($sql);
|
|
return $db->fetch_all($result);
|
|
}
|
|
|
|
/**
|
|
* Select an object by id
|
|
* @param int $id The id of the object in the database
|
|
* @returns $this
|
|
*/
|
|
public function select(int $id): self
|
|
{
|
|
// Check if the object exists in the database
|
|
$this->id = $id;
|
|
$this->getObjectProperties();
|
|
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
|
|
*/
|
|
public function exists(): bool
|
|
{
|
|
// Check if the id is set
|
|
if (!isset($this->id)) {
|
|
return false;
|
|
}
|
|
// Check if the object exists in the database
|
|
global $db;
|
|
$id = $this->id;
|
|
$sql = "SELECT * FROM $this->table WHERE id = $id";
|
|
$result = $db->query($sql);
|
|
// Check if the object exists, and if there's a deleted_at column, check if the object is not deleted
|
|
return $db->num_rows($result) > 0 && (!$this->columnsExist(['deleted_at']) || $db->fetch_assoc($result)['deleted_at'] === null);
|
|
}
|
|
|
|
/**
|
|
* Restore the object from the database, if the deleted_at column exists.
|
|
* @throws Exception If the object is not selected, it throws an exception
|
|
*/
|
|
public function restore(): void
|
|
{
|
|
self::requireSelected();
|
|
// Check if the deleted_at column exists
|
|
if (self::columnsExist(['deleted_at'])) {
|
|
// Restore the object
|
|
global $db;
|
|
$id = $this->id;
|
|
$sql = "UPDATE $this->table SET deleted_at = NULL WHERE id = $id";
|
|
$db->query($sql);
|
|
// Trigger the object changed event
|
|
self::objectChanged();
|
|
}
|
|
}
|
|
} |