Add caching layer and database helper
- Add CacheHelper for file-based caching with TTL support - Add Database helper for centralized connection management - Update WorkflowModel to cache status transitions (10 min TTL) - Update UserPreferencesModel to cache user prefs (5 min TTL) - Update manage_workflows.php to clear cache on changes - Update get_users.php to use Database helper (example migration) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ RateLimitMiddleware::apply('api');
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
require_once dirname(__DIR__) . '/config/config.php';
|
require_once dirname(__DIR__) . '/config/config.php';
|
||||||
|
require_once dirname(__DIR__) . '/helpers/Database.php';
|
||||||
|
|
||||||
// Check authentication (session already started by RateLimitMiddleware)
|
// Check authentication (session already started by RateLimitMiddleware)
|
||||||
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
||||||
@@ -20,22 +21,10 @@ try {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$conn = new mysqli(
|
|
||||||
$GLOBALS['config']['DB_HOST'],
|
|
||||||
$GLOBALS['config']['DB_USER'],
|
|
||||||
$GLOBALS['config']['DB_PASS'],
|
|
||||||
$GLOBALS['config']['DB_NAME']
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
throw new Exception("Database connection failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Get all users for mentions/assignment
|
// Get all users for mentions/assignment
|
||||||
$sql = "SELECT user_id, username, display_name FROM users ORDER BY display_name, username";
|
$result = Database::query("SELECT user_id, username, display_name FROM users ORDER BY display_name, username");
|
||||||
$result = $conn->query($sql);
|
|
||||||
|
|
||||||
if (!$result) {
|
if (!$result) {
|
||||||
throw new Exception("Failed to query users");
|
throw new Exception("Failed to query users");
|
||||||
@@ -52,8 +41,6 @@ try {
|
|||||||
|
|
||||||
echo json_encode(['success' => true, 'users' => $users]);
|
echo json_encode(['success' => true, 'users' => $users]);
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ ini_set('display_errors', 0);
|
|||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
require_once dirname(__DIR__) . '/middleware/RateLimitMiddleware.php';
|
require_once dirname(__DIR__) . '/middleware/RateLimitMiddleware.php';
|
||||||
|
require_once dirname(__DIR__) . '/models/WorkflowModel.php';
|
||||||
RateLimitMiddleware::apply('api');
|
RateLimitMiddleware::apply('api');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -91,6 +92,7 @@ try {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if ($stmt->execute()) {
|
if ($stmt->execute()) {
|
||||||
|
WorkflowModel::clearCache(); // Clear workflow cache
|
||||||
echo json_encode(['success' => true, 'transition_id' => $conn->insert_id]);
|
echo json_encode(['success' => true, 'transition_id' => $conn->insert_id]);
|
||||||
} else {
|
} else {
|
||||||
echo json_encode(['success' => false, 'error' => $stmt->error]);
|
echo json_encode(['success' => false, 'error' => $stmt->error]);
|
||||||
@@ -118,7 +120,11 @@ try {
|
|||||||
$id
|
$id
|
||||||
);
|
);
|
||||||
|
|
||||||
echo json_encode(['success' => $stmt->execute()]);
|
$success = $stmt->execute();
|
||||||
|
if ($success) {
|
||||||
|
WorkflowModel::clearCache(); // Clear workflow cache
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => $success]);
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -130,7 +136,11 @@ try {
|
|||||||
|
|
||||||
$stmt = $conn->prepare("DELETE FROM status_transitions WHERE transition_id = ?");
|
$stmt = $conn->prepare("DELETE FROM status_transitions WHERE transition_id = ?");
|
||||||
$stmt->bind_param('i', $id);
|
$stmt->bind_param('i', $id);
|
||||||
echo json_encode(['success' => $stmt->execute()]);
|
$success = $stmt->execute();
|
||||||
|
if ($success) {
|
||||||
|
WorkflowModel::clearCache(); // Clear workflow cache
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => $success]);
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
191
helpers/CacheHelper.php
Normal file
191
helpers/CacheHelper.php
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Simple File-Based Cache Helper
|
||||||
|
*
|
||||||
|
* Provides caching for frequently accessed data that doesn't change often,
|
||||||
|
* such as workflow rules, user preferences, and configuration data.
|
||||||
|
*/
|
||||||
|
class CacheHelper {
|
||||||
|
private static $cacheDir = null;
|
||||||
|
private static $memoryCache = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cache directory path
|
||||||
|
*
|
||||||
|
* @return string Cache directory path
|
||||||
|
*/
|
||||||
|
private static function getCacheDir() {
|
||||||
|
if (self::$cacheDir === null) {
|
||||||
|
self::$cacheDir = sys_get_temp_dir() . '/tinker_tickets_cache';
|
||||||
|
if (!is_dir(self::$cacheDir)) {
|
||||||
|
mkdir(self::$cacheDir, 0755, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self::$cacheDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a cache key from components
|
||||||
|
*
|
||||||
|
* @param string $prefix Cache prefix (e.g., 'workflow', 'user_prefs')
|
||||||
|
* @param mixed $identifier Unique identifier
|
||||||
|
* @return string Cache key
|
||||||
|
*/
|
||||||
|
private static function makeKey($prefix, $identifier = null) {
|
||||||
|
$key = $prefix;
|
||||||
|
if ($identifier !== null) {
|
||||||
|
$key .= '_' . md5(serialize($identifier));
|
||||||
|
}
|
||||||
|
return preg_replace('/[^a-zA-Z0-9_]/', '_', $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached data
|
||||||
|
*
|
||||||
|
* @param string $prefix Cache prefix
|
||||||
|
* @param mixed $identifier Unique identifier
|
||||||
|
* @param int $ttl Time-to-live in seconds (default 300 = 5 minutes)
|
||||||
|
* @return mixed|null Cached data or null if not found/expired
|
||||||
|
*/
|
||||||
|
public static function get($prefix, $identifier = null, $ttl = 300) {
|
||||||
|
$key = self::makeKey($prefix, $identifier);
|
||||||
|
|
||||||
|
// Check memory cache first (fastest)
|
||||||
|
if (isset(self::$memoryCache[$key])) {
|
||||||
|
$cached = self::$memoryCache[$key];
|
||||||
|
if (time() - $cached['time'] < $ttl) {
|
||||||
|
return $cached['data'];
|
||||||
|
}
|
||||||
|
unset(self::$memoryCache[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file cache
|
||||||
|
$filePath = self::getCacheDir() . '/' . $key . '.json';
|
||||||
|
if (file_exists($filePath)) {
|
||||||
|
$content = @file_get_contents($filePath);
|
||||||
|
if ($content !== false) {
|
||||||
|
$cached = json_decode($content, true);
|
||||||
|
if ($cached && isset($cached['time']) && isset($cached['data'])) {
|
||||||
|
if (time() - $cached['time'] < $ttl) {
|
||||||
|
// Store in memory cache for faster subsequent access
|
||||||
|
self::$memoryCache[$key] = $cached;
|
||||||
|
return $cached['data'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Expired - delete file
|
||||||
|
@unlink($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store data in cache
|
||||||
|
*
|
||||||
|
* @param string $prefix Cache prefix
|
||||||
|
* @param mixed $identifier Unique identifier
|
||||||
|
* @param mixed $data Data to cache
|
||||||
|
* @return bool Success
|
||||||
|
*/
|
||||||
|
public static function set($prefix, $identifier, $data) {
|
||||||
|
$key = self::makeKey($prefix, $identifier);
|
||||||
|
$cached = [
|
||||||
|
'time' => time(),
|
||||||
|
'data' => $data
|
||||||
|
];
|
||||||
|
|
||||||
|
// Store in memory cache
|
||||||
|
self::$memoryCache[$key] = $cached;
|
||||||
|
|
||||||
|
// Store in file cache
|
||||||
|
$filePath = self::getCacheDir() . '/' . $key . '.json';
|
||||||
|
return @file_put_contents($filePath, json_encode($cached), LOCK_EX) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete cached data
|
||||||
|
*
|
||||||
|
* @param string $prefix Cache prefix
|
||||||
|
* @param mixed $identifier Unique identifier (null to delete all with prefix)
|
||||||
|
* @return bool Success
|
||||||
|
*/
|
||||||
|
public static function delete($prefix, $identifier = null) {
|
||||||
|
if ($identifier !== null) {
|
||||||
|
$key = self::makeKey($prefix, $identifier);
|
||||||
|
unset(self::$memoryCache[$key]);
|
||||||
|
$filePath = self::getCacheDir() . '/' . $key . '.json';
|
||||||
|
return !file_exists($filePath) || @unlink($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all files with this prefix
|
||||||
|
$pattern = self::getCacheDir() . '/' . preg_replace('/[^a-zA-Z0-9_]/', '_', $prefix) . '*.json';
|
||||||
|
$files = glob($pattern);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
@unlink($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear memory cache entries with this prefix
|
||||||
|
foreach (array_keys(self::$memoryCache) as $key) {
|
||||||
|
if (strpos($key, $prefix) === 0) {
|
||||||
|
unset(self::$memoryCache[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all cache
|
||||||
|
*
|
||||||
|
* @return bool Success
|
||||||
|
*/
|
||||||
|
public static function clearAll() {
|
||||||
|
self::$memoryCache = [];
|
||||||
|
|
||||||
|
$files = glob(self::getCacheDir() . '/*.json');
|
||||||
|
foreach ($files as $file) {
|
||||||
|
@unlink($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data from cache or fetch it using a callback
|
||||||
|
*
|
||||||
|
* @param string $prefix Cache prefix
|
||||||
|
* @param mixed $identifier Unique identifier
|
||||||
|
* @param callable $callback Function to call if cache miss
|
||||||
|
* @param int $ttl Time-to-live in seconds
|
||||||
|
* @return mixed Cached or freshly fetched data
|
||||||
|
*/
|
||||||
|
public static function remember($prefix, $identifier, $callback, $ttl = 300) {
|
||||||
|
$data = self::get($prefix, $identifier, $ttl);
|
||||||
|
|
||||||
|
if ($data === null) {
|
||||||
|
$data = $callback();
|
||||||
|
if ($data !== null) {
|
||||||
|
self::set($prefix, $identifier, $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up expired cache files (call periodically)
|
||||||
|
*
|
||||||
|
* @param int $maxAge Maximum age in seconds (default 1 hour)
|
||||||
|
*/
|
||||||
|
public static function cleanup($maxAge = 3600) {
|
||||||
|
$files = glob(self::getCacheDir() . '/*.json');
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if ($now - filemtime($file) > $maxAge) {
|
||||||
|
@unlink($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
174
helpers/Database.php
Normal file
174
helpers/Database.php
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Database Connection Factory
|
||||||
|
*
|
||||||
|
* Centralizes database connection creation and management.
|
||||||
|
* Provides a singleton connection for the request lifecycle.
|
||||||
|
*/
|
||||||
|
class Database {
|
||||||
|
private static $connection = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database connection (singleton pattern)
|
||||||
|
*
|
||||||
|
* @return mysqli Database connection
|
||||||
|
* @throws Exception If connection fails
|
||||||
|
*/
|
||||||
|
public static function getConnection() {
|
||||||
|
if (self::$connection === null) {
|
||||||
|
self::$connection = self::createConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if connection is still alive
|
||||||
|
if (!self::$connection->ping()) {
|
||||||
|
self::$connection = self::createConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new database connection
|
||||||
|
*
|
||||||
|
* @return mysqli Database connection
|
||||||
|
* @throws Exception If connection fails
|
||||||
|
*/
|
||||||
|
private static function createConnection() {
|
||||||
|
// Ensure config is loaded
|
||||||
|
if (!isset($GLOBALS['config'])) {
|
||||||
|
require_once dirname(__DIR__) . '/config/config.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn = new mysqli(
|
||||||
|
$GLOBALS['config']['DB_HOST'],
|
||||||
|
$GLOBALS['config']['DB_USER'],
|
||||||
|
$GLOBALS['config']['DB_PASS'],
|
||||||
|
$GLOBALS['config']['DB_NAME']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
throw new Exception("Database connection failed: " . $conn->connect_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set charset to utf8mb4 for proper Unicode support
|
||||||
|
$conn->set_charset('utf8mb4');
|
||||||
|
|
||||||
|
return $conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the database connection
|
||||||
|
*/
|
||||||
|
public static function close() {
|
||||||
|
if (self::$connection !== null) {
|
||||||
|
self::$connection->close();
|
||||||
|
self::$connection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin a transaction
|
||||||
|
*
|
||||||
|
* @return bool Success
|
||||||
|
*/
|
||||||
|
public static function beginTransaction() {
|
||||||
|
return self::getConnection()->begin_transaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit a transaction
|
||||||
|
*
|
||||||
|
* @return bool Success
|
||||||
|
*/
|
||||||
|
public static function commit() {
|
||||||
|
return self::getConnection()->commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rollback a transaction
|
||||||
|
*
|
||||||
|
* @return bool Success
|
||||||
|
*/
|
||||||
|
public static function rollback() {
|
||||||
|
return self::getConnection()->rollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a query and return results
|
||||||
|
*
|
||||||
|
* @param string $sql SQL query with placeholders
|
||||||
|
* @param string $types Parameter types (i=int, s=string, d=double, b=blob)
|
||||||
|
* @param array $params Parameters to bind
|
||||||
|
* @return mysqli_result|bool Query result
|
||||||
|
*/
|
||||||
|
public static function query($sql, $types = '', $params = []) {
|
||||||
|
$conn = self::getConnection();
|
||||||
|
|
||||||
|
if (empty($types) || empty($params)) {
|
||||||
|
return $conn->query($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $conn->prepare($sql);
|
||||||
|
if (!$stmt) {
|
||||||
|
throw new Exception("Query preparation failed: " . $conn->error);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->bind_param($types, ...$params);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute an INSERT/UPDATE/DELETE and return affected rows
|
||||||
|
*
|
||||||
|
* @param string $sql SQL query with placeholders
|
||||||
|
* @param string $types Parameter types
|
||||||
|
* @param array $params Parameters to bind
|
||||||
|
* @return int Affected rows (-1 on failure)
|
||||||
|
*/
|
||||||
|
public static function execute($sql, $types = '', $params = []) {
|
||||||
|
$conn = self::getConnection();
|
||||||
|
|
||||||
|
$stmt = $conn->prepare($sql);
|
||||||
|
if (!$stmt) {
|
||||||
|
throw new Exception("Query preparation failed: " . $conn->error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($types) && !empty($params)) {
|
||||||
|
$stmt->bind_param($types, ...$params);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
$affected = $stmt->affected_rows;
|
||||||
|
$stmt->close();
|
||||||
|
return $affected;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = $stmt->error;
|
||||||
|
$stmt->close();
|
||||||
|
throw new Exception("Query execution failed: " . $error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last insert ID
|
||||||
|
*
|
||||||
|
* @return int Last insert ID
|
||||||
|
*/
|
||||||
|
public static function lastInsertId() {
|
||||||
|
return self::getConnection()->insert_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape a string for use in queries (prefer prepared statements)
|
||||||
|
*
|
||||||
|
* @param string $string String to escape
|
||||||
|
* @return string Escaped string
|
||||||
|
*/
|
||||||
|
public static function escape($string) {
|
||||||
|
return self::getConnection()->real_escape_string($string);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* UserPreferencesModel
|
* UserPreferencesModel
|
||||||
* Handles user-specific preferences and settings
|
* Handles user-specific preferences and settings with caching
|
||||||
*/
|
*/
|
||||||
|
require_once dirname(__DIR__) . '/helpers/CacheHelper.php';
|
||||||
|
|
||||||
class UserPreferencesModel {
|
class UserPreferencesModel {
|
||||||
private $conn;
|
private $conn;
|
||||||
|
private static $CACHE_PREFIX = 'user_prefs';
|
||||||
|
private static $CACHE_TTL = 300; // 5 minutes
|
||||||
|
|
||||||
public function __construct($conn) {
|
public function __construct($conn) {
|
||||||
$this->conn = $conn;
|
$this->conn = $conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all preferences for a user
|
* Get all preferences for a user (with caching)
|
||||||
* @param int $userId User ID
|
* @param int $userId User ID
|
||||||
* @return array Associative array of preference_key => preference_value
|
* @return array Associative array of preference_key => preference_value
|
||||||
*/
|
*/
|
||||||
public function getUserPreferences($userId) {
|
public function getUserPreferences($userId) {
|
||||||
|
return CacheHelper::remember(self::$CACHE_PREFIX, $userId, function() use ($userId) {
|
||||||
$sql = "SELECT preference_key, preference_value
|
$sql = "SELECT preference_key, preference_value
|
||||||
FROM user_preferences
|
FROM user_preferences
|
||||||
WHERE user_id = ?";
|
WHERE user_id = ?";
|
||||||
@@ -29,7 +33,9 @@ class UserPreferencesModel {
|
|||||||
while ($row = $result->fetch_assoc()) {
|
while ($row = $result->fetch_assoc()) {
|
||||||
$prefs[$row['preference_key']] = $row['preference_value'];
|
$prefs[$row['preference_key']] = $row['preference_value'];
|
||||||
}
|
}
|
||||||
|
$stmt->close();
|
||||||
return $prefs;
|
return $prefs;
|
||||||
|
}, self::$CACHE_TTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,7 +51,15 @@ class UserPreferencesModel {
|
|||||||
ON DUPLICATE KEY UPDATE preference_value = VALUES(preference_value)";
|
ON DUPLICATE KEY UPDATE preference_value = VALUES(preference_value)";
|
||||||
$stmt = $this->conn->prepare($sql);
|
$stmt = $this->conn->prepare($sql);
|
||||||
$stmt->bind_param("iss", $userId, $key, $value);
|
$stmt->bind_param("iss", $userId, $key, $value);
|
||||||
return $stmt->execute();
|
$result = $stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
// Invalidate cache for this user
|
||||||
|
if ($result) {
|
||||||
|
CacheHelper::delete(self::$CACHE_PREFIX, $userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,17 +70,8 @@ class UserPreferencesModel {
|
|||||||
* @return mixed Preference value or default
|
* @return mixed Preference value or default
|
||||||
*/
|
*/
|
||||||
public function getPreference($userId, $key, $default = null) {
|
public function getPreference($userId, $key, $default = null) {
|
||||||
$sql = "SELECT preference_value FROM user_preferences
|
$prefs = $this->getUserPreferences($userId);
|
||||||
WHERE user_id = ? AND preference_key = ?";
|
return $prefs[$key] ?? $default;
|
||||||
$stmt = $this->conn->prepare($sql);
|
|
||||||
$stmt->bind_param("is", $userId, $key);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
|
|
||||||
if ($row = $result->fetch_assoc()) {
|
|
||||||
return $row['preference_value'];
|
|
||||||
}
|
|
||||||
return $default;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,7 +85,15 @@ class UserPreferencesModel {
|
|||||||
WHERE user_id = ? AND preference_key = ?";
|
WHERE user_id = ? AND preference_key = ?";
|
||||||
$stmt = $this->conn->prepare($sql);
|
$stmt = $this->conn->prepare($sql);
|
||||||
$stmt->bind_param("is", $userId, $key);
|
$stmt->bind_param("is", $userId, $key);
|
||||||
return $stmt->execute();
|
$result = $stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
// Invalidate cache for this user
|
||||||
|
if ($result) {
|
||||||
|
CacheHelper::delete(self::$CACHE_PREFIX, $userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,7 +105,21 @@ class UserPreferencesModel {
|
|||||||
$sql = "DELETE FROM user_preferences WHERE user_id = ?";
|
$sql = "DELETE FROM user_preferences WHERE user_id = ?";
|
||||||
$stmt = $this->conn->prepare($sql);
|
$stmt = $this->conn->prepare($sql);
|
||||||
$stmt->bind_param("i", $userId);
|
$stmt->bind_param("i", $userId);
|
||||||
return $stmt->execute();
|
$result = $stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
// Invalidate cache for this user
|
||||||
|
if ($result) {
|
||||||
|
CacheHelper::delete(self::$CACHE_PREFIX, $userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all user preferences cache
|
||||||
|
*/
|
||||||
|
public static function clearCache() {
|
||||||
|
CacheHelper::delete(self::$CACHE_PREFIX);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
|
||||||
|
|||||||
@@ -1,14 +1,49 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* WorkflowModel - Handles status transition workflows and validation
|
* WorkflowModel - Handles status transition workflows and validation
|
||||||
|
*
|
||||||
|
* Uses caching for frequently accessed transition rules since they rarely change.
|
||||||
*/
|
*/
|
||||||
|
require_once dirname(__DIR__) . '/helpers/CacheHelper.php';
|
||||||
|
|
||||||
class WorkflowModel {
|
class WorkflowModel {
|
||||||
private $conn;
|
private $conn;
|
||||||
|
private static $CACHE_PREFIX = 'workflow';
|
||||||
|
private static $CACHE_TTL = 600; // 10 minutes
|
||||||
|
|
||||||
public function __construct($conn) {
|
public function __construct($conn) {
|
||||||
$this->conn = $conn;
|
$this->conn = $conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active transitions (with caching)
|
||||||
|
*
|
||||||
|
* @return array All active transitions indexed by from_status
|
||||||
|
*/
|
||||||
|
private function getAllTransitions() {
|
||||||
|
return CacheHelper::remember(self::$CACHE_PREFIX, 'all_transitions', function() {
|
||||||
|
$sql = "SELECT from_status, to_status, requires_comment, requires_admin
|
||||||
|
FROM status_transitions
|
||||||
|
WHERE is_active = TRUE";
|
||||||
|
$result = $this->conn->query($sql);
|
||||||
|
|
||||||
|
$transitions = [];
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$from = $row['from_status'];
|
||||||
|
if (!isset($transitions[$from])) {
|
||||||
|
$transitions[$from] = [];
|
||||||
|
}
|
||||||
|
$transitions[$from][$row['to_status']] = [
|
||||||
|
'to_status' => $row['to_status'],
|
||||||
|
'requires_comment' => (bool)$row['requires_comment'],
|
||||||
|
'requires_admin' => (bool)$row['requires_admin']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $transitions;
|
||||||
|
}, self::$CACHE_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get allowed status transitions for a given status
|
* Get allowed status transitions for a given status
|
||||||
*
|
*
|
||||||
@@ -16,21 +51,13 @@ class WorkflowModel {
|
|||||||
* @return array Array of allowed transitions with requirements
|
* @return array Array of allowed transitions with requirements
|
||||||
*/
|
*/
|
||||||
public function getAllowedTransitions($currentStatus) {
|
public function getAllowedTransitions($currentStatus) {
|
||||||
$sql = "SELECT to_status, requires_comment, requires_admin
|
$allTransitions = $this->getAllTransitions();
|
||||||
FROM status_transitions
|
|
||||||
WHERE from_status = ? AND is_active = TRUE";
|
|
||||||
$stmt = $this->conn->prepare($sql);
|
|
||||||
$stmt->bind_param("s", $currentStatus);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
|
|
||||||
$transitions = [];
|
if (!isset($allTransitions[$currentStatus])) {
|
||||||
while ($row = $result->fetch_assoc()) {
|
return [];
|
||||||
$transitions[] = $row;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt->close();
|
return array_values($allTransitions[$currentStatus]);
|
||||||
return $transitions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,22 +74,15 @@ class WorkflowModel {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql = "SELECT requires_admin FROM status_transitions
|
$allTransitions = $this->getAllTransitions();
|
||||||
WHERE from_status = ? AND to_status = ? AND is_active = TRUE";
|
|
||||||
$stmt = $this->conn->prepare($sql);
|
|
||||||
$stmt->bind_param("ss", $fromStatus, $toStatus);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
|
|
||||||
if ($result->num_rows === 0) {
|
if (!isset($allTransitions[$fromStatus][$toStatus])) {
|
||||||
$stmt->close();
|
|
||||||
return false; // Transition not defined
|
return false; // Transition not defined
|
||||||
}
|
}
|
||||||
|
|
||||||
$row = $result->fetch_assoc();
|
$transition = $allTransitions[$fromStatus][$toStatus];
|
||||||
$stmt->close();
|
|
||||||
|
|
||||||
if ($row['requires_admin'] && !$isAdmin) {
|
if ($transition['requires_admin'] && !$isAdmin) {
|
||||||
return false; // Admin required
|
return false; // Admin required
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +95,7 @@ class WorkflowModel {
|
|||||||
* @return array Array of unique status values
|
* @return array Array of unique status values
|
||||||
*/
|
*/
|
||||||
public function getAllStatuses() {
|
public function getAllStatuses() {
|
||||||
|
return CacheHelper::remember(self::$CACHE_PREFIX, 'all_statuses', function() {
|
||||||
$sql = "SELECT DISTINCT from_status as status FROM status_transitions
|
$sql = "SELECT DISTINCT from_status as status FROM status_transitions
|
||||||
UNION
|
UNION
|
||||||
SELECT DISTINCT to_status as status FROM status_transitions
|
SELECT DISTINCT to_status as status FROM status_transitions
|
||||||
@@ -87,6 +108,7 @@ class WorkflowModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return $statuses;
|
return $statuses;
|
||||||
|
}, self::$CACHE_TTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,21 +119,23 @@ class WorkflowModel {
|
|||||||
* @return array|null Transition requirements or null if not found
|
* @return array|null Transition requirements or null if not found
|
||||||
*/
|
*/
|
||||||
public function getTransitionRequirements($fromStatus, $toStatus) {
|
public function getTransitionRequirements($fromStatus, $toStatus) {
|
||||||
$sql = "SELECT requires_comment, requires_admin
|
$allTransitions = $this->getAllTransitions();
|
||||||
FROM status_transitions
|
|
||||||
WHERE from_status = ? AND to_status = ? AND is_active = TRUE";
|
|
||||||
$stmt = $this->conn->prepare($sql);
|
|
||||||
$stmt->bind_param("ss", $fromStatus, $toStatus);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
|
|
||||||
if ($result->num_rows === 0) {
|
if (!isset($allTransitions[$fromStatus][$toStatus])) {
|
||||||
$stmt->close();
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$row = $result->fetch_assoc();
|
$transition = $allTransitions[$fromStatus][$toStatus];
|
||||||
$stmt->close();
|
return [
|
||||||
return $row;
|
'requires_comment' => $transition['requires_comment'],
|
||||||
|
'requires_admin' => $transition['requires_admin']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear workflow cache (call when transitions are modified)
|
||||||
|
*/
|
||||||
|
public static function clearCache() {
|
||||||
|
CacheHelper::delete(self::$CACHE_PREFIX);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user