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:
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user