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 { self::requireSelected(); // Return the object as a string // Check if the asArray function is set, if not, return the object row directly if (method_exists($this, 'asArray')) { return json_encode($this->asArray()); } // If the asArray function is not set, return the object row directly return json_encode($this->getArray()); } /** * 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); } } /** * 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) * @param callable|null $parseFunction The function to parse the objects * @param string|null $forcedFilters The filters to force on the objects (Use the array_to_filters function to convert an array to a string) * @return array The list of objects in the table * @throws Exception */ public function listObjectsWithPaginationIfSet($parseFunction = null, $forcedFilters = null, array $join = []): array { // Link to the listObjectsWithPaginationIfSet function. return self::db_object_t__listObjectsWithPaginationIfSet($parseFunction, $forcedFilters, $join); } /** * List objects with pagination (if set) * @note This is separate from listObjectsWithPaginationIfSet, as it is used in the API routes with specific permission checks * @param callable|null $parseFunction The function to parse the objects * @param string|null $forcedFilters The filters to force on the objects (Use the array_to_filters function to convert an array to a string) * @return array * @throws Exception If the user does not have permission to list the objects */ public function db_object_t__listObjectsWithPaginationIfSet($parseFunction = null, $forcedFilters = null, array $join = []): 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 // If the forced filters are set, use them if ($forcedFilters) { $filters = $forcedFilters; } else { $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); // Check if the filter already exists, if it does, make it an array if (isset($temp[$filter[0]])) { if (!is_array($temp[$filter[0]])) { $temp[$filter[0]] = [$temp[$filter[0]]]; } // Check if the filter already exists within the array with the same key, if it does, skip it if (in_array($filter[1], $temp[$filter[0]])) { continue; } $temp[$filter[0]][] = $filter[1]; continue; } $temp[$filter[0]] = $filter[1]; } // Check if the value is "null", if it is, set it to null foreach ( $temp as $key => $value ) { if (!is_array($value) && strtolower($value) === 'null') { $temp[$key] = null; } } $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'); } } else { $order = ['id' => 'ASC']; // Default order } // List the objects with pagination if ($page && $limit) { return $this->listObjectsWithPagination($page, $limit, $search, $filters, $order, $parseFunction, $join); } // If the page and limit are not set, list all objects return $this->listObjectsWithPagination(1, 1000, $search, $filters, $order, $parseFunction, $join); } /** * 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 * @throws Exception */ public function listObjectsWithPagination(int $page, int $limit, ?string $search = null, ?array $filters = null, ?array $order = null, $parseFunction = null, array $join = []): 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; $fieldTypes = []; if (empty($fields)) { $fields = []; $result = $mysqli->query("SHOW COLUMNS FROM {$this->table}"); while ($row = $result->fetch_assoc()) { $fields[] = $row['Field']; $fieldTypes[$row['Field']] = $row['Type']; } $result->free(); } $whereClauses = []; $params = []; $joinClauses = []; // 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)) { // If the value is an array, add multiple filters if (is_array($value)) { $temp = []; foreach ( $value as $v ) { // Determine the type of the field $type = $fieldTypes[$field]; // If the field is an integer, cast the value to an integer if (str_contains($type, 'int')) { $temp[] = "`$field` = $v"; } else { $temp[] = "`$field` = ?"; $params[] = $v; } //$temp[] = "`$field` = ?"; //$params[] = $v; } $whereClauses[] = '(' . implode(' OR ', $temp) . ')'; continue; } // If the value is null, add a where clause to check if the field is null if ($value === null || (is_string($value) && strtolower($value) === 'null') || (is_string($value) && strtolower($value === 'is_null'))) { $whereClauses[] = "$field IS NULL"; continue; } // If the value is "NOT NULL", add a where clause to check if the field is not null if ($value === 'NOT NULL' || (is_string($value) && strtolower($value) === 'not null')) { $whereClauses[] = "$field IS NOT NULL"; continue; } // If the value is "NOT ZERO", add a where clause to check if the field is not zero if ($value === 'NOT ZERO' || (is_string($value) && strtolower($value) === 'not zero')) { $whereClauses[] = "$field != 0"; continue; } $whereClauses[] = "`$field` = ?"; $params[] = $value; } else { // Add support for 'date_from' and 'date_to' filters // Example of date_from: 'date_from:2023-01-01' // Example of date_to: 'date_to:2023-12-31' // In total the filter string would look like: 'created_at-date_from:2023-01-01,created_at-date_to:2023-12-31' if (preg_match('/^(.+?)-(date_from|date_to)/', $field, $matches)) { $fieldName = $matches[1]; $filterType = $matches[2]; $dateValue = $value; // If the (date_to|date_from) is E.g. '2023-12-31', then we need to include the entire from/to date // If the date is to be inclusive, we need to add 1 day to the date if ($filterType === 'date_to') { $dateValue = date('Y-m-d', strtotime($dateValue . ' +1 day')); } if (in_array($fieldName, $fields)) { if ($filterType === 'date_from') { $whereClauses[] = "`$fieldName` >= ?"; } elseif ($filterType === 'date_to') { $whereClauses[] = "`$fieldName` <= ?"; } $params[] = $dateValue; } } elseif (preg_match('/^(.+?)-has_attribute/', $field, $matches)) { // If the filter is e.g. customer_number-has_attribute:invoiceOrdersIndividually // Get the field name and the attribute $fieldName = $matches[1]; // Set the attribute to the value, without the (optional) "!" prefix $attribute = (string)str_replace('!', '', $value); $state = !str_starts_with($value, '!'); // Check if the field name is valid if (!in_array($fieldName, $fields)) { throw new Exception('Invalid filter: ' . $field); } $attributes = array(); // Push the attribute to the array $attributes[] = (string)$attribute; // Get the customer numbers with the attribute $tmp_customer_numbers_with_attribute = (new users_o())->getCustomerNumbersWithAttributes($attributes); // If the state is true, add the customer numbers to the where clause if ($state) { $whereClauses[] = "$fieldName IN (" . implode(',', array_map('intval', $tmp_customer_numbers_with_attribute)) . ")"; } else { // If the state is false, add the customer numbers to the where clause $whereClauses[] = "$fieldName NOT IN (" . implode(',', array_map('intval', $tmp_customer_numbers_with_attribute)) . ")"; } } elseif (preg_match('/^(.+?)-has_key/', $field, $matches)) { // If the filter is e.g. customer_number-has_key:invoiceOrdersIndividually // Get the field name and the key $fieldName = $matches[1]; // Set the key to the value, without the (optional) "!" prefix $key = (string)str_replace('!', '', $value); $state = !str_starts_with($value, '!'); // Check if the field name is valid if (!in_array($fieldName, $fields)) { throw new Exception('Invalid filter: ' . $field); } $attributes = array(); // Push the attribute to the array $attributes[] = (string)$key; // Get the customer numbers with the attribute $tmp_customer_numbers_with_attribute = (new user_key_value_pairs_o())->getCustomerNumbersWithKey($attributes); // If the state is true, add the customer numbers to the where clause if ($state) { $whereClauses[] = "$fieldName IN (" . implode(',', array_map('intval', $tmp_customer_numbers_with_attribute)) . ")"; } else { // If the state is false, add the customer numbers to the where clause $whereClauses[] = "$fieldName NOT IN (" . implode(',', array_map('intval', $tmp_customer_numbers_with_attribute)) . ")"; } } else { throw new Exception('Invalid filter: ' . $field); } } } } // 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; // Join clause with table prefix, so it doesn't conflict with the other columns // This is done to allow for custom join clauses in routes, while still allowing for pagination $joinClauses = ''; if (!empty($join)) { // Build the join clauses foreach ( $join as $table => $on ) { $joinClauses .= " LEFT JOIN `$table` ON $on"; } } // Final query $sql = "SELECT * FROM {$this->table} $joinClauses $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 /** @var db $db */ $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; } /** * Forcefully add or override filters to the user's request * @param string $filterString The filters to forcefully override/add on the objects (Eg. 'department_id:1,role_id:2') * @returns string The updated filters string with the forced filters added */ public function forceAddFilters(string $filterString): string { global /** @var response $response */ $response; $filters = $response->getRequestParameter('filters') ?? null; // Get the filters ( Eg. department_id:1,role_id:2 OR department_id:1 ) // If the filters are set, add the forced filters to the filters if ($filters) { $filters .= ',' . $filterString; } else { $filters = $filterString; } return $filters; } /** * Forcefully restrict filters to the user's request * @param array $fieldsAndValues The filters to forcefully restrict on the objects (Eg. ['department_id' => 1, 'role_id' => [1,2]]) - This would throw an error if the user tries to search for a different department_id or role_id than 1 or 2 * @returns string The updated filters string with the forced filters added (Eg. 'department_id:1,role_id:1,role_id:2') * @throws Exception If the user tries to search for a different department_id or role_id than 1 or 2 */ public function forceRestrictFilters(array $fieldsAndValues): string { global /** @var response $response */ $response; $filters = $response->getRequestParameter('filters') ?? null; // Get the filters ( Eg. department_id:1,role_id:2 OR department_id:1 ) // If the filters are set, add the forced filters to the filters if ($filters) { $filters = $this->filter_string_to_array($filters); foreach ( $fieldsAndValues as $field => $value ) { // If the value is an array, check if the user tries to search for a different value than the ones provided if (is_array($value)) { // Check if the filter is even set if (isset($filters[$field]) && !in_array($filters[$field], $value)) { throw new Exception('Invalid filter: ' . $field . ' - ' . $filters[$field] . '. Permitted values: ' . implode(', ', $value)); } continue; } // Check if the user tries to search for a different value than the one provided if ($filters[$field] !== $value) { throw new Exception('Invalid filter: ' . $field . ' - ' . $filters[$field] . '. Permitted value: ' . $value); } } // If any of the forced filters are not set, set them - This is done to prevent the user from searching by omitting the forced filters foreach ( $fieldsAndValues as $field => $value ) { if (!isset($filters[$field])) { $filters[$field] = $value; } } } else { $filters = $fieldsAndValues; } return $this->array_to_filters($filters); } /** * Filter string to array * @param string $filterString The filter string (Eg. 'department_id:1,role_id:2') * @return array The array of filters (Eg. ['department_id' => 1, 'role_id' => 2]) */ public function filter_string_to_array(string $filterString): array { $filters = explode(',', $filterString); $temp = []; foreach ( $filters as $filter ) { $filter = explode(':', $filter); // Check if the value is "null", if it is, set it to null if (strtolower($filter[1]) === 'null') { $filter[1] = null; continue; } $temp[$filter[0]] = $filter[1]; } return $temp; } /** * Convert an array of filters to a string * @param $array array The array of filters * @return string * @example ['department_id' => [1,2], 'role_id' => 2, 'id' => 1] => 'department_id:1,department_id:2,role_id:2,id:1' */ public function array_to_filters(array $array): string { $filter_string = ''; foreach ( $array as $key => $value ) { // If this is not the first filter, add a comma to separate the filters if ($filter_string !== '') { $filter_string .= ','; } // If the value is an array, add multiple filters if (is_array($value)) { foreach ( $value as $v ) { $filter_string .= $key . ':' . $v . ','; } // Remove the last comma $filter_string = rtrim($filter_string, ','); continue; } // Add the filter $filter_string .= $key . ':' . $value; } return $filter_string; } /** * 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 { // Add the backticks to the table name, to prevent SQL injection and reserved keyword issues. $table = "`$table`"; $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(); } /** * 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 ) { // If the value is null, set it to null if ($value === null || (is_string($value) && strtolower($value) === 'null')) { $set[] = "$key = NULL"; continue; } $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 * @throws Exception If the object does not exist in the database, it throws an exception */ public function select(int $id): self { // Check if the object exists in the database $this->id = $id; $this->getObjectProperties(); return $this; } /** * Set the view, used on the pagination * @param string $view The view to set * @return users_o|bookings_new_o|bookings_o|branding_o|categories_o|collected_order_invoices_o|cron_o|currency_conversion_rates_o|customer_codes_o|customer_notes_o|customer_vehicles_o|department_categories_o|department_daily_reports_o|department_variables_o|departments_o|economic_module_orders|form_submissions_o|fxratesapi_conversion_rates_o|groups_o|groups_permissions_o|logs_o|module_action_logs_o|motorapi_lookups_o|notifications_o|order_items_o|orders_o|plate_scanners_o|plate_scans_o|product_options_o|products_o|ratelimit_o|stripe_module_customers_o|stripe_module_orders_o|tokens_o|user_key_value_pairs_o|user_price_overrides_o|db_object_t */ public function setView(string $view): self { $this->table = $view; 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(); } } }