core/xpdo/cache/xpdocachemanager.class.php
<?php
/*
* Copyright 2010-2015 by MODX, LLC.
*
* This file is part of xPDO.
*
* xPDO is free software; you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* xPDO is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* xPDO; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
* Suite 330, Boston, MA 02111-1307 USA
*/
/**
* Classes implementing a default cache implementation for xPDO.
*
* @package xpdo
* @subpackage cache
*/
/**
* The default cache manager implementation for xPDO.
*
* @package xpdo
* @subpackage cache
*/
class xPDOCacheManager {
const CACHE_PHP = 0;
const CACHE_JSON = 1;
const CACHE_SERIALIZE = 2;
const CACHE_DIR = 'objects/';
const LOG_DIR = 'logs/';
protected $xpdo= null;
protected $caches= array();
protected $options= array();
protected $_umask= null;
public function __construct(& $xpdo, $options = array()) {
$this->xpdo= & $xpdo;
$this->options= $options;
$this->_umask= umask();
}
/**
* Get an instance of a provider which implements the xPDOCache interface.
*/
public function & getCacheProvider($key = '', $options = array()) {
$objCache = null;
if (empty($key)) {
$key = $this->getOption(xPDO::OPT_CACHE_KEY, $options, 'default');
}
$objCacheClass= 'xPDOFileCache';
if (!isset($this->caches[$key]) || !is_object($this->caches[$key])) {
if ($cacheClass = $this->getOption($key . '_' . xPDO::OPT_CACHE_HANDLER, $options, $this->getOption(xPDO::OPT_CACHE_HANDLER, $options))) {
$cacheClass = $this->xpdo->loadClass($cacheClass, XPDO_CORE_PATH, false, true);
if ($cacheClass) {
$objCacheClass= $cacheClass;
}
}
$options[xPDO::OPT_CACHE_KEY]= $key;
$this->caches[$key] = new $objCacheClass($this->xpdo, $options);
if (empty($this->caches[$key]) || !$this->caches[$key]->isInitialized()) {
$this->caches[$key] = new xPDOFileCache($this->xpdo, $options);
}
$objCache = $this->caches[$key];
$objCacheClass= get_class($objCache);
} else {
$objCache =& $this->caches[$key];
$objCacheClass= get_class($objCache);
}
if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Returning {$objCacheClass}:{$key} cache provider from available providers: " . print_r(array_keys($this->caches), 1));
return $objCache;
}
/**
* Get an option from supplied options, the cacheManager options, or xpdo itself.
*
* @param string $key Unique identifier for the option.
* @param array $options A set of explicit options to override those from xPDO or the
* xPDOCacheManager implementation.
* @param mixed $default An optional default value to return if no value is found.
* @return mixed The value of the option.
*/
public function getOption($key, $options = array(), $default = null) {
$option = $default;
if (is_array($key)) {
if (!is_array($option)) {
$default= $option;
$option= array();
}
foreach ($key as $k) {
$option[$k]= $this->getOption($k, $options, $default);
}
} elseif (is_string($key) && !empty($key)) {
if (is_array($options) && !empty($options) && array_key_exists($key, $options)) {
$option = $options[$key];
} elseif (is_array($this->options) && !empty($this->options) && array_key_exists($key, $this->options)) {
$option = $this->options[$key];
} else {
$option = $this->xpdo->getOption($key, null, $default);
}
}
return $option;
}
/**
* Get default folder permissions based on umask
*
* @return integer The default folder permissions.
*/
public function getFolderPermissions() {
$perms = 0777;
$perms = $perms & (0777 - $this->_umask);
return $perms;
}
/**
* Get default file permissions based on umask
*
* @return integer The default file permissions.
*/
public function getFilePermissions() {
$perms = 0666;
$perms = $perms & (0777 - $this->_umask);
return $perms;
}
/**
* Get the absolute path to a writable directory for storing files.
*
* @access public
* @return string The absolute path of the xPDO cache directory.
*/
public function getCachePath() {
$cachePath= false;
if (empty($this->xpdo->cachePath)) {
if (!isset ($this->xpdo->config['cache_path'])) {
while (true) {
if (!empty ($_ENV['TMP'])) {
if ($cachePath= strtr($_ENV['TMP'], '\\', '/'))
break;
}
if (!empty ($_ENV['TMPDIR'])) {
if ($cachePath= strtr($_ENV['TMPDIR'], '\\', '/'))
break;
}
if (!empty ($_ENV['TEMP'])) {
if ($cachePath= strtr($_ENV['TEMP'], '\\', '/'))
break;
}
if ($temp_file= @ tempnam(md5(uniqid(rand(), TRUE)), '')) {
$cachePath= strtr(dirname($temp_file), '\\', '/');
@ unlink($temp_file);
}
break;
}
if ($cachePath) {
if ($cachePath[strlen($cachePath) - 1] != '/') $cachePath .= '/';
$cachePath .= '.xpdo-cache';
}
}
else {
$cachePath= strtr($this->xpdo->config['cache_path'], '\\', '/');
}
} else {
$cachePath= $this->xpdo->cachePath;
}
if ($cachePath) {
$perms = $this->getOption('new_folder_permissions', null, $this->getFolderPermissions());
if (is_string($perms)) $perms = octdec($perms);
if (@ $this->writeTree($cachePath, $perms)) {
if ($cachePath[strlen($cachePath) - 1] != '/') $cachePath .= '/';
if (!is_writeable($cachePath)) {
@ chmod($cachePath, $perms);
}
} else {
$cachePath= false;
}
}
return $cachePath;
}
/**
* Writes a file to the filesystem.
*
* @access public
* @param string $filename The absolute path to the location the file will
* be written in.
* @param string $content The content of the newly written file.
* @param string $mode The php file mode to write in. Defaults to 'wb'. Note that this method always
* uses a (with b or t if specified) to open the file and that any mode except a means existing file
* contents will be overwritten.
* @param array $options An array of options for the function.
* @return int|bool Returns the number of bytes written to the file or false on failure.
*/
public function writeFile($filename, $content, $mode= 'wb', $options= array()) {
$written= false;
if (!is_array($options)) {
$options = is_scalar($options) && !is_bool($options) ? array('new_folder_permissions' => $options) : array();
}
$dirname= dirname($filename);
if (!file_exists($dirname)) {
$this->writeTree($dirname, $options);
}
$mode = str_replace('+', '', $mode);
switch ($mode[0]) {
case 'a':
$append = true;
break;
default:
$append = false;
break;
}
$fmode = (strlen($mode) > 1 && in_array($mode[1], array('b', 't'))) ? "a{$mode[1]}" : 'a';
$file= @fopen($filename, $fmode);
if ($file) {
if ($append === true) {
$written= fwrite($file, $content);
} else {
$locked = false;
$attempt = 1;
$attempts = (integer) $this->getOption(xPDO::OPT_CACHE_ATTEMPTS, $options, 1);
$attemptDelay = (integer) $this->getOption(xPDO::OPT_CACHE_ATTEMPT_DELAY, $options, 1000);
while (!$locked && ($attempts === 0 || $attempt <= $attempts)) {
if ($this->getOption('use_flock', $options, true)) {
$locked = flock($file, LOCK_EX | LOCK_NB);
} else {
$lockFile = $this->lockFile($filename, $options);
$locked = $lockFile != false;
}
if (!$locked && $attemptDelay > 0 && ($attempts === 0 || $attempt < $attempts)) {
usleep($attemptDelay);
}
$attempt++;
}
if ($locked) {
fseek($file, 0);
ftruncate($file, 0);
$written= fwrite($file, $content);
if ($this->getOption('use_flock', $options, true)) {
flock($file, LOCK_UN);
} else {
$this->unlockFile($filename, $options);
}
}
}
@fclose($file);
if ($written !== false && $fileMode = $this->getOption('new_file_permissions', $options, false)) {
if (is_string($fileMode)) $fileMode = octdec($fileMode);
@ chmod($filename, $fileMode);
}
}
return ($written !== false);
}
/**
* Add an exclusive lock to a file for atomic write operations in multi-threaded environments.
*
* xPDO::OPT_USE_FLOCK must be set to false (or 0) or xPDO will assume flock is reliable.
*
* @param string $file The name of the file to lock.
* @param array $options An array of options for the process.
* @return boolean True only if the current process obtained an exclusive lock for writing.
*/
public function lockFile($file, array $options = array()) {
$locked = false;
$lockDir = $this->getOption('lock_dir', $options, $this->getCachePath() . 'locks' . DIRECTORY_SEPARATOR);
if ($this->writeTree($lockDir, $options)) {
$lockFile = $this->lockFileName($file, $options);
if (!file_exists($lockFile)) {
$myPID = (XPDO_CLI_MODE || !isset($_SERVER['SERVER_ADDR']) ? gethostname() : $_SERVER['SERVER_ADDR']) . '.' . getmypid();
$myPID .= mt_rand();
$tmpLockFile = "{$lockFile}.{$myPID}";
if (file_put_contents($tmpLockFile, $myPID)) {
if (link($tmpLockFile, $lockFile)) {
$locked = true;
}
@unlink($tmpLockFile);
}
}
} else {
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "The lock_dir at {$lockDir} is not writable and could not be created");
}
if (!$locked) $this->xpdo->log(xPDO::LOG_LEVEL_WARN, "Attempt to lock file {$file} failed");
return $locked;
}
/**
* Release an exclusive lock on a file created by lockFile().
*
* @param string $file The name of the file to unlock.
* @param array $options An array of options for the process.
*/
public function unlockFile($file, array $options = array()) {
@unlink($this->lockFileName($file, $options));
}
/**
* Get an absolute path to a lock file for a specified file path.
*
* @param string $file The absolute path to get the lock filename for.
* @param array $options An array of options for the process.
* @return string The absolute path for the lock file
*/
protected function lockFileName($file, array $options = array()) {
$lockDir = $this->getOption('lock_dir', $options, $this->getCachePath() . 'locks' . DIRECTORY_SEPARATOR);
return $lockDir . preg_replace('/\W/', '_', $file) . $this->getOption(xPDO::OPT_LOCKFILE_EXTENSION, $options, '.lock');
}
/**
* Recursively writes a directory tree of files to the filesystem
*
* @access public
* @param string $dirname The directory to write
* @param array $options An array of options for the function. Can also be a value representing
* a permissions mode to write new directories with, though this is deprecated.
* @return boolean Returns true if the directory was successfully written.
*/
public function writeTree($dirname, $options= array()) {
$written= false;
if (!empty ($dirname)) {
if (!is_array($options)) $options = is_scalar($options) && !is_bool($options) ? array('new_folder_permissions' => $options) : array();
$mode = $this->getOption('new_folder_permissions', $options, $this->getFolderPermissions());
if (is_string($mode)) $mode = octdec($mode);
$dirname= strtr(trim($dirname), '\\', '/');
if ($dirname[strlen($dirname) - 1] == '/') $dirname = substr($dirname, 0, strlen($dirname) - 1);
if (is_dir($dirname) || (is_writable(dirname($dirname)) && @mkdir($dirname, $mode))) {
$written= true;
} elseif (!$this->writeTree(dirname($dirname), $options)) {
$written= false;
} else {
$written= @ mkdir($dirname, $mode);
}
if ($written) {
@ chmod($dirname, $mode);
}
}
return $written;
}
/**
* Copies a file from a source file to a target directory.
*
* @access public
* @param string $source The absolute path of the source file.
* @param string $target The absolute path of the target destination
* directory.
* @param array $options An array of options for this function.
* @return boolean|array Returns true if the copy operation was successful, or a single element
* array with filename as key and stat results of the successfully copied file as a result.
*/
public function copyFile($source, $target, $options = array()) {
$copied= false;
if (!is_array($options)) $options = is_scalar($options) && !is_bool($options) ? array('new_file_permissions' => $options) : array();
if (func_num_args() === 4) $options['new_folder_permissions'] = func_get_arg(3);
if ($this->writeTree(dirname($target), $options)) {
$existed= file_exists($target);
if ($existed && $this->getOption('copy_newer_only', $options, false) && (($ttime = filemtime($target)) > ($stime = filemtime($source)))) {
$this->xpdo->log(xPDO::LOG_LEVEL_INFO, "xPDOCacheManager->copyFile(): Skipping copy of newer file {$target} ({$ttime}) from {$source} ({$stime})");
} else {
$copied= copy($source, $target);
}
if ($copied) {
if (!$this->getOption('copy_preserve_permissions', $options, false)) {
$fileMode = $this->getOption('new_file_permissions', $options, $this->getFilePermissions());
if (is_string($fileMode)) $fileMode = octdec($fileMode);
@ chmod($target, $fileMode);
}
if ($this->getOption('copy_preserve_filemtime', $options, true)) @ touch($target, filemtime($source));
if ($this->getOption('copy_return_file_stat', $options, false)) {
$stat = stat($target);
if (is_array($stat)) {
$stat['overwritten']= $existed;
$copied = array($target => $stat);
}
}
}
}
if (!$copied) {
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "xPDOCacheManager->copyFile(): Could not copy file {$source} to {$target}");
}
return $copied;
}
/**
* Recursively copies a directory tree from a source directory to a target
* directory.
*
* @access public
* @param string $source The absolute path of the source directory.
* @param string $target The absolute path of the target destination directory.
* @param array $options An array of options for this function.
* @return array|boolean Returns an array of all files and folders that were copied or false.
*/
public function copyTree($source, $target, $options= array()) {
$copied= false;
$source= strtr($source, '\\', '/');
$target= strtr($target, '\\', '/');
if ($source[strlen($source) - 1] == '/') $source = substr($source, 0, strlen($source) - 1);
if ($target[strlen($target) - 1] == '/') $target = substr($target, 0, strlen($target) - 1);
if (is_dir($source . '/')) {
if (!is_array($options)) $options = is_scalar($options) && !is_bool($options) ? array('new_folder_permissions' => $options) : array();
if (func_num_args() === 4) $options['new_file_permissions'] = func_get_arg(3);
if (!is_dir($target . '/')) {
$this->writeTree($target . '/', $options);
}
if (is_dir($target)) {
if (!is_writable($target)) {
$dirMode = $this->getOption('new_folder_permissions', $options, $this->getFolderPermissions());
if (is_string($dirMode)) $dirMode = octdec($dirMode);
if (! @ chmod($target, $dirMode)) {
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "{$target} is not writable and permissions could not be modified.");
}
}
if ($handle= @ opendir($source)) {
$excludeItems = $this->getOption('copy_exclude_items', $options, array('.', '..','.svn','.svn/','.svn\\'));
$excludePatterns = $this->getOption('copy_exclude_patterns', $options);
$copiedFiles = array();
$error = false;
while (false !== ($item= readdir($handle))) {
$copied = false;
if (is_array($excludeItems) && !empty($excludeItems) && in_array($item, $excludeItems)) continue;
if (is_array($excludePatterns) && !empty($excludePatterns) && $this->matches($item, $excludePatterns)) continue;
$from= $source . '/' . $item;
$to= $target . '/' . $item;
if (is_dir($from)) {
if (!($copied= $this->copyTree($from, $to, $options))) {
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not copy directory {$from} to {$to}");
$error = true;
} else {
$copiedFiles = array_merge($copiedFiles, $copied);
}
} elseif (is_file($from)) {
if (!$copied= $this->copyFile($from, $to, $options)) {
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not copy file {$from} to {$to}; could not create directory.");
$error = true;
} else {
$copiedFiles[] = $to;
}
} else {
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not copy {$from} to {$to}");
}
}
@ closedir($handle);
if (!$error) $copiedFiles[] = $target;
$copied = $copiedFiles;
} else {
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not read source directory {$source}");
}
} else {
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not create target directory {$target}");
}
} else {
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Source directory {$source} does not exist.");
}
return $copied;
}
/**
* Recursively deletes a directory tree of files.
*
* @access public
* @param string $dirname An absolute path to the source directory to delete.
* @param array $options An array of options for this function.
* @return boolean Returns true if the deletion was successful.
*/
public function deleteTree($dirname, $options= array('deleteTop' => false, 'skipDirs' => false, 'extensions' => array('.cache.php'))) {
$result= false;
if (is_dir($dirname)) { /* Operate on dirs only */
if (substr($dirname, -1) != '/') {
$dirname .= '/';
}
$result= array ();
if (!is_array($options)) {
$numArgs = func_num_args();
$options = array(
'deleteTop' => is_scalar($options) ? (boolean) $options : false
,'skipDirs' => $numArgs > 2 ? func_get_arg(2) : false
,'extensions' => $numArgs > 3 ? func_get_arg(3) : array('.cache.php')
);
}
$hasMore= true;
if ($handle= opendir($dirname)) {
$limit= 4;
$extensions= $this->getOption('extensions', $options, array('.cache.php'));
$excludeItems = $this->getOption('delete_exclude_items', $options, array('.', '..','.svn','.svn/','.svn\\'));
$excludePatterns = $this->getOption('delete_exclude_patterns', $options);
while ($hasMore && $limit--) {
if (!$handle) {
$handle= opendir($dirname);
}
$hasMore= false;
if (!is_resource($handle)) {
continue;
}
while (false !== ($file= @ readdir($handle))) {
if (is_array($excludeItems) && !empty($excludeItems) && in_array($file, $excludeItems)) continue;
if (is_array($excludePatterns) && !empty($excludePatterns) && $this->matches($file, $excludePatterns)) continue;
if ($file != '.' && $file != '..') { /* Ignore . and .. */
$path= $dirname . $file;
if (is_dir($path)) {
$suboptions = array_merge($options, array('deleteTop' => !$this->getOption('skipDirs', $options, false)));
if ($subresult= $this->deleteTree($path, $suboptions)) {
$result= array_merge($result, $subresult);
}
}
elseif (is_file($path)) {
if (is_array($extensions) && !empty($extensions) && !$this->endsWith($file, $extensions)) continue;
if (unlink($path)) {
array_push($result, $path);
} else {
$hasMore= true;
}
}
}
}
closedir($handle);
}
if ($this->getOption('deleteTop', $options, false)) {
if (@ rmdir($dirname)) {
array_push($result, $dirname);
}
}
}
} else {
$result= false; /* return false if attempting to operate on a file */
}
return $result;
}
/**
* Sees if a string ends with a specific pattern or set of patterns.
*
* @access public
* @param string $string The string to check.
* @param string|array $pattern The pattern or an array of patterns to check against.
* @return boolean True if the string ends with the pattern or any of the patterns provided.
*/
public function endsWith($string, $pattern) {
$matched= false;
if (is_string($string) && ($stringLen= strlen($string))) {
if (is_array($pattern)) {
foreach ($pattern as $subPattern) {
if (is_string($subPattern) && $this->endsWith($string, $subPattern)) {
$matched= true;
break;
}
}
} elseif (is_string($pattern)) {
if (($patternLen= strlen($pattern)) && $stringLen >= $patternLen) {
$matched= (substr($string, -$patternLen) === $pattern);
}
}
}
return $matched;
}
/**
* Sees if a string matches a specific pattern or set of patterns.
*
* @access public
* @param string $string The string to check.
* @param string|array $pattern The pattern or an array of patterns to check against.
* @return boolean True if the string matched the pattern or any of the patterns provided.
*/
public function matches($string, $pattern) {
$matched= false;
if (is_string($string) && ($stringLen= strlen($string))) {
if (is_array($pattern)) {
foreach ($pattern as $subPattern) {
if (is_string($subPattern) && $this->matches($string, $subPattern)) {
$matched= true;
break;
}
}
} elseif (is_string($pattern)) {
$matched= preg_match($pattern, $string);
}
}
return $matched;
}
/**
* Generate a PHP executable representation of an xPDOObject.
*
* @todo Complete $generateRelated functionality.
* @todo Add stdObject support.
*
* @access public
* @param xPDOObject $obj An xPDOObject to generate the cache file for
* @param string $objName The name of the xPDOObject
* @param boolean $generateObjVars If true, will also generate maps for all
* object variables. Defaults to false.
* @param boolean $generateRelated If true, will also generate maps for all
* related objects. Defaults to false.
* @param string $objRef The reference to the xPDO instance, in string
* format.
* @param boolean $format The format to cache in. Defaults to
* xPDOCacheManager::CACHE_PHP, which is set to cache in executable PHP format.
* @return string The source map file, in string format.
*/
public function generateObject($obj, $objName, $generateObjVars= false, $generateRelated= false, $objRef= 'this->xpdo', $format= xPDOCacheManager::CACHE_PHP) {
$source= false;
if (is_object($obj) && $obj instanceof xPDOObject) {
$className= $obj->_class;
$source= "\${$objName}= \${$objRef}->newObject('{$className}');\n";
$source .= "\${$objName}->fromArray(" . var_export($obj->toArray('', true), true) . ", '', true, true);\n";
if ($generateObjVars && $objectVars= get_object_vars($obj)) {
foreach ($objectVars as $vk => $vv) {
if ($vk === 'modx') {
$source .= "\${$objName}->{$vk}= & \${$objRef};\n";
}
elseif ($vk === 'xpdo') {
$source .= "\${$objName}->{$vk}= & \${$objRef};\n";
}
elseif (!is_resource($vv)) {
$source .= "\${$objName}->{$vk}= " . var_export($vv, true) . ";\n";
}
}
}
if ($generateRelated && !empty ($obj->_relatedObjects)) {
foreach ($obj->_relatedObjects as $className => $fk) {
foreach ($fk as $key => $relObj) {} /* TODO: complete $generateRelated functionality */
}
}
}
return $source;
}
/**
* Add a key-value pair to a cache provider if it does not already exist.
*
* @param string $key A unique key identifying the item being stored.
* @param mixed & $var A reference to the PHP variable representing the item.
* @param integer $lifetime Seconds the item will be valid in cache.
* @param array $options Additional options for the cache add operation.
*/
public function add($key, & $var, $lifetime= 0, $options= array()) {
$return= false;
if ($cache = $this->getCacheProvider($this->getOption(xPDO::OPT_CACHE_KEY, $options))) {
$value= null;
if (is_object($var) && $var instanceof xPDOObject) {
$value= $var->toArray('', true);
} else {
$value= $var;
}
$return= $cache->add($key, $value, $lifetime, $options);
}
return $return;
}
/**
* Replace a key-value pair in in a cache provider.
*
* @access public
* @param string $key A unique key identifying the item being replaced.
* @param mixed & $var A reference to the PHP variable representing the item.
* @param integer $lifetime Seconds the item will be valid in objcache.
* @param array $options Additional options for the cache replace operation.
* @return boolean True if the replace was successful.
*/
public function replace($key, & $var, $lifetime= 0, $options= array()) {
$return= false;
if ($cache = $this->getCacheProvider($this->getOption(xPDO::OPT_CACHE_KEY, $options), $options)) {
$value= null;
if (is_object($var) && $var instanceof xPDOObject) {
$value= $var->toArray('', true);
} else {
$value= $var;
}
$return= $cache->replace($key, $value, $lifetime, $options);
}
return $return;
}
/**
* Set a key-value pair in a cache provider.
*
* @access public
* @param string $key A unique key identifying the item being set.
* @param mixed & $var A reference to the PHP variable representing the item.
* @param integer $lifetime Seconds the item will be valid in objcache.
* @param array $options Additional options for the cache set operation.
* @return boolean True if the set was successful
*/
public function set($key, & $var, $lifetime= 0, $options= array()) {
$return= false;
if ($cache = $this->getCacheProvider($this->getOption(xPDO::OPT_CACHE_KEY, $options), $options)) {
$value= null;
if (is_object($var) && $var instanceof xPDOObject) {
$value= $var->toArray('', true);
} else {
$value= $var;
}
$return= $cache->set($key, $value, $lifetime, $options);
} else {
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'No cache implementation found.');
}
return $return;
}
/**
* Delete a key-value pair from a cache provider.
*
* @access public
* @param string $key A unique key identifying the item being deleted.
* @param array $options Additional options for the cache deletion.
* @return boolean True if the deletion was successful.
*/
public function delete($key, $options = array()) {
$return= false;
if ($cache = $this->getCacheProvider($this->getOption(xPDO::OPT_CACHE_KEY, $options), $options)) {
$return= $cache->delete($key, $options);
}
return $return;
}
/**
* Get a value from a cache provider by key.
*
* @access public
* @param string $key A unique key identifying the item being retrieved.
* @param array $options Additional options for the cache retrieval.
* @return mixed The value of the object cache key
*/
public function get($key, $options = array()) {
$return= false;
if ($cache = $this->getCacheProvider($this->getOption(xPDO::OPT_CACHE_KEY, $options), $options)) {
$return= $cache->get($key, $options);
}
return $return;
}
/**
* Flush the contents of a cache provider.
*
* @access public
* @param array $options Additional options for the cache flush.
* @return boolean True if the flush was successful.
*/
public function clean($options = array()) {
$return= false;
if ($cache = $this->getCacheProvider($this->getOption(xPDO::OPT_CACHE_KEY, $options), $options)) {
$return= $cache->flush($options);
}
return $return;
}
/**
* Refresh specific or all cache providers.
*
* The default behavior is to call clean() with the provided options
*
* @param array $providers An associative array with keys representing the cache provider key
* and the value an array of options.
* @param array &$results An associative array for collecting results for each provider.
* @return array An array of results for each provider that is refreshed.
*/
public function refresh(array $providers = array(), array &$results = array()) {
if (empty($providers)) {
foreach ($this->caches as $cacheKey => $cache) {
$providers[$cacheKey] = array();
}
}
foreach ($providers as $key => $options) {
if (array_key_exists($key, $this->caches) && !array_key_exists($key, $results)) {
$results[$key] = $this->clean(array_merge($options, array(xPDO::OPT_CACHE_KEY => $key)));
}
}
return (array_search(false, $results, true) === false);
}
/**
* Escapes all single quotes in a string
*
* @access public
* @param string $s The string to escape single quotes in.
* @return string The string with single quotes escaped.
*/
public function escapeSingleQuotes($s) {
$q1= array (
"\\",
"'"
);
$q2= array (
"\\\\",
"\\'"
);
return str_replace($q1, $q2, $s);
}
}
/**
* An abstract class that defines the methods a cache provider must implement.
*
* @package xpdo
* @subpackage cache
*/
abstract class xPDOCache {
public $xpdo= null;
protected $options= array();
protected $key= '';
protected $initialized= false;
public function __construct(& $xpdo, $options = array()) {
$this->xpdo= & $xpdo;
$this->options= $options;
$this->key = $this->getOption(xPDO::OPT_CACHE_KEY, $options, 'default');
}
/**
* Indicates if this xPDOCache instance has been properly initialized.
*
* @return boolean true if the implementation was initialized successfully.
*/
public function isInitialized() {
return (boolean) $this->initialized;
}
/**
* Get an option from supplied options, the cache options, or the xpdo config.
*
* @param string $key Unique identifier for the option.
* @param array $options A set of explicit options to override those from xPDO or the xPDOCache
* implementation.
* @param mixed $default An optional default value to return if no value is found.
* @return mixed The value of the option.
*/
public function getOption($key, $options = array(), $default = null) {
$option = $default;
if (is_array($key)) {
if (!is_array($option)) {
$default= $option;
$option= array();
}
foreach ($key as $k) {
$option[$k]= $this->getOption($k, $options, $default);
}
} elseif (is_string($key) && !empty($key)) {
if (is_array($options) && !empty($options) && array_key_exists($key, $options)) {
$option = $options[$key];
} elseif (is_array($this->options) && !empty($this->options) && array_key_exists($key, $this->options)) {
$option = $this->options[$key];
} else {
$option = $this->xpdo->cacheManager->getOption($key, null, $default);
}
}
return $option;
}
/**
* Get the actual cache key the implementation will use.
*
* @param string $key The identifier the application uses.
* @param array $options Additional options for the operation.
* @return string The identifier with any implementation specific prefixes or other
* transformations applied.
*/
public function getCacheKey($key, $options = array()) {
$prefix = $this->getOption('cache_prefix', $options);
if (!empty($prefix)) $key = $prefix . $key;
return $this->key . '/' . $key;
}
/**
* Adds a value to the cache.
*
* @access public
* @param string $key A unique key identifying the item being set.
* @param mixed $var A reference to the PHP variable representing the item.
* @param integer $expire The amount of seconds for the variable to expire in.
* @param array $options Additional options for the operation.
* @return boolean True if successful
*/
abstract public function add($key, $var, $expire= 0, $options= array());
/**
* Sets a value in the cache.
*
* @access public
* @param string $key A unique key identifying the item being set.
* @param mixed $var A reference to the PHP variable representing the item.
* @param integer $expire The amount of seconds for the variable to expire in.
* @param array $options Additional options for the operation.
* @return boolean True if successful
*/
abstract public function set($key, $var, $expire= 0, $options= array());
/**
* Replaces a value in the cache.
*
* @access public
* @param string $key A unique key identifying the item being set.
* @param mixed $var A reference to the PHP variable representing the item.
* @param integer $expire The amount of seconds for the variable to expire in.
* @param array $options Additional options for the operation.
* @return boolean True if successful
*/
abstract public function replace($key, $var, $expire= 0, $options= array());
/**
* Deletes a value from the cache.
*
* @access public
* @param string $key A unique key identifying the item being deleted.
* @param array $options Additional options for the operation.
* @return boolean True if successful
*/
abstract public function delete($key, $options= array());
/**
* Gets a value from the cache.
*
* @access public
* @param string $key A unique key identifying the item to fetch.
* @param array $options Additional options for the operation.
* @return mixed The value retrieved from the cache.
*/
public function get($key, $options= array()) {}
/**
* Flush all values from the cache.
*
* @access public
* @param array $options Additional options for the operation.
* @return boolean True if successful.
*/
abstract public function flush($options= array());
}
/**
* A simple file-based caching implementation using executable PHP.
*
* This can be used to relieve database loads, though the overall performance is
* about the same as without the file-based cache. For maximum performance and
* scalability, use a server with memcached and the PHP memcache extension
* configured.
*
* @package xpdo
* @subpackage cache
*/
class xPDOFileCache extends xPDOCache {
public function __construct(& $xpdo, $options = array()) {
parent :: __construct($xpdo, $options);
$this->initialized = true;
}
public function getCacheKey($key, $options = array()) {
$cachePath = $this->getOption('cache_path', $options);
$cacheExt = $this->getOption('cache_ext', $options, '.cache.php');
$key = parent :: getCacheKey($key, $options);
return $cachePath . $key . $cacheExt;
}
public function add($key, $var, $expire= 0, $options= array()) {
$added= false;
if (!file_exists($this->getCacheKey($key, $options))) {
if ($expire === true)
$expire= 0;
$added= $this->set($key, $var, $expire, $options);
}
return $added;
}
public function set($key, $var, $expire= 0, $options= array()) {
$set= false;
if ($var !== null) {
if ($expire === true)
$expire= 0;
$expirationTS= $expire ? time() + $expire : 0;
$expireContent= '';
if ($expirationTS) {
$expireContent= 'if(time() > ' . $expirationTS . '){return null;}';
}
$fileName= $this->getCacheKey($key, $options);
$format = (integer) $this->getOption(xPDO::OPT_CACHE_FORMAT, $options, xPDOCacheManager::CACHE_PHP);
switch ($format) {
case xPDOCacheManager::CACHE_SERIALIZE:
$content= serialize(array('expires' => $expirationTS, 'content' => $var));
break;
case xPDOCacheManager::CACHE_JSON:
$content= $this->xpdo->toJSON(array('expires' => $expirationTS, 'content' => $var));
break;
case xPDOCacheManager::CACHE_PHP:
default:
$content= '<?php ' . $expireContent . ' return ' . var_export($var, true) . ';';
break;
}
$folderMode = $this->getOption('new_cache_folder_permissions', $options, false);
if ($folderMode) $options['new_folder_permissions'] = $folderMode;
$fileMode = $this->getOption('new_cache_file_permissions', $options, false);
if ($fileMode) $options['new_file_permissions'] = $fileMode;
$set= $this->xpdo->cacheManager->writeFile($fileName, $content, 'wb', $options);
}
return $set;
}
public function replace($key, $var, $expire= 0, $options= array()) {
$replaced= false;
if (file_exists($this->getCacheKey($key, $options))) {
if ($expire === true)
$expire= 0;
$replaced= $this->set($key, $var, $expire, $options);
}
return $replaced;
}
public function delete($key, $options= array()) {
$deleted= false;
if ($this->getOption('multiple_object_delete', $options, true)) {
$cacheKey= $this->getCacheKey($key, array_merge($options, array('cache_ext' => '')));
if (file_exists($cacheKey) && is_dir($cacheKey)) {
$results = $this->xpdo->cacheManager->deleteTree($cacheKey, array_merge(array('deleteTop' => false, 'skipDirs' => false, 'extensions' => array('.cache.php')), $options));
if ($results !== false) {
$deleted = true;
}
}
}
$cacheKey= $this->getCacheKey($key, $options);
if (file_exists($cacheKey)) {
$deleted= @ unlink($cacheKey);
}
return $deleted;
}
public function get($key, $options= array()) {
$value= null;
$cacheKey= $this->getCacheKey($key, $options);
if (file_exists($cacheKey)) {
if ($file = @fopen($cacheKey, 'rb')) {
$format = (integer) $this->getOption(xPDO::OPT_CACHE_FORMAT, $options, xPDOCacheManager::CACHE_PHP);
if (flock($file, LOCK_SH)) {
switch ($format) {
case xPDOCacheManager::CACHE_PHP:
if (!filesize($cacheKey)) {
$value= false;
break;
}
$value= @include $cacheKey;
break;
case xPDOCacheManager::CACHE_JSON:
$payload = stream_get_contents($file);
if ($payload !== false) {
$payload = $this->xpdo->fromJSON($payload);
if (is_array($payload) && isset($payload['expires']) && (empty($payload['expires']) || time() < $payload['expires'])) {
if (array_key_exists('content', $payload)) {
$value= $payload['content'];
}
}
}
break;
case xPDOCacheManager::CACHE_SERIALIZE:
$payload = stream_get_contents($file);
if ($payload !== false) {
$payload = unserialize($payload);
if (is_array($payload) && isset($payload['expires']) && (empty($payload['expires']) || time() < $payload['expires'])) {
if (array_key_exists('content', $payload)) {
$value= $payload['content'];
}
}
}
break;
}
flock($file, LOCK_UN);
if ($value === null && $this->getOption('removeIfEmpty', $options, true)) {
fclose($file);
@ unlink($cacheKey);
return $value;
}
}
@fclose($file);
}
}
return $value;
}
public function flush($options= array()) {
$cacheKey= $this->getCacheKey('', array_merge($options, array('cache_ext' => '')));
$results = $this->xpdo->cacheManager->deleteTree($cacheKey, array_merge(array('deleteTop' => false, 'skipDirs' => false, 'extensions' => array('.cache.php')), $options));
return ($results !== false);
}
}