* @author Iván -DrSlump- Montes * @version 1.0 * @license BSD */ /** * @see Zend_Cache_Backend_Interface */ require_once 'Zend/Cache/Backend/Interface.php'; /** * @see Zend_Cache_Backend */ require_once 'Zend/Cache/Backend.php'; class Zend_Cache_Backend_Tags extends Zend_Cache_Backend implements Zend_Cache_Backend_Interface { /** * Available options * * =====> (Zend_Cache_Core) backend: * A Zend_Cache_Backend object or an associative array declaring its configuration: * 'class' => (string) The full class name of the backend (uses Zend_Loader to get it) * 'options' => (array) The array of options passed to the class constructor * * =====> (PDO|array) pdo: * A PDO object or an associative array with the DSN and DB connection data: * 'dsn' => (string) the database DSN in PDO format * 'user' => (string) the username for the connection * 'password' => (string) the password for the connection * 'options' => (array) additional configuration parameters to use with PDO::__construct() * 'lazy' => (boolean) if set to false will create the connection at instantation time * * =====> (book) track_untagged: * If true it will keep track of untagged items so they can be purged by the NOT_MATCHING_TAG mode * * =====> (book) duplicates_ok: * If true it won't make sure that there are no duplicate entries in the database. * * =====> (array) schema: * 'tablename' => (string) the name of the table to store the tag->key relation * 'tagcolumn' => (string) the name of the column with the tag * 'keycolumn' => (string) the name of the column with the key * * @var array available options */ protected $_options = array( 'backend' => null, 'pdo' => null, 'track_untagged'=> false, 'duplicates_ok' => false, 'schema' => array( 'tablename' => 'cachetags', 'tagcolumn' => 'cachetag', 'keycolumn' => 'cachekey', ), ); /** * Zend_Cache_Core object * * @var Zend_Cache_Core object */ private $_backend = null; /** * PDO object * * @var PDO object */ private $_pdo = null; /** * Constructor * * @param array $options associative array of options * @throws Zend_Cache_Exception * @return void */ public function __construct($options = array()) { parent::__construct($options); if ( is_array($options['backend']) ) { $backendClass = $options['backend']['class']; // To avoid security problems in this case, we use Zend_Loader to load the custom class require_once 'Zend/Loader.php'; Zend_Loader::loadClass($backendClass); $this->_backend = new $backendClass($options['backend']['options']); } else if ( $options['backend'] instanceof Zend_Cache_Backend_Interface ) { $this->_backend = $options['backend']; } else { Zend_Cache::throwException('The backend option is not correctly set!'); } if ( is_array($options['pdo']) ) { $pdoConf = array( 'lazy' => true, 'dsn' => '', 'user' => '', 'password' => '', 'options' => array(), ); $this->_pdoConf = array_merge( $pdoConf, $options['pdo'] ); if ( !$this->_pdoConf['lazy'] ) { $this->_connectDb(); $this->_createStatements(); } } else if ( $options['pdo'] instanceof PDO ) { $this->_pdo = $options['pdo']; $this->_createStatements(); } else { Zend_Cache::throwException('The pdo option is not correctly set!'); } } protected function _connectDb() { if ( !$this->_pdo ) { try { $this->_pdo = new PDO( $this->_pdoConf['dsn'], $this->_pdoConf['user'], $this->_pdoConf['password'], $this->_pdoConf['options'] ); //$this->_pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); } catch ( PDOException $e ) { Zend_Cache::throwException('The PDO connection could not be stablished: ' . $e->getMessage() ); } $this->_createStatements(); } return $this->_pdo; } protected function _createStatements() { $this->_stInsert = $this->_pdo->prepare( sprintf('INSERT INTO %s ( %s, %s ) VALUES ( ?, ? )', $this->_options['schema']['tablename'], $this->_options['schema']['tagcolumn'], $this->_options['schema']['keycolumn'] ) ); $this->_stDelete = $this->_pdo->prepare( sprintf('DELETE FROM %s WHERE %s = ?', $this->_options['schema']['tablename'], $this->_options['schema']['keycolumn'] ) ); } /** * Test if a cache is available for the given id and (if yes) return it (false else) * * @param string $id Cache id * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested * @return string|false cached datas */ public function load($id, $doNotTestCacheValidity = false) { $result = $this->_backend->load( $id, $doNotTestCacheValidity ); // If not in cache make sure to delete the tag mapping to handle cache expiration if ( $result === false && !$this->_options['duplicates_ok'] ) { $this->_connectDb(); $this->_stDelete->execute( array($id) ); } return $result; } /** * Test if a cache is available or not (for the given id) * * @param string $id Cache id * @return mixed|false (a cache is not available) or "last modified" timestamp (int) of the available cache record */ public function test($id) { return $this->_backend->test($id); } /** * Save some string datas into a cache record * * Note : $data is always "string" (serialization is done by the * core not by the backend) * * @param string $data Datas to cache * @param string $id Cache id * @param array $tags Array of strings, the cache record will be tagged by each string entry * @param int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime) * @return boolean True if no problem */ public function save($data, $id, $tags = array(), $specificLifetime = false) { $result = $this->_backend->save( $data, $id, array(), $specificLifetime ); if ( $result ) { // Makes sure to always store the key for notMatchingTag cleaning mode if ( $this->_options['track_untagged'] && !count($tags) ) { $tags = array(''); } if ( count($tags) ) { $this->_connectDb(); foreach ( $tags as $tag ) { $this->_stInsert->execute( array($tag, $id) ); } } } return $result; } /** * Remove a cache record * * @param string $id Cache id * @return boolean True if no problem */ public function remove($id) { if ( !$this->_options['duplicates_ok'] ) { $this->_connectDb(); $this->_stDelete->execute( array($id) ); } return $this->_backend->remove($id); } /** * Clean some cache records * * Available modes are : * 'all' (default) => remove all cache entries ($tags is not used) * 'old' => remove too old cache entries ($tags is not used) * 'matchingTag' => remove cache entries matching all given tags * ($tags can be an array of strings or a single string) * 'notMatchingTag' => remove cache entries not matching one of the given tags * ($tags can be an array of strings or a single string) * * @param string $mode Clean mode * @param array $tags Array of tags * @return boolean True if no problem */ public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) { if ( $mode == Zend_Cache::CLEANING_MODE_MATCHING_TAG || $mode == Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG ) { if ( !is_array($tags) ) { $tags = array($tags); } $this->_connectDb(); // Quote the tags and convert to a string $tags = array_map(array($this->_pdo, 'quote'), $tags); $tags = implode(', ', $tags); // Build the query for the desired matching mode if ( $mode == Zend_Cache::CLEANING_MODE_MATCHING_TAG ) { $q = 'SELECT %s FROM %s WHERE %s IN (%s)'; } else { $q = 'SELECT %s FROM %s WHERE %s NOT IN (%s)'; } // Interpolate the table schema data $schema = $this->_options['schema']; $q = sprintf($q, $schema['keycolumn'], $schema['tablename'], $schema['tagcolumn'], $tags); // Iterate over the matching keys and remove them $st = $this->_pdo->query($q); foreach ( $st->fetchAll(PDO::FETCH_COLUMN) as $key ) { $this->remove( $key ); } return true; } else { return $this->_backend->clean($mode, $tags); } } /** * Magic call handler to forward to the real backend the method calls * * @param string $method The method called * @param array $args The arguments used packed as an array * @return mixed */ public function __call( $method, $args ) { return call_user_method_array( $method, $this->_backend, $args ); } }