diff --git a/qa-config-example.php b/qa-config-example.php index 7f555c658..264f777f1 100644 --- a/qa-config-example.php +++ b/qa-config-example.php @@ -98,6 +98,15 @@ define('QA_CACHE_DIRECTORY', '/path/to/writable_cache_directory/'); */ +/* + If you wish to use memcached-based caching, you can define the host and port in which the + memcached server resides. Default values are 127.0.0.1 and 11211, respectively. You only need + to define the constats below if you want to use different values for them. + + define('QA_MEMCACHED_HOST', '123.123.123.123'); + define('QA_MEMCACHED_PORT', 12345); +*/ + /* If you wish, you can define QA_COOKIE_DOMAIN so that any cookies created by Q2A are assigned to a specific domain name, instead of the full domain name of the request by default. This is diff --git a/qa-include/Q2A/Storage/CacheDriver.php b/qa-include/Q2A/Storage/CacheDriver.php index 97b9f3f6a..18c2b24d5 100644 --- a/qa-include/Q2A/Storage/CacheDriver.php +++ b/qa-include/Q2A/Storage/CacheDriver.php @@ -57,7 +57,7 @@ public function delete($key); * @param int $start Offset from which to start (used for 'batching' deletes). * @param bool $expiredOnly Delete cache only if it has expired. * - * @return int Number of files deleted. + * @return int Number of items deleted. */ public function clear($limit = 0, $start = 0, $expiredOnly = false); @@ -68,13 +68,6 @@ public function clear($limit = 0, $start = 0, $expiredOnly = false); */ public function isEnabled(); - /** - * Get the last error. - * - * @return string - */ - public function getError(); - /** * Get the prefix used for all cache keys. * @@ -85,7 +78,14 @@ public function getKeyPrefix(); /** * Get current statistics for the cache. * - * @return array Array of stats: 'files' => number of files, 'size' => total file size in bytes. + * @return array Array of stats: 'items' => number of items, 'size' => total item size in bytes. */ public function getStats(); + + /** + * Perform test operations and return an error string or null if no error was found + * + * @return string|null + */ + public function test(); } diff --git a/qa-include/Q2A/Storage/CacheFactory.php b/qa-include/Q2A/Storage/CacheFactory.php index fa9b57ce8..b72dec467 100644 --- a/qa-include/Q2A/Storage/CacheFactory.php +++ b/qa-include/Q2A/Storage/CacheFactory.php @@ -37,23 +37,22 @@ public static function getCacheDriver() $config = array( 'enabled' => (int) qa_opt('caching_enabled') === 1, 'keyprefix' => QA_FINAL_MYSQL_DATABASE . '.' . QA_MYSQL_TABLE_PREFIX . '.', - 'dir' => defined('QA_CACHE_DIRECTORY') ? QA_CACHE_DIRECTORY : null, ); $driver = qa_opt('caching_driver'); - switch($driver) - { + switch ($driver) { case 'memcached': self::$cacheDriver = new Q2A_Storage_MemcachedDriver($config); break; case 'filesystem': default: + $config['dir'] = defined('QA_CACHE_DIRECTORY') ? QA_CACHE_DIRECTORY : null; + self::$cacheDriver = new Q2A_Storage_FileCacheDriver($config); break; } - } return self::$cacheDriver; diff --git a/qa-include/Q2A/Storage/FileCacheDriver.php b/qa-include/Q2A/Storage/FileCacheDriver.php index 929582ca6..e51801d33 100644 --- a/qa-include/Q2A/Storage/FileCacheDriver.php +++ b/qa-include/Q2A/Storage/FileCacheDriver.php @@ -27,7 +27,6 @@ class Q2A_Storage_FileCacheDriver implements Q2A_Storage_CacheDriver { private $enabled = false; private $keyPrefix = ''; - private $error; private $cacheDir; private $phpProtect = ''; @@ -46,16 +45,12 @@ public function __construct($config) $this->keyPrefix = $config['keyprefix']; } - if (isset($config['dir'])) { - $this->cacheDir = realpath($config['dir']); - if (!is_writable($this->cacheDir)) { - $this->error = qa_lang_html_sub('admin/caching_dir_error', $config['dir']); - } - } else { - $this->error = qa_lang_html('admin/caching_dir_missing'); + if (!isset($config['dir'])) { + return; } - $this->enabled = empty($this->error); + $this->enabled = true; + $this->cacheDir = realpath($config['dir']); } /** @@ -107,7 +102,7 @@ public function get($key) public function set($key, $data, $ttl) { $success = false; - $ttl = (int) $ttl; + $ttl = (int)$ttl; $fullKey = $this->keyPrefix . $key; if ($this->enabled && $ttl > 0) { @@ -171,7 +166,7 @@ public function clear($limit = 0, $start = 0, $expiredOnly = false) $fp = fopen($file, 'r'); $skipLine = fgets($fp); $key = fgets($fp); - $expiry = (int) trim(fgets($fp)); + $expiry = (int)trim(fgets($fp)); if (time() > $expiry) { $wasDeleted = $this->deleteFile($file); } @@ -205,16 +200,6 @@ public function isEnabled() return $this->enabled; } - /** - * Get the last error. - * - * @return string - */ - public function getError() - { - return $this->error; - } - /** * Get the prefix used for all cache keys. * @@ -228,29 +213,32 @@ public function getKeyPrefix() /** * Get current statistics for the cache. * - * @return array Array of stats: 'files' => number of files, 'size' => total file size in bytes. + * @return array Array of stats: 'items' => number of files, 'size' => total item size in bytes. */ public function getStats() { if (!$this->enabled) { - return array('files' => 0, 'size' => 0); + return array('items' => 0, 'size' => 0); } $totalFiles = 0; $totalBytes = 0; - $dirIter = new RecursiveDirectoryIterator($this->cacheDir); - foreach (new RecursiveIteratorIterator($dirIter) as $file) { - if (strpos($file->getFilename(), '.') === 0) { - // TODO: use FilesystemIterator::SKIP_DOTS once we're on minimum PHP 5.3 - continue; - } + try { + $dirIter = new RecursiveDirectoryIterator($this->cacheDir); + foreach (new RecursiveIteratorIterator($dirIter) as $file) { + if (strpos($file->getFilename(), '.') === 0) { + // TODO: use FilesystemIterator::SKIP_DOTS once we're on minimum PHP 5.3 + continue; + } - $totalFiles++; - $totalBytes += $file->getSize(); + $totalFiles++; + $totalBytes += $file->getSize(); + } + } catch (Exception $e) { } return array( - 'files' => $totalFiles, + 'items' => $totalFiles, 'size' => $totalBytes, ); } @@ -281,4 +269,47 @@ private function getFilename($fullKey) $filename = sha1($fullKey); return $this->cacheDir . '/' . substr($filename, 0, 1) . '/' . substr($filename, 1, 2) . '/' . $filename . '.php'; } + + /** + * Perform test operations and return an error string or null if no error was found + * + * @return string|null + */ + public function test() + { + if (!$this->enabled) { + return null; + } + + if (!isset($this->cacheDir)) { + return qa_lang_html('admin/caching_dir_missing'); + } + + try { + if (!is_writable($this->cacheDir)) { + throw new Exception(); + } + + $dirIter = new RecursiveDirectoryIterator($this->cacheDir); + foreach (new RecursiveIteratorIterator($dirIter) as $file) { + if (strpos($file->getFilename(), '.') === 0) { + // TODO: use FilesystemIterator::SKIP_DOTS once we're on minimum PHP 5.3 + continue; + } + + break; + } + + $result = $this->set('test', 'TEST', 1); + if (!$result) { + throw new Exception(); + } + } catch (Exception $e) { + return qa_lang_html_sub('admin/caching_dir_error', $this->cacheDir); + } finally { + $this->delete('test'); + } + + return null; + } } diff --git a/qa-include/Q2A/Storage/MemcachedDriver.php b/qa-include/Q2A/Storage/MemcachedDriver.php index f3c881121..a2765e260 100644 --- a/qa-include/Q2A/Storage/MemcachedDriver.php +++ b/qa-include/Q2A/Storage/MemcachedDriver.php @@ -28,11 +28,10 @@ class Q2A_Storage_MemcachedDriver implements Q2A_Storage_CacheDriver private $memcached; private $enabled = false; private $keyPrefix = ''; - private $error; private $flushed = false; - const HOST = '127.0.0.1'; - const PORT = 11211; + private $host = '127.0.0.1'; + private $port = 11211; /** * Creates a new Memcached instance and checks we can cache items. @@ -50,17 +49,21 @@ public function __construct($config) $this->keyPrefix = $config['keyprefix']; } - if (extension_loaded('memcached')) { - $this->memcached = new Memcached; - $this->memcached->addServer(self::HOST, self::PORT); - if ($this->memcached->set($this->keyPrefix . 'test', 'TEST')) { - $this->enabled = true; - } else { - $this->setMemcachedError(); - } - } else { - $this->error = qa_lang_html('admin/no_memcached'); + if (!extension_loaded('memcached')) { + return; + } + + if (defined('QA_MEMCACHED_HOST')) { + $this->host = QA_MEMCACHED_HOST; } + if (defined('QA_MEMCACHED_PORT')) { + $this->port = QA_MEMCACHED_PORT; + } + + $this->memcached = new Memcached; + $this->memcached->addServer($this->host, $this->port); + + $this->enabled = true; } /** @@ -77,12 +80,7 @@ public function get($key) $result = $this->memcached->get($this->keyPrefix . $key); - if ($result === false) { - $this->setMemcachedError(); - return null; - } - - return $result; + return $this->memcached->getResultCode() === Memcached::RES_SUCCESS ? $result : null; } /** @@ -91,7 +89,7 @@ public function get($key) * @param mixed $data The data to cache (in core Q2A this is usually an array). * @param int $ttl Number of minutes for which to cache the data. * - * @return bool Whether the file was successfully cached. + * @return bool Whether the item was successfully cached. */ public function set($key, $data, $ttl) { @@ -99,15 +97,9 @@ public function set($key, $data, $ttl) return false; } - $ttl = (int) $ttl; - $expiry = time() + ($ttl * 60); - $success = $this->memcached->set($this->keyPrefix . $key, $data, $expiry); - - if (!$success) { - $this->setMemcachedError(); - } + $expiry = time() + ((int)$ttl * 60); - return $success; + return $this->memcached->set($this->keyPrefix . $key, $data, $expiry); } /** @@ -122,13 +114,7 @@ public function delete($key) return false; } - $success = $this->memcached->delete($this->keyPrefix . $key); - - if (!$success) { - $this->setMemcachedError(); - } - - return $success; + return $this->memcached->delete($this->keyPrefix . $key); } /** @@ -137,18 +123,14 @@ public function delete($key) * @param int $start Offset from which to start (used for 'batching' deletes). * @param bool $expiredOnly This parameter is ignored because Memcached automatically clears expired items. * - * @return int Number of files deleted. For Memcached we return 0 + * @return int Number of elements deleted. For Memcached we return 0 */ public function clear($limit = 0, $start = 0, $expiredOnly = false) { if ($this->enabled && !$expiredOnly && !$this->flushed) { - $success = $this->memcached->flush(); // avoid multiple calls to flush() $this->flushed = true; - - if (!$success) { - $this->setMemcachedError(); - } + $this->memcached->flush(); } return 0; @@ -164,16 +146,6 @@ public function isEnabled() return $this->enabled; } - /** - * Get the last error. - * - * @return string - */ - public function getError() - { - return $this->error; - } - /** * Get the prefix used for all cache keys. * @@ -187,7 +159,7 @@ public function getKeyPrefix() /** * Get current statistics for the cache. * - * @return array Array of stats: 'files' => number of files, 'size' => total file size in bytes. + * @return array Array of stats: 'items' => number of files, 'size' => total item size in bytes. */ public function getStats() { @@ -196,25 +168,53 @@ public function getStats() if ($this->enabled) { $stats = $this->memcached->getStats(); - $key = self::HOST . ':' . self::PORT; + $key = $this->host . ':' . $this->port; $totalFiles = isset($stats[$key]['curr_items']) ? $stats[$key]['curr_items'] : 0; $totalBytes = isset($stats[$key]['bytes']) ? $stats[$key]['bytes'] : 0; } return array( - 'files' => $totalFiles, + 'items' => $totalFiles, 'size' => $totalBytes, ); } /** - * Set current error to Memcached result message + * Return result code from Memcached instance + */ + public function getResultCode() + { + return isset($this->memcached) ? $this->memcached->getResultCode() : null; + } + + /** + * Return result message from Memcached instance + */ + public function getResultMessage() + { + return isset($this->memcached) ? $this->memcached->getResultMessage() : null; + } + + /** + * Perform test operations and return an error string or null if no error was found * - * @return void + * @return string|null */ - private function setMemcachedError() + public function test() { - $this->error = qa_lang_html_sub('admin/memcached_error', $this->memcached->getResultMessage()); + if (!extension_loaded('memcached')) { + return qa_lang_html('admin/no_memcached'); + } + + if (!$this->enabled) { + return null; + } + + if (!$this->memcached->set($this->keyPrefix . 'test', 'TEST')) { + return qa_lang_html_sub('admin/memcached_error', $this->memcached->getResultMessage()); + } + + return null; } } diff --git a/qa-include/pages/admin/admin-default.php b/qa-include/pages/admin/admin-default.php index b7ccc62c0..8f0323bdb 100644 --- a/qa-include/pages/admin/admin-default.php +++ b/qa-include/pages/admin/admin-default.php @@ -1804,7 +1804,7 @@ function qa_optionfield_make_select(&$optionfield, $options, $value, $default) case 'caching': $cacheDriver = Q2A_Storage_CacheFactory::getCacheDriver(); - $qa_content['error'] = $cacheDriver->getError(); + $qa_content['error'] = $cacheDriver->test(); $cacheStats = $cacheDriver->getStats(); $qa_content['form_2'] = array( @@ -1818,7 +1818,7 @@ function qa_optionfield_make_select(&$optionfield, $options, $value, $default) 'cache_files' => array( 'type' => 'static', 'label' => qa_lang_html('admin/caching_num_items'), - 'value' => qa_html(qa_format_number($cacheStats['files'])), + 'value' => qa_html(qa_format_number($cacheStats['items'])), ), 'cache_size' => array( 'type' => 'static',