diff --git a/framework/caching/DbCache.php b/framework/caching/DbCache.php index 3bd1fcf..ca4198e 100644 --- a/framework/caching/DbCache.php +++ b/framework/caching/DbCache.php @@ -16,16 +16,28 @@ use yii\db\dao\Query; /** * DbCache implements a cache application component by storing cached data in a database. * - * DbCache stores cache data in a DB table named {@link cacheTableName}. - * If the table does not exist, it will be automatically created. - * By setting {@link autoCreateCacheTable} to false, you can also manually create the DB table. + * DbCache stores cache data in a DB table whose name is specified via [[cacheTableName]]. + * For MySQL database, the table should be created beforehand as follows : * - * DbCache relies on {@link http://www.php.net/manual/en/ref.pdo.php PDO} to access database. - * By default, it will use a SQLite3 database under the application runtime directory. - * You can also specify {@link connectionID} so that it makes use of - * a DB application component to access database. + * ~~~ + * CREATE TABLE tbl_cache ( + * id char(128) NOT NULL, + * expire int(11) DEFAULT NULL, + * data LONGBLOB, + * PRIMARY KEY (id), + * KEY expire (expire) + * ); + * ~~~ * - * See {@link CCache} manual for common cache operations that are supported by DbCache. + * You should replace `LONGBLOB` as follows if you are using a different DBMS: + * + * - PostgreSQL: `BYTEA` + * - SQLite, SQL server, Oracle: `BLOB` + * + * DbCache connects to the database via the DB connection specified in [[connectionID]] + * which must refer to a valid DB application component. + * + * Please refer to [[Cache]] for common cache operations that are supported by DbCache. * * @property Connection $dbConnection The DB connection instance. * @@ -35,30 +47,16 @@ use yii\db\dao\Query; class DbCache extends Cache { /** - * @var string the ID of the {@link Connection} application component. If not set, - * a SQLite3 database will be automatically created and used. The SQLite database file - * is protected/runtime/cache-YiiVersion.db. + * @var string the ID of the [[Connection|DB connection]] application component. + * Defaults to 'db'. */ - public $connectionID; + public $connectionID = 'db'; /** - * @var string name of the DB table to store cache content. Defaults to 'YiiCache'. - * Note, if {@link autoCreateCacheTable} is false and you want to create the DB table - * manually by yourself, you need to make sure the DB table is of the following structure: - *
-	 * (id CHAR(128) PRIMARY KEY, expire INTEGER, value BLOB)
-	 * 
- * Note, some DBMS might not support BLOB type. In this case, replace 'BLOB' with a suitable - * binary data type (e.g. LONGBLOB in MySQL, BYTEA in PostgreSQL.) - * @see autoCreateCacheTable + * @var string name of the DB table to store cache content. Defaults to 'tbl_cache'. + * The table must be created before using this cache component. */ public $cacheTableName = 'tbl_cache'; /** - * @var boolean whether the cache DB table should be created automatically if it does not exist. Defaults to true. - * If you already have the table created, it is recommended you set this property to be false to improve performance. - * @see cacheTableName - */ - public $autoCreateCacheTable = true; - /** * @var integer the probability (parts per million) that garbage collection (GC) should be performed * when storing a piece of data in the cache. Defaults to 100, meaning 0.01% chance. * This number should be between 0 and 1000000. A value 0 meaning no GC will be performed at all. @@ -70,57 +68,6 @@ class DbCache extends Cache private $_db; /** - * Initializes this application component. - * - * This method is required by the {@link IApplicationComponent} interface. - * It ensures the existence of the cache DB table. - * It also removes expired data items from the cache. - */ - public function init() - { - parent::init(); - - $db = $this->getDbConnection(); - $db->setActive(true); - if ($this->autoCreateCacheTable) { - $sql = "DELETE FROM {$this->cacheTableName} WHERE expire>0 AND expire<" . time(); - try { - $db->createCommand($sql)->execute(); - } catch (Exception $e) { - $this->createCacheTable($db, $this->cacheTableName); - } - } - } - - /** - * Creates the cache DB table. - * @param Connection $db the database connection - * @param string $tableName the name of the table to be created - */ - protected function createCacheTable($db, $tableName) - { - $driver = $db->getDriverName(); - if ($driver === 'mysql') { - $blob = 'LONGBLOB'; - } else { - if ($driver === 'pgsql') { - $blob = 'BYTEA'; - } else { - $blob = 'BLOB'; - } - } - $sql = <<createCommand($sql)->execute(); - } - - /** * Returns the DB connection instance used for caching purpose. * @return Connection the DB connection instance * @throws Exception if [[connectionID]] does not point to a valid application component. @@ -128,7 +75,8 @@ EOD; public function getDbConnection() { if ($this->_db === null) { - if ($this->connectionID !== null && ($db = \Yii::$application->getComponent($this->connectionID)) instanceof Connection) { + $db = \Yii::$application->getComponent($this->connectionID); + if ($db instanceof Connection) { $this->_db = $db; } else { throw new Exception("DbCache.connectionID must refer to the ID of a DB connection application component."); @@ -155,10 +103,9 @@ EOD; protected function getValue($key) { $query = new Query; - $query->select(array('value')) + $query->select(array('data')) ->from($this->cacheTableName) - ->where(array('id' => $key)) - ->andWhere('expire = 0 OR expire > ' . time()); + ->where('id = :id AND (expire = 0 OR expire > :time)', array(':id' => $key, ':time' => time())); $db = $this->getDbConnection(); if ($db->queryCachingDuration >= 0) { $duration = $db->queryCachingDuration; @@ -181,19 +128,20 @@ EOD; if (empty($keys)) { return array(); } - - $ids = implode("','", $keys); - $time = time(); - $sql = "SELECT id, value FROM {$this->cacheTableName} WHERE id IN ('$ids') AND (expire=0 OR expire>$time)"; + $query = new Query; + $query->select(array('id', 'data')) + ->from($this->cacheTableName) + ->where(array('id' => $keys)) + ->andWhere("expire = 0 OR expire > " . time() . ")"); $db = $this->getDbConnection(); - if ($db->queryCachingDuration > 0) { + if ($db->queryCachingDuration >= 0) { $duration = $db->queryCachingDuration; - $db->queryCachingDuration = 0; - $rows = $db->createCommand($sql)->queryAll(); + $db->queryCachingDuration = -1; + $rows = $query->createCommand($db)->queryAll(); $db->queryCachingDuration = $duration; } else { - $rows = $db->createCommand($sql)->queryAll(); + $rows = $query->createCommand($db)->queryAll(); } $results = array(); @@ -201,7 +149,7 @@ EOD; $results[$key] = false; } foreach ($rows as $row) { - $results[$row['id']] = $row['value']; + $results[$row['id']] = $row['data']; } return $results; } @@ -217,9 +165,21 @@ EOD; */ protected function setValue($key, $value, $expire) { - $this->deleteValue($key); - return $this->addValue($key, $value, $expire); - } + $query = new Query; + $command = $query->update($this->cacheTableName, array( + 'expire' => $expire > 0 ? $expire + time() : 0, + 'data' => array($value, \PDO::PARAM_LOB), + ), array( + 'id' => $key, + ))->createCommand($this->getDbConnection()); + + if ($command->execute()) { + $this->gc(); + return true; + } else { + return $this->addValue($key, $value, $expire); + } + } /** * Stores a value identified by a key into cache if the cache does not contain this key. @@ -232,19 +192,21 @@ EOD; */ protected function addValue($key, $value, $expire) { - if (mt_rand(0, 1000000) < $this->_gcProbability) { - $this->gc(); - } + $this->gc(); if ($expire > 0) { $expire += time(); } else { $expire = 0; } - $sql = "INSERT INTO {$this->cacheTableName} (id,expire,value) VALUES ('$key',$expire,:value)"; + + $query = new Query; + $command = $query->insert($this->cacheTableName, array( + 'id' => $key, + 'expire' => $expire, + 'data' => array($value, \PDO::PARAM_LOB), + ))->createCommand($this->getDbConnection()); try { - $command = $this->getDbConnection()->createCommand($sql); - $command->bindValue(':value', $value, PDO::PARAM_LOB); $command->execute(); return true; } catch (Exception $e) { @@ -260,28 +222,39 @@ EOD; */ protected function deleteValue($key) { - $sql = "DELETE FROM {$this->cacheTableName} WHERE id='$key'"; - $this->getDbConnection()->createCommand($sql)->execute(); + $query = new Query; + $query->delete($this->cacheTableName, array('id' => $key)) + ->createCommand($this->getDbConnection()) + ->execute(); return true; } /** * Removes the expired data values. + * @param boolean $force whether to enforce the garbage collection regardless of [[gcProbability]]. + * Defaults to false, meaning the actual deletion happens with the probability as specified by [[gcProbability]]. */ - protected function gc() + public function gc($force = false) { - $this->getDbConnection()->createCommand("DELETE FROM {$this->cacheTableName} WHERE expire>0 AND expire<" . time())->execute(); + if ($force || mt_rand(0, 1000000) < $this->gcProbability) { + $query = new Query; + $query->delete($this->cacheTableName, 'expire > 0 AND expire < ' . time()) + ->createCommand($this->getDbConnection()) + ->execute(); + } } /** * Deletes all values from cache. * This is the implementation of the method declared in the parent class. * @return boolean whether the flush operation was successful. - * @since 1.1.5 */ protected function flushValues() { - $this->getDbConnection()->createCommand("DELETE FROM {$this->cacheTableName}")->execute(); + $query = new Query; + $query->delete($this->cacheTableName) + ->createCommand($this->getDbConnection()) + ->execute(); return true; } } diff --git a/framework/caching/FileCache.php b/framework/caching/FileCache.php index 9de4b7f..2d043b4 100644 --- a/framework/caching/FileCache.php +++ b/framework/caching/FileCache.php @@ -1,6 +1,6 @@ * @since 2.0 */ -class CFileCache extends CCache +class FileCache extends Cache { /** - * @var string the directory to store cache files. Defaults to null, meaning - * using 'protected/runtime/cache' as the directory. + * @var string the directory to store cache files. You may use path alias here. */ - public $cachePath; + public $cachePath = '@application/runtime/cache'; /** * @var string cache file suffix. Defaults to '.bin'. */ - public $cacheFileSuffix='.bin'; - /** - * @var integer the level of sub-directories to store cache files. Defaults to 0, - * meaning no sub-directories. If the system has huge number of cache files (e.g. 10K+), - * you may want to set this value to be 1 or 2 so that the file system is not over burdened. - * The value of this property should not exceed 16 (less than 3 is recommended). - */ - public $directoryLevel=0; - - private $_gcProbability=100; - private $_gced=false; - - /** - * Initializes this application component. - * This method is required by the {@link IApplicationComponent} interface. - * It checks the availability of memcache. - * @throws CException if APC cache extension is not loaded or is disabled. - */ - public function init() - { - parent::init(); - if($this->cachePath===null) - $this->cachePath=\Yii::$application->getRuntimePath().DIRECTORY_SEPARATOR.'cache'; - if(!is_dir($this->cachePath)) - mkdir($this->cachePath,0777,true); - } - + public $cacheFileSuffix = '.bin'; /** - * @return integer the probability (parts per million) that garbage collection (GC) should be performed - * when storing a piece of data in the cache. Defaults to 100, meaning 0.01% chance. + * @var integer the level of sub-directories to store cache files. Defaults to 1. + * If the system has huge number of cache files (e.g. one million), you may use a bigger value + * (usually no bigger than 3). Using sub-directories is mainly to ensure the file system + * is not over burdened with a single directory having too many files. */ - public function getGCProbability() - { - return $this->_gcProbability; - } - + public $directoryLevel = 1; /** - * @param integer $value the probability (parts per million) that garbage collection (GC) should be performed + * @var integer the probability (parts per million) that garbage collection (GC) should be performed * when storing a piece of data in the cache. Defaults to 100, meaning 0.01% chance. * This number should be between 0 and 1000000. A value 0 meaning no GC will be performed at all. - */ - public function setGCProbability($value) - { - $value=(int)$value; - if($value<0) - $value=0; - if($value>1000000) - $value=1000000; - $this->_gcProbability=$value; - } + **/ + public $gcProbability = 100; /** - * Deletes all values from cache. - * This is the implementation of the method declared in the parent class. - * @return boolean whether the flush operation was successful. - * @since 1.1.5 + * Initializes this component by ensuring the existence of the cache path. */ - protected function flushValues() + public function init() { - $this->gc(false); - return true; + parent::init(); + $this->cachePath = \Yii::getAlias($this->cachePath); + if ($this->cachePath === false) { + throw new Exception('FileCache.cachePath must be a valid path alias.'); + } + if (!is_dir($this->cachePath)) { + mkdir($this->cachePath, 0777, true); + } } /** @@ -105,12 +70,12 @@ class CFileCache extends CCache */ protected function getValue($key) { - $cacheFile=$this->getCacheFile($key); - if(($time=@filemtime($cacheFile))>time()) + $cacheFile = $this->getCacheFile($key); + if (($time = @filemtime($cacheFile)) > time()) { return @file_get_contents($cacheFile); - else if($time>0) - @unlink($cacheFile); - return false; + } else { + return false; + } } /** @@ -122,28 +87,23 @@ class CFileCache extends CCache * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. * @return boolean true if the value is successfully stored into cache, false otherwise */ - protected function setValue($key,$value,$expire) + protected function setValue($key, $value, $expire) { - if(!$this->_gced && mt_rand(0,1000000)<$this->_gcProbability) - { - $this->gc(); - $this->_gced=true; + if ($expire <= 0) { + $expire = 31536000; // 1 year } + $expire += time(); - if($expire<=0) - $expire=31536000; // 1 year - $expire+=time(); - - $cacheFile=$this->getCacheFile($key); - if($this->directoryLevel>0) - @mkdir(dirname($cacheFile),0777,true); - if(@file_put_contents($cacheFile,$value,LOCK_EX)!==false) - { - @chmod($cacheFile,0777); - return @touch($cacheFile,$expire); + $cacheFile = $this->getCacheFile($key); + if ($this->directoryLevel > 0) { + @mkdir(dirname($cacheFile), 0777, true); } - else + if (@file_put_contents($cacheFile, $value, LOCK_EX) !== false) { + @chmod($cacheFile, 0777); + return @touch($cacheFile, $expire); + } else { return false; + } } /** @@ -155,12 +115,13 @@ class CFileCache extends CCache * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. * @return boolean true if the value is successfully stored into cache, false otherwise */ - protected function addValue($key,$value,$expire) + protected function addValue($key, $value, $expire) { - $cacheFile=$this->getCacheFile($key); - if(@filemtime($cacheFile)>time()) + $cacheFile = $this->getCacheFile($key); + if (@filemtime($cacheFile) > time()) { return false; - return $this->setValue($key,$value,$expire); + } + return $this->setValue($key, $value, $expire); } /** @@ -171,7 +132,7 @@ class CFileCache extends CCache */ protected function deleteValue($key) { - $cacheFile=$this->getCacheFile($key); + $cacheFile = $this->getCacheFile($key); return @unlink($cacheFile); } @@ -182,41 +143,69 @@ class CFileCache extends CCache */ protected function getCacheFile($key) { - if($this->directoryLevel>0) - { - $base=$this->cachePath; - for($i=0;$i<$this->directoryLevel;++$i) - { - if(($prefix=substr($key,$i+$i,2))!==false) - $base.=DIRECTORY_SEPARATOR.$prefix; + if ($this->directoryLevel > 0) { + $base = $this->cachePath; + for ($i = 0; $i < $this->directoryLevel; ++$i) { + if (($prefix = substr($key, $i + $i, 2)) !== false) { + $base .= DIRECTORY_SEPARATOR . $prefix; + } } - return $base.DIRECTORY_SEPARATOR.$key.$this->cacheFileSuffix; + return $base . DIRECTORY_SEPARATOR . $key . $this->cacheFileSuffix; + } else { + return $this->cachePath . DIRECTORY_SEPARATOR . $key . $this->cacheFileSuffix; } - else - return $this->cachePath.DIRECTORY_SEPARATOR.$key.$this->cacheFileSuffix; + } + + /** + * Deletes all values from cache. + * This is the implementation of the method declared in the parent class. + * @return boolean whether the flush operation was successful. + */ + protected function flushValues() + { + $this->gc(true, false); + return true; } /** * Removes expired cache files. - * @param boolean $expiredOnly whether to removed expired cache files only. If true, all cache files under {@link cachePath} will be removed. - * @param string $path the path to clean with. If null, it will be {@link cachePath}. + * @param boolean $force whether to enforce the garbage collection regardless of [[gcProbability]]. + * Defaults to false, meaning the actual deletion happens with the probability as specified by [[gcProbability]]. + * @param boolean $expiredOnly whether to removed expired cache files only. + * If true, all cache files under [[cachePath]] will be removed. + */ + public function gc($force = false, $expiredOnly = true) + { + if ($force || mt_rand(0, 1000000) < $this->gcProbability) { + $this->gcRecursive($this->cachePath, $expiredOnly); + } + } + + /** + * Recursively removing expired cache files under a directory. + * This method is mainly used by [[gc()]]. + * @param string $path the directory under which expired cache files are removed. + * @param boolean $expiredOnly whether to only remove expired cache files. If false, all files + * under `$path` will be removed. */ - public function gc($expiredOnly=true,$path=null) + protected function gcRecursive($path, $expiredOnly) { - if($path===null) - $path=$this->cachePath; - if(($handle=opendir($path))===false) - return; - while(($file=readdir($handle))!==false) - { - if($file[0]==='.') - continue; - $fullPath=$path.DIRECTORY_SEPARATOR.$file; - if(is_dir($fullPath)) - $this->gc($expiredOnly,$fullPath); - else if($expiredOnly && @filemtime($fullPath)gcRecursive($fullPath, $expiredOnly); + if (!$expiredOnly) { + @rmdir($fullPath); + } + } elseif (!$expiredOnly || $expiredOnly && @filemtime($fullPath) < time()) { + @unlink($fullPath); + } + } + closedir($handle); } - closedir($handle); } } diff --git a/framework/caching/MemCache.php b/framework/caching/MemCache.php index 706eb28..5366584 100644 --- a/framework/caching/MemCache.php +++ b/framework/caching/MemCache.php @@ -54,7 +54,7 @@ use yii\base\Exception; * In the above, two memcache servers are used: server1 and server2. You can configure more properties of * each server, such as `persistent`, `weight`, `timeout`. Please see [[MemCacheServer]] for available options. * - * @property mixed $memCache The memcache instance (or memcached if [[useMemcached]] is true) used by this component. + * @property \Memcache|\Memcached $memCache The memcache instance (or memcached if [[useMemcached]] is true) used by this component. * @property MemCacheServer[] $servers List of memcache server configurations. * * @author Qiang Xue diff --git a/framework/db/dao/Command.php b/framework/db/dao/Command.php index 769fde7..e025ca0 100644 --- a/framework/db/dao/Command.php +++ b/framework/db/dao/Command.php @@ -186,7 +186,9 @@ class Command extends \yii\base\Component * Note that the SQL data type of each value is determined by its PHP type. * @param array $values the values to be bound. This must be given in terms of an associative * array with array keys being the parameter names, and array values the corresponding parameter values, - * e.g. `array(':name'=>'John', ':age'=>25)`. + * e.g. `array(':name'=>'John', ':age'=>25)`. By default, the PDO type of each value is determined + * by its PHP type. You may explicitly specify the PDO type by using an array: `array(value, type)`, + * e.g. `array(':name'=>'John', ':profile'=>array($profile, \PDO::PARAM_LOB))`. * @return Command the current command being executed */ public function bindValues($values) @@ -194,7 +196,13 @@ class Command extends \yii\base\Component if (!empty($values)) { $this->prepare(); foreach ($values as $name => $value) { - $this->pdoStatement->bindValue($name, $value, $this->connection->getPdoType(gettype($value))); + if (is_array($value)) { + $type = $value[1]; + $value = $value[0]; + } else { + $type = $this->connection->getPdoType(gettype($value)); + } + $this->pdoStatement->bindValue($name, $value, $type); $this->_params[$name] = $value; } }