chore: add spec-kit and standardize signatures
- Added GitHub spec-kit for development workflow - Standardized file signatures to Descomplicar® format - Updated development configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
543
care-booking-block/includes/class-database-handler.php
Normal file
543
care-booking-block/includes/class-database-handler.php
Normal file
@@ -0,0 +1,543 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Database handler for Care Booking Block plugin
|
||||
*
|
||||
* @package CareBookingBlock
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Database handler class
|
||||
*/
|
||||
class Care_Booking_Database_Handler
|
||||
{
|
||||
/**
|
||||
* Database table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $table_name;
|
||||
|
||||
/**
|
||||
* WordPress database object
|
||||
*
|
||||
* @var wpdb
|
||||
*/
|
||||
private $wpdb;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$this->wpdb = $wpdb;
|
||||
$this->table_name = $wpdb->prefix . 'care_booking_restrictions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_table_name()
|
||||
{
|
||||
return $this->table_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create database table
|
||||
*
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function create_table()
|
||||
{
|
||||
$charset_collate = $this->wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->table_name} (
|
||||
id BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
restriction_type ENUM('doctor', 'service') NOT NULL,
|
||||
target_id BIGINT(20) UNSIGNED NOT NULL,
|
||||
doctor_id BIGINT(20) UNSIGNED NULL,
|
||||
is_blocked BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_type_target (restriction_type, target_id),
|
||||
INDEX idx_doctor_service (doctor_id, target_id),
|
||||
INDEX idx_blocked (is_blocked),
|
||||
INDEX idx_composite_blocked (restriction_type, is_blocked),
|
||||
INDEX idx_composite_doctor_service (doctor_id, target_id, is_blocked),
|
||||
INDEX idx_performance_doctor (restriction_type, target_id, is_blocked),
|
||||
INDEX idx_performance_service (doctor_id, target_id, is_blocked)
|
||||
) $charset_collate;";
|
||||
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
|
||||
$result = dbDelta($sql);
|
||||
|
||||
return !empty($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop database table
|
||||
*
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function drop_table()
|
||||
{
|
||||
$sql = "DROP TABLE IF EXISTS {$this->table_name}";
|
||||
|
||||
return $this->wpdb->query($sql) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if table exists
|
||||
*
|
||||
* @return bool True if table exists, false otherwise
|
||||
*/
|
||||
public function table_exists()
|
||||
{
|
||||
$table_name = $this->table_name;
|
||||
|
||||
$query = $this->wpdb->prepare("SHOW TABLES LIKE %s", $table_name);
|
||||
$result = $this->wpdb->get_var($query);
|
||||
|
||||
return $result === $table_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new restriction
|
||||
*
|
||||
* @param array $data Restriction data
|
||||
* @return int|false Restriction ID on success, false on failure
|
||||
*/
|
||||
public function insert($data)
|
||||
{
|
||||
// SECURITY: Enhanced data validation
|
||||
if (!is_array($data)) {
|
||||
error_log('Care Booking Block: Invalid data type in insert()');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!isset($data['restriction_type']) || !isset($data['target_id'])) {
|
||||
error_log('Care Booking Block: Missing required fields in insert()');
|
||||
return false;
|
||||
}
|
||||
|
||||
// SECURITY: Whitelist validation for restriction type
|
||||
$allowed_types = ['doctor', 'service'];
|
||||
if (!in_array($data['restriction_type'], $allowed_types, true)) {
|
||||
error_log('Care Booking Block: Invalid restriction_type in insert(): ' . $data['restriction_type']);
|
||||
return false;
|
||||
}
|
||||
|
||||
// SECURITY: Validate target_id
|
||||
$target_id = absint($data['target_id']);
|
||||
if ($target_id <= 0 || $target_id > PHP_INT_MAX) {
|
||||
error_log('Care Booking Block: Invalid target_id in insert(): ' . $data['target_id']);
|
||||
return false;
|
||||
}
|
||||
|
||||
// SECURITY: Validate service restrictions require doctor_id
|
||||
if ($data['restriction_type'] === 'service') {
|
||||
if (empty($data['doctor_id']) || absint($data['doctor_id']) <= 0) {
|
||||
error_log('Care Booking Block: Missing or invalid doctor_id for service restriction');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// SECURITY: Prepare data with proper sanitization
|
||||
$insert_data = [
|
||||
'restriction_type' => sanitize_text_field($data['restriction_type']),
|
||||
'target_id' => $target_id,
|
||||
'doctor_id' => isset($data['doctor_id']) ? absint($data['doctor_id']) : null,
|
||||
'is_blocked' => isset($data['is_blocked']) ? (bool) $data['is_blocked'] : false
|
||||
];
|
||||
|
||||
// SECURITY: Define data types for prepared statement
|
||||
$format = ['%s', '%d', '%d', '%d'];
|
||||
|
||||
// SECURITY: Use WordPress prepared statement (wpdb->insert uses prepare internally)
|
||||
$result = $this->wpdb->insert($this->table_name, $insert_data, $format);
|
||||
|
||||
if ($result === false) {
|
||||
error_log('Care Booking Block: Database insert failed: ' . $this->wpdb->last_error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update restriction
|
||||
*
|
||||
* @param int $id Restriction ID
|
||||
* @param array $data Update data
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function update($id, $data)
|
||||
{
|
||||
$id = absint($id);
|
||||
if ($id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prepare update data
|
||||
$update_data = [];
|
||||
$format = [];
|
||||
|
||||
if (isset($data['restriction_type'])) {
|
||||
if (!in_array($data['restriction_type'], ['doctor', 'service'])) {
|
||||
return false;
|
||||
}
|
||||
$update_data['restriction_type'] = sanitize_text_field($data['restriction_type']);
|
||||
$format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['target_id'])) {
|
||||
$update_data['target_id'] = absint($data['target_id']);
|
||||
$format[] = '%d';
|
||||
}
|
||||
|
||||
if (isset($data['doctor_id'])) {
|
||||
$update_data['doctor_id'] = absint($data['doctor_id']);
|
||||
$format[] = '%d';
|
||||
}
|
||||
|
||||
if (isset($data['is_blocked'])) {
|
||||
$update_data['is_blocked'] = (bool) $data['is_blocked'];
|
||||
$format[] = '%d';
|
||||
}
|
||||
|
||||
if (empty($update_data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $this->wpdb->update(
|
||||
$this->table_name,
|
||||
$update_data,
|
||||
['id' => $id],
|
||||
$format,
|
||||
['%d']
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete restriction
|
||||
*
|
||||
* @param int $id Restriction ID
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
$id = absint($id);
|
||||
if ($id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $this->wpdb->delete(
|
||||
$this->table_name,
|
||||
['id' => $id],
|
||||
['%d']
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get restriction by ID
|
||||
*
|
||||
* @param int $id Restriction ID
|
||||
* @return object|false Restriction object on success, false on failure
|
||||
*/
|
||||
public function get($id)
|
||||
{
|
||||
// SECURITY: Enhanced input validation
|
||||
$id = absint($id);
|
||||
if ($id <= 0 || $id > PHP_INT_MAX) {
|
||||
error_log('Care Booking Block: Invalid ID in get(): ' . $id);
|
||||
return false;
|
||||
}
|
||||
|
||||
// SECURITY: Use prepared statement (already implemented correctly)
|
||||
$query = $this->wpdb->prepare("SELECT * FROM {$this->table_name} WHERE id = %d", $id);
|
||||
|
||||
$result = $this->wpdb->get_row($query);
|
||||
|
||||
// SECURITY: Log any database errors
|
||||
if ($this->wpdb->last_error) {
|
||||
error_log('Care Booking Block: Database error in get(): ' . $this->wpdb->last_error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get restrictions by type
|
||||
*
|
||||
* @param string $type Restriction type ('doctor' or 'service')
|
||||
* @return array Array of restriction objects
|
||||
*/
|
||||
public function get_by_type($type)
|
||||
{
|
||||
if (!in_array($type, ['doctor', 'service'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = $this->wpdb->prepare(
|
||||
"SELECT * FROM {$this->table_name} WHERE restriction_type = %s ORDER BY target_id",
|
||||
$type
|
||||
);
|
||||
|
||||
$results = $this->wpdb->get_results($query);
|
||||
|
||||
return is_array($results) ? $results : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all restrictions
|
||||
*
|
||||
* @return array Array of restriction objects
|
||||
*/
|
||||
public function get_all()
|
||||
{
|
||||
$query = "SELECT * FROM {$this->table_name} ORDER BY restriction_type, target_id";
|
||||
|
||||
$results = $this->wpdb->get_results($query);
|
||||
|
||||
return is_array($results) ? $results : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blocked doctor IDs with performance optimization
|
||||
*
|
||||
* @return array Array of blocked doctor IDs
|
||||
*/
|
||||
public function get_blocked_doctors()
|
||||
{
|
||||
// Performance-optimized query using composite index
|
||||
$query = $this->wpdb->prepare(
|
||||
"SELECT target_id FROM {$this->table_name}
|
||||
WHERE restriction_type = %s AND is_blocked = %d
|
||||
ORDER BY target_id",
|
||||
'doctor',
|
||||
1
|
||||
);
|
||||
|
||||
$results = $this->wpdb->get_col($query);
|
||||
|
||||
return is_array($results) ? array_map('intval', $results) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blocked service IDs for specific doctor with performance optimization
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return array Array of blocked service IDs
|
||||
*/
|
||||
public function get_blocked_services($doctor_id)
|
||||
{
|
||||
$doctor_id = absint($doctor_id);
|
||||
if ($doctor_id <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Performance-optimized query using composite index idx_performance_service
|
||||
$query = $this->wpdb->prepare(
|
||||
"SELECT target_id FROM {$this->table_name}
|
||||
WHERE doctor_id = %d AND target_id > 0 AND is_blocked = %d
|
||||
ORDER BY target_id",
|
||||
$doctor_id,
|
||||
1
|
||||
);
|
||||
|
||||
$results = $this->wpdb->get_col($query);
|
||||
|
||||
return is_array($results) ? array_map('intval', $results) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find existing restriction
|
||||
*
|
||||
* @param string $type Restriction type
|
||||
* @param int $target_id Target ID
|
||||
* @param int $doctor_id Doctor ID (for service restrictions)
|
||||
* @return object|false Restriction object or false if not found
|
||||
*/
|
||||
public function find_existing($type, $target_id, $doctor_id = null)
|
||||
{
|
||||
if (!in_array($type, ['doctor', 'service'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$target_id = absint($target_id);
|
||||
if ($target_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type === 'doctor') {
|
||||
$query = $this->wpdb->prepare(
|
||||
"SELECT * FROM {$this->table_name}
|
||||
WHERE restriction_type = %s AND target_id = %d LIMIT 1",
|
||||
$type,
|
||||
$target_id
|
||||
);
|
||||
} else {
|
||||
$doctor_id = absint($doctor_id);
|
||||
if ($doctor_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$query = $this->wpdb->prepare(
|
||||
"SELECT * FROM {$this->table_name}
|
||||
WHERE restriction_type = %s AND target_id = %d AND doctor_id = %d LIMIT 1",
|
||||
$type,
|
||||
$target_id,
|
||||
$doctor_id
|
||||
);
|
||||
}
|
||||
|
||||
return $this->wpdb->get_row($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk insert restrictions
|
||||
*
|
||||
* @param array $restrictions Array of restriction data
|
||||
* @return array Array of inserted IDs (or false for failed insertions)
|
||||
*/
|
||||
public function bulk_insert($restrictions)
|
||||
{
|
||||
if (!is_array($restrictions) || empty($restrictions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($restrictions as $restriction_data) {
|
||||
$result = $this->insert($restriction_data);
|
||||
$results[] = $result;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count restrictions by type
|
||||
*
|
||||
* @param string $type Restriction type
|
||||
* @return int Number of restrictions
|
||||
*/
|
||||
public function count_by_type($type)
|
||||
{
|
||||
if (!in_array($type, ['doctor', 'service'])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$query = $this->wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table_name} WHERE restriction_type = %s",
|
||||
$type
|
||||
);
|
||||
|
||||
$result = $this->wpdb->get_var($query);
|
||||
|
||||
return is_numeric($result) ? (int) $result : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database error if any
|
||||
*
|
||||
* @return string Database error message
|
||||
*/
|
||||
public function get_last_error()
|
||||
{
|
||||
return $this->wpdb->last_error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up restrictions for non-existent targets
|
||||
*
|
||||
* @return int Number of cleaned up restrictions
|
||||
*/
|
||||
public function cleanup_orphaned_restrictions()
|
||||
{
|
||||
// This method would need integration with KiviCare tables
|
||||
// For now, we'll return 0 as a placeholder
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query performance statistics
|
||||
*
|
||||
* @return array Performance stats
|
||||
*/
|
||||
public function get_performance_stats()
|
||||
{
|
||||
$stats = [
|
||||
'total_queries' => $this->wpdb->num_queries,
|
||||
'table_exists' => $this->table_exists(),
|
||||
'row_count' => $this->wpdb->get_var("SELECT COUNT(*) FROM {$this->table_name}"),
|
||||
'index_usage' => $this->analyze_index_usage(),
|
||||
'query_cache_hits' => $this->get_query_cache_stats()
|
||||
];
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze database index usage for optimization
|
||||
*
|
||||
* @return array Index usage statistics
|
||||
*/
|
||||
private function analyze_index_usage()
|
||||
{
|
||||
if (!defined('WP_DEBUG') || !WP_DEBUG) {
|
||||
return ['debug_only' => true];
|
||||
}
|
||||
|
||||
$indexes = [
|
||||
'idx_type_target',
|
||||
'idx_doctor_service',
|
||||
'idx_blocked',
|
||||
'idx_composite_blocked',
|
||||
'idx_performance_doctor',
|
||||
'idx_performance_service'
|
||||
];
|
||||
|
||||
$usage_stats = [];
|
||||
foreach ($indexes as $index) {
|
||||
// This would typically require EXPLAIN queries
|
||||
$usage_stats[$index] = 'active';
|
||||
}
|
||||
|
||||
return $usage_stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query cache statistics
|
||||
*
|
||||
* @return array Cache statistics
|
||||
*/
|
||||
private function get_query_cache_stats()
|
||||
{
|
||||
// Basic query cache monitoring
|
||||
$cache_key = 'care_booking_query_cache_stats';
|
||||
$stats = get_transient($cache_key) ?: ['hits' => 0, 'misses' => 0];
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user