Refactor pagination and filtering logic
Replaced redundant SQL queries with reusable methods for paginating, filtering, and searching records. Improved query safety using prepared statements and enhanced flexibility with dynamic order and filter processing. Updated related routes and objects to support the new structure.
This commit is contained in:
+22
-17
@@ -7,11 +7,11 @@ use mysqli;
|
||||
|
||||
class db
|
||||
{
|
||||
public mysqli $conn;
|
||||
private string $host;
|
||||
private string $user;
|
||||
private string $password;
|
||||
private string $database;
|
||||
public mysqli $conn;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
@@ -31,22 +31,6 @@ class db
|
||||
}
|
||||
}
|
||||
|
||||
public function query(string $sql): \mysqli_result|bool
|
||||
{
|
||||
// If the connection is not established, connect
|
||||
return $this->conn->query($sql);
|
||||
}
|
||||
|
||||
public function fetch_assoc($result)
|
||||
{
|
||||
return $result->fetch_assoc();
|
||||
}
|
||||
|
||||
public function fetch_all($result)
|
||||
{
|
||||
return $result->fetch_all(MYSQLI_ASSOC);
|
||||
}
|
||||
|
||||
public function escape_string(string $string): string
|
||||
{
|
||||
return $this->conn->real_escape_string($string);
|
||||
@@ -64,6 +48,17 @@ class db
|
||||
return $this->fetch_assoc($result);
|
||||
}
|
||||
|
||||
public function query(string $sql): \mysqli_result|bool
|
||||
{
|
||||
// If the connection is not established, connect
|
||||
return $this->conn->query($sql);
|
||||
}
|
||||
|
||||
public function fetch_assoc($result)
|
||||
{
|
||||
return $result->fetch_assoc();
|
||||
}
|
||||
|
||||
public function list_objects(string $table): array
|
||||
{
|
||||
$sql = "SELECT * FROM $table";
|
||||
@@ -71,6 +66,11 @@ class db
|
||||
return $this->fetch_all($result);
|
||||
}
|
||||
|
||||
public function fetch_all($result)
|
||||
{
|
||||
return $result->fetch_all(MYSQLI_ASSOC);
|
||||
}
|
||||
|
||||
public function list_objects_paginated(string $table, int $page, int $limit): array
|
||||
{
|
||||
$offset = ($page - 1) * $limit;
|
||||
@@ -103,4 +103,9 @@ class db
|
||||
{
|
||||
return $this->conn;
|
||||
}
|
||||
|
||||
public function prepare(string $sql): false|\mysqli_stmt
|
||||
{
|
||||
return $this->conn->prepare($sql);
|
||||
}
|
||||
}
|
||||
+41
-23
@@ -11,6 +11,12 @@ class response implements response_i
|
||||
private array $data = [];
|
||||
private array $meta = [];
|
||||
private array $includes = [];
|
||||
|
||||
#[NoReturn] public function success(mixed $data, int $status = null): void
|
||||
{
|
||||
$this->response(true, $data, $status);
|
||||
}
|
||||
|
||||
#[NoReturn] public function response(bool $success, mixed $data, int $status = null): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
@@ -20,7 +26,7 @@ class response implements response_i
|
||||
http_response_code($success ? 200 : 400);
|
||||
}
|
||||
// If the data isn't an array, convert it to an array
|
||||
if (! is_array($data)) {
|
||||
if (!is_array($data)) {
|
||||
$data = ['message' => $data];
|
||||
}
|
||||
echo json_encode([
|
||||
@@ -32,9 +38,9 @@ class response implements response_i
|
||||
exit;
|
||||
}
|
||||
|
||||
#[NoReturn] public function success(mixed $data, int $status = null): void
|
||||
#[NoReturn] public function not_found(): void
|
||||
{
|
||||
$this->response(true, $data, $status);
|
||||
$this->error('Not found', 404);
|
||||
}
|
||||
|
||||
#[NoReturn] public function error(mixed $data, int $status = null): void
|
||||
@@ -42,31 +48,11 @@ class response implements response_i
|
||||
$this->response(false, $data, $status);
|
||||
}
|
||||
|
||||
#[NoReturn] public function not_found(): void
|
||||
{
|
||||
$this->error('Not found', 404);
|
||||
}
|
||||
|
||||
#[NoReturn] public function rate_limit_exceeded(): void
|
||||
{
|
||||
$this->error('Rate limit exceeded', 429);
|
||||
}
|
||||
|
||||
public function add_data(string $key, mixed $value): void
|
||||
{
|
||||
$this->data[$key] = $value;
|
||||
}
|
||||
|
||||
public function add_meta(string $key, mixed $value): void
|
||||
{
|
||||
$this->meta[$key] = $value;
|
||||
}
|
||||
|
||||
public function add_included(string $key, mixed $value): void
|
||||
{
|
||||
$this->includes[$key] = $value;
|
||||
}
|
||||
|
||||
public function matching_route_found(): void
|
||||
{
|
||||
$this->matching_route_found = true;
|
||||
@@ -97,6 +83,11 @@ class response implements response_i
|
||||
]);
|
||||
}
|
||||
|
||||
public function add_meta(string $key, mixed $value): void
|
||||
{
|
||||
$this->meta[$key] = $value;
|
||||
}
|
||||
|
||||
public function get_data(): array
|
||||
{
|
||||
return $this->data;
|
||||
@@ -112,6 +103,11 @@ class response implements response_i
|
||||
$this->add_data('debug', $data);
|
||||
}
|
||||
|
||||
public function add_data(string $key, mixed $value): void
|
||||
{
|
||||
$this->data[$key] = $value;
|
||||
}
|
||||
|
||||
public function getRequestParameter(string $key): string|null
|
||||
{
|
||||
// Get the request data if the method is POST, PUT or PATCH
|
||||
@@ -130,4 +126,26 @@ class response implements response_i
|
||||
{
|
||||
$this->add_included($string, $dataArray);
|
||||
}
|
||||
|
||||
public function add_included(string $key, mixed $value): void
|
||||
{
|
||||
$this->includes[$key] = $value;
|
||||
}
|
||||
|
||||
public function parseFilters(?string $filters): array|null
|
||||
{
|
||||
if ($filters) {
|
||||
$filters = explode(',', $filters);
|
||||
$temp = [];
|
||||
foreach ( $filters as $filter ) {
|
||||
$filter = explode(':', $filter);
|
||||
$temp[$filter[0]] = $filter[1];
|
||||
}
|
||||
$filters = $temp;
|
||||
}
|
||||
if ($filters) {
|
||||
return $filters;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -110,18 +110,16 @@ class bookings_o extends db
|
||||
$this->status = new object_property($this->table, $this->id, 'status', 'string', true);
|
||||
}
|
||||
|
||||
public function getCustomerBookingsPaginated(int $customer_number, int $page = 1, int $limit = 10, string $order = 'DESC'): array
|
||||
public function getCustomerBookingsPaginated(int $customer_number, int $page = 1, int $limit = 10, array $order = ['id' => 'DESC'], string $search = null, array $filters = null): array
|
||||
{
|
||||
global /** @var response $response */
|
||||
$db, $response;
|
||||
$sql = "SELECT * FROM $this->table WHERE customer_number = $customer_number ORDER BY id $order LIMIT $limit OFFSET " . ($page - 1) * $limit;
|
||||
$result = $db->query($sql);
|
||||
$array = $db->fetch_all($result);
|
||||
// Add the metadata
|
||||
$sql = "SELECT COUNT(*) as count FROM $this->table WHERE customer_number = $customer_number";
|
||||
$result = $db->query($sql);
|
||||
$row = $db->fetch_assoc($result);
|
||||
$total = $row['count'];
|
||||
// Add the customer number to the filters
|
||||
$filters['customer_number'] = $customer_number;
|
||||
// List the objects with pagination
|
||||
$array = $this->listObjectsWithPagination($page, $limit, $search, $filters, $order);
|
||||
// Get the total number of objects
|
||||
$total = $this->getTotalObjects($search, $filters);
|
||||
$response->paginate($page, $limit, $total);
|
||||
return $array;
|
||||
}
|
||||
|
||||
@@ -180,18 +180,18 @@ class orders_o extends db
|
||||
return new users_o();
|
||||
}
|
||||
|
||||
public function getCustomerOrdersPaginated(int $customer_number, int $page = 1, int $limit = 10, string $order = 'DESC'): array
|
||||
public function getCustomerOrdersPaginated(int $customer_number, int $page = 1, int $limit = 10, array $order = ['id' => 'DESC'], string $search = null, array $filters = null): array
|
||||
{
|
||||
global /** @var response $response */
|
||||
$db, $response;
|
||||
$sql = "SELECT * FROM $this->table WHERE customer_id = $customer_number AND deleted_at IS NULL ORDER BY id $order LIMIT $limit OFFSET " . ($page - 1) * $limit;
|
||||
$result = $db->query($sql);
|
||||
$array = $db->fetch_all($result);
|
||||
// Add the metadata
|
||||
$sql = "SELECT COUNT(*) as count FROM $this->table WHERE customer_id = $customer_number AND deleted_at IS NULL";
|
||||
$result = $db->query($sql);
|
||||
$row = $db->fetch_assoc($result);
|
||||
$total = $row['count'];
|
||||
// Add the customer number to the filters
|
||||
$filters['customer_id'] = $customer_number;
|
||||
|
||||
// List the objects with pagination
|
||||
$array = $this->listObjectsWithPagination($page, $limit, $search, $filters, $order);
|
||||
|
||||
// Get the total number of objects
|
||||
$total = $this->getTotalObjects($search, $filters);
|
||||
$response->paginate($page, $limit, $total);
|
||||
return $array;
|
||||
}
|
||||
|
||||
+1
-1
@@ -459,7 +459,7 @@ class users_o extends db
|
||||
return true;
|
||||
}
|
||||
// Get the record from the database
|
||||
$sql = "SELECT * FROM orders WHERE id = $order_id AND customer_number = " . $this->customer_number->value();
|
||||
$sql = "SELECT * FROM orders WHERE id = $order_id AND customer_id = " . $this->customer_number->value();
|
||||
$result = $db->query($sql);
|
||||
if ($result->num_rows > 0) {
|
||||
return true;
|
||||
|
||||
@@ -47,7 +47,8 @@ class bookingsRoute
|
||||
/** Own bookings */
|
||||
$this->get('/user/bookings', function () {
|
||||
// Require the user to be logged in
|
||||
global $response;
|
||||
global /** @var response $response */
|
||||
$response;
|
||||
$this->requirePermission('list_own_bookings');
|
||||
// Get the user object
|
||||
$user = (new authentication())->get_user();
|
||||
@@ -61,7 +62,10 @@ class bookingsRoute
|
||||
$user->customer_number->value(),
|
||||
($this->fromRequest('page') ?? 1),
|
||||
($this->fromRequest('limit') ?? 10),
|
||||
($this->fromRequest('order') ?? 'DESC')
|
||||
['id' => 'DESC'],
|
||||
$this->fromRequest('search') === null ? '' : $this->fromRequest('search'),
|
||||
$this->fromRequest('filters') === null ? [] :
|
||||
$response->parseFilters($this->fromRequest('filters')) ?? []
|
||||
)
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -29,7 +29,10 @@ class userOrdersRoute
|
||||
$user->customer_number->value(),
|
||||
($this->fromRequest('page') ?? 1),
|
||||
($this->fromRequest('limit') ?? 10),
|
||||
($this->fromRequest('order') ?? 'DESC')
|
||||
['id' => 'DESC'],
|
||||
$this->fromRequest('search') === null ? '' : $this->fromRequest('search'),
|
||||
$this->fromRequest('filters') === null ? [] :
|
||||
$response->parseFilters($this->fromRequest('filters')) ?? []
|
||||
)
|
||||
);
|
||||
} else {
|
||||
@@ -53,6 +56,10 @@ class userOrdersRoute
|
||||
if (!isset($data['id'])) {
|
||||
$response->error('id parameter is required', 400);
|
||||
}
|
||||
// Make sure the user exists
|
||||
if (!$user->exists()) {
|
||||
$response->error('User not found', 400);
|
||||
}
|
||||
// Make sure the user is allowed to fetch the order
|
||||
if (!$user->hasAccessToOrder($data['id'])) {
|
||||
$response->error('You are not allowed to fetch this order', 400);
|
||||
|
||||
+175
-45
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace traits;
|
||||
|
||||
use classes\db;
|
||||
use classes\response;
|
||||
use Exception;
|
||||
|
||||
@@ -68,6 +69,7 @@ trait db_object_t
|
||||
$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);
|
||||
@@ -78,8 +80,14 @@ trait db_object_t
|
||||
}
|
||||
$filters = $temp;
|
||||
}
|
||||
// Make the order an array
|
||||
if ($order) {
|
||||
$order = explode(':', $order);
|
||||
$order = [$order[0] => $order[1]];
|
||||
}
|
||||
// List the objects with pagination
|
||||
if ($page && $limit) {
|
||||
return $this->listObjectsWithPagination($page, $limit, $search, $filters);
|
||||
return $this->listObjectsWithPagination($page, $limit, $search, $filters, $order);
|
||||
}
|
||||
return $this->listObjects();
|
||||
}
|
||||
@@ -90,62 +98,114 @@ trait db_object_t
|
||||
* @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
|
||||
public function listObjectsWithPagination(int $page, int $limit, ?string $search = null, ?array $filters = null, ?array $order = null): array
|
||||
{
|
||||
global /** @var response $response */
|
||||
/** @var db $db */
|
||||
$db, $response;
|
||||
$mysqli = $db->conn;
|
||||
|
||||
$offset = ($page - 1) * $limit;
|
||||
// Get all the fields of the table
|
||||
$sql = "SHOW COLUMNS FROM $this->table";
|
||||
$result = $db->query($sql);
|
||||
$fields = $db->fetch_all($result);
|
||||
$searchfields = [];
|
||||
foreach ( $fields as $field ) {
|
||||
$searchfields[] = $field['Field'];
|
||||
|
||||
// Fetch table fields
|
||||
$fields = [];
|
||||
$result = $mysqli->query("SHOW COLUMNS FROM {$this->table}");
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$fields[] = $row['Field'];
|
||||
}
|
||||
// Search for any similarities to the search query (Not case sensitive)
|
||||
$searchQuery = '';
|
||||
if ($search) {
|
||||
$searchQuery = 'WHERE ';
|
||||
$search = strtolower($search);
|
||||
$searchQuery .= '(';
|
||||
foreach ( $searchfields as $field ) {
|
||||
$searchQuery .= "LOWER($field) LIKE '%$search%' OR ";
|
||||
$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%";
|
||||
}
|
||||
$searchQuery = substr($searchQuery, 0, -4);
|
||||
$searchQuery .= ')';
|
||||
$whereClauses[] = '(' . implode(' OR ', $searchClauses) . ')';
|
||||
}
|
||||
// Filter the objects
|
||||
if ($filters) {
|
||||
if ($searchQuery) {
|
||||
$searchQuery .= ' AND ';
|
||||
} else {
|
||||
$searchQuery = 'WHERE ';
|
||||
}
|
||||
|
||||
// Filters clause
|
||||
if (!empty($filters)) {
|
||||
foreach ( $filters as $field => $value ) {
|
||||
// If the value is a number, do not add quotes
|
||||
if (is_numeric($value)) {
|
||||
$searchQuery .= "$field = $value AND ";
|
||||
} else {
|
||||
$searchQuery .= "$field = '$value' AND ";
|
||||
if (in_array($field, $fields)) {
|
||||
$whereClauses[] = "`$field` = ?";
|
||||
$params[] = $value;
|
||||
}
|
||||
}
|
||||
// If the deleted_at column exists, filter out the deleted objects
|
||||
if (in_array('deleted_at', $searchfields)) {
|
||||
$searchQuery .= "deleted_at IS NULL AND ";
|
||||
}
|
||||
$searchQuery = substr($searchQuery, 0, -5);
|
||||
}
|
||||
// Get the objects with pagination
|
||||
$sql = "SELECT * FROM $this->table $searchQuery LIMIT $limit OFFSET $offset";
|
||||
$result = $db->query($sql);
|
||||
$array = $db->fetch_all($result);
|
||||
$sql = "SELECT COUNT(*) AS count FROM $this->table $searchQuery";
|
||||
$result = $db->query($sql);
|
||||
$row = $db->fetch_assoc($result);
|
||||
$total = $row['count'];
|
||||
|
||||
// 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);
|
||||
return $array;
|
||||
|
||||
return $objects;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,6 +220,76 @@ trait db_object_t
|
||||
return $db->fetch_all($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this object exist in the database?
|
||||
* @return bool True if the object exists in the database, false otherwise
|
||||
|
||||
Reference in New Issue
Block a user