Files
api/services/nginx/app/traits/db_object_t.php
T
Jepp9350 2151cf2b7b Add customer listing and import functionality for Economic
Implemented endpoints to list and import customers from the Economic API, including query filtering, pagination, and validation. Added supporting trait methods, database functions, and error handling. Expanded the user object for Economic integration and introduced utilities for type checking and parameter extraction.
2025-02-13 12:50:35 +01:00

617 lines
20 KiB
PHP

<?php
namespace traits;
use classes\db;
use classes\response;
use Exception;
use objects\bookings_o;
use objects\cron_o;
use objects\customer_codes_o;
use objects\customer_notes_o;
use objects\customer_vehicles_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\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
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 ) {
$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;
}
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
*/
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 = [];
// 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);
return $result->num_rows > 0;
}
/**
* 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();
}
}
}