1: <?php
2:
3: /**
4: * PDO Cache Handler
5: * Allows you to store Smarty Cache files into your db.
6: * Example table :
7: * CREATE TABLE `smarty_cache` (
8: * `id` char(40) NOT NULL COMMENT 'sha1 hash',
9: * `name` varchar(250) NOT NULL,
10: * `cache_id` varchar(250) DEFAULT NULL,
11: * `compile_id` varchar(250) DEFAULT NULL,
12: * `modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
13: * `expire` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
14: * `content` mediumblob NOT NULL,
15: * PRIMARY KEY (`id`),
16: * KEY `name` (`name`),
17: * KEY `cache_id` (`cache_id`),
18: * KEY `compile_id` (`compile_id`),
19: * KEY `modified` (`modified`),
20: * KEY `expire` (`expire`)
21: * ) ENGINE=InnoDB
22: * Example usage :
23: * $cnx = new PDO("mysql:host=localhost;dbname=mydb", "username", "password");
24: * $smarty->setCachingType('pdo');
25: * $smarty->loadPlugin('Smarty_CacheResource_Pdo');
26: * $smarty->registerCacheResource('pdo', new Smarty_CacheResource_Pdo($cnx, 'smarty_cache'));
27: *
28: * @author Beno!t POLASZEK - 2014
29: */
30: class Smarty_CacheResource_Pdo extends Smarty_CacheResource_Custom
31: {
32: /**
33: * @var string[]
34: */
35: protected $fetchStatements = array('default' => 'SELECT %2$s
36: FROM %1$s
37: WHERE 1
38: AND id = :id
39: AND cache_id IS NULL
40: AND compile_id IS NULL',
41: 'withCacheId' => 'SELECT %2$s
42: FROM %1$s
43: WHERE 1
44: AND id = :id
45: AND cache_id = :cache_id
46: AND compile_id IS NULL',
47: 'withCompileId' => 'SELECT %2$s
48: FROM %1$s
49: WHERE 1
50: AND id = :id
51: AND compile_id = :compile_id
52: AND cache_id IS NULL',
53: 'withCacheIdAndCompileId' => 'SELECT %2$s
54: FROM %1$s
55: WHERE 1
56: AND id = :id
57: AND cache_id = :cache_id
58: AND compile_id = :compile_id');
59:
60: /**
61: * @var string
62: */
63: protected $insertStatement = 'INSERT INTO %s
64:
65: SET id = :id,
66: name = :name,
67: cache_id = :cache_id,
68: compile_id = :compile_id,
69: modified = CURRENT_TIMESTAMP,
70: expire = DATE_ADD(CURRENT_TIMESTAMP, INTERVAL :expire SECOND),
71: content = :content
72:
73: ON DUPLICATE KEY UPDATE
74: name = :name,
75: cache_id = :cache_id,
76: compile_id = :compile_id,
77: modified = CURRENT_TIMESTAMP,
78: expire = DATE_ADD(CURRENT_TIMESTAMP, INTERVAL :expire SECOND),
79: content = :content';
80:
81: /**
82: * @var string
83: */
84: protected $deleteStatement = 'DELETE FROM %1$s WHERE %2$s';
85:
86: /**
87: * @var string
88: */
89: protected $truncateStatement = 'TRUNCATE TABLE %s';
90:
91: /**
92: * @var string
93: */
94: protected $fetchColumns = 'modified, content';
95:
96: /**
97: * @var string
98: */
99: protected $fetchTimestampColumns = 'modified';
100:
101: /**
102: * @var \PDO
103: */
104: protected $pdo;
105:
106: /**
107: * @var
108: */
109: protected $table;
110:
111: /**
112: * @var null
113: */
114: protected $database;
115:
116: /**
117: * Constructor
118: *
119: * @param PDO $pdo PDO : active connection
120: * @param string $table : table (or view) name
121: * @param string $database : optional - if table is located in another db
122: *
123: * @throws \SmartyException
124: */
125: public function __construct(PDO $pdo, $table, $database = null)
126: {
127: if (is_null($table)) {
128: throw new SmartyException("Table name for caching can't be null");
129: }
130: $this->pdo = $pdo;
131: $this->table = $table;
132: $this->database = $database;
133: $this->fillStatementsWithTableName();
134: }
135:
136: /**
137: * Fills the table name into the statements.
138: *
139: * @return $this Current Instance
140: * @access protected
141: */
142: protected function fillStatementsWithTableName()
143: {
144: foreach ($this->fetchStatements as &$statement) {
145: $statement = sprintf($statement, $this->getTableName(), '%s');
146: }
147: $this->insertStatement = sprintf($this->insertStatement, $this->getTableName());
148: $this->deleteStatement = sprintf($this->deleteStatement, $this->getTableName(), '%s');
149: $this->truncateStatement = sprintf($this->truncateStatement, $this->getTableName());
150: return $this;
151: }
152:
153: /**
154: * Gets the fetch statement, depending on what you specify
155: *
156: * @param string $columns : the column(s) name(s) you want to retrieve from the database
157: * @param string $id unique cache content identifier
158: * @param string|null $cache_id cache id
159: * @param string|null $compile_id compile id
160: *
161: * @access protected
162: * @return \PDOStatement
163: */
164: protected function getFetchStatement($columns, $id, $cache_id = null, $compile_id = null)
165: {
166: $args = array();
167: if (!is_null($cache_id) && !is_null($compile_id)) {
168: $query = $this->fetchStatements[ 'withCacheIdAndCompileId' ] and
169: $args = array('id' => $id, 'cache_id' => $cache_id, 'compile_id' => $compile_id);
170: } elseif (is_null($cache_id) && !is_null($compile_id)) {
171: $query = $this->fetchStatements[ 'withCompileId' ] and
172: $args = array('id' => $id, 'compile_id' => $compile_id);
173: } elseif (!is_null($cache_id) && is_null($compile_id)) {
174: $query = $this->fetchStatements[ 'withCacheId' ] and $args = array('id' => $id, 'cache_id' => $cache_id);
175: } else {
176: $query = $this->fetchStatements[ 'default' ] and $args = array('id' => $id);
177: }
178: $query = sprintf($query, $columns);
179: $stmt = $this->pdo->prepare($query);
180: foreach ($args as $key => $value) {
181: $stmt->bindValue($key, $value);
182: }
183: return $stmt;
184: }
185:
186: /**
187: * fetch cached content and its modification time from data source
188: *
189: * @param string $id unique cache content identifier
190: * @param string $name template name
191: * @param string|null $cache_id cache id
192: * @param string|null $compile_id compile id
193: * @param string $content cached content
194: * @param integer $mtime cache modification timestamp (epoch)
195: *
196: * @return void
197: * @access protected
198: */
199: protected function fetch($id, $name, $cache_id = null, $compile_id = null, &$content, &$mtime)
200: {
201: $stmt = $this->getFetchStatement($this->fetchColumns, $id, $cache_id, $compile_id);
202: $stmt->execute();
203: $row = $stmt->fetch();
204: $stmt->closeCursor();
205: if ($row) {
206: $content = $this->outputContent($row[ 'content' ]);
207: $mtime = strtotime($row[ 'modified' ]);
208: } else {
209: $content = null;
210: $mtime = null;
211: }
212: }
213:
214: /**
215: * Fetch cached content's modification timestamp from data source
216: * {@internal implementing this method is optional.
217: * Only implement it if modification times can be accessed faster than loading the complete cached content.}}
218: *
219: * @param string $id unique cache content identifier
220: * @param string $name template name
221: * @param string|null $cache_id cache id
222: * @param string|null $compile_id compile id
223: *
224: * @return integer|boolean timestamp (epoch) the template was modified, or false if not found
225: * @access protected
226: */
227: // protected function fetchTimestamp($id, $name, $cache_id = null, $compile_id = null) {
228: // $stmt = $this->getFetchStatement($this->fetchTimestampColumns, $id, $cache_id, $compile_id);
229: // $stmt -> execute();
230: // $mtime = strtotime($stmt->fetchColumn());
231: // $stmt -> closeCursor();
232: // return $mtime;
233: // }
234: /**
235: * Save content to cache
236: *
237: * @param string $id unique cache content identifier
238: * @param string $name template name
239: * @param string|null $cache_id cache id
240: * @param string|null $compile_id compile id
241: * @param integer|null $exp_time seconds till expiration time in seconds or null
242: * @param string $content content to cache
243: *
244: * @return boolean success
245: * @access protected
246: */
247: protected function save($id, $name, $cache_id = null, $compile_id = null, $exp_time, $content)
248: {
249: $stmt = $this->pdo->prepare($this->insertStatement);
250: $stmt->bindValue('id', $id);
251: $stmt->bindValue('name', $name);
252: $stmt->bindValue('cache_id', $cache_id, (is_null($cache_id)) ? PDO::PARAM_NULL : PDO::PARAM_STR);
253: $stmt->bindValue('compile_id', $compile_id, (is_null($compile_id)) ? PDO::PARAM_NULL : PDO::PARAM_STR);
254: $stmt->bindValue('expire', (int)$exp_time, PDO::PARAM_INT);
255: $stmt->bindValue('content', $this->inputContent($content));
256: $stmt->execute();
257: return !!$stmt->rowCount();
258: }
259:
260: /**
261: * Encodes the content before saving to database
262: *
263: * @param string $content
264: *
265: * @return string $content
266: * @access protected
267: */
268: protected function inputContent($content)
269: {
270: return $content;
271: }
272:
273: /**
274: * Decodes the content before saving to database
275: *
276: * @param string $content
277: *
278: * @return string $content
279: * @access protected
280: */
281: protected function outputContent($content)
282: {
283: return $content;
284: }
285:
286: /**
287: * Delete content from cache
288: *
289: * @param string|null $name template name
290: * @param string|null $cache_id cache id
291: * @param string|null $compile_id compile id
292: * @param integer|null|-1 $exp_time seconds till expiration or null
293: *
294: * @return integer number of deleted caches
295: * @access protected
296: */
297: protected function delete($name = null, $cache_id = null, $compile_id = null, $exp_time = null)
298: {
299: // delete the whole cache
300: if ($name === null && $cache_id === null && $compile_id === null && $exp_time === null) {
301: // returning the number of deleted caches would require a second query to count them
302: $this->pdo->query($this->truncateStatement);
303: return -1;
304: }
305: // build the filter
306: $where = array();
307: // equal test name
308: if ($name !== null) {
309: $where[] = 'name = ' . $this->pdo->quote($name);
310: }
311: // equal test cache_id and match sub-groups
312: if ($cache_id !== null) {
313: $where[] =
314: '(cache_id = ' .
315: $this->pdo->quote($cache_id) .
316: ' OR cache_id LIKE ' .
317: $this->pdo->quote($cache_id . '|%') .
318: ')';
319: }
320: // equal test compile_id
321: if ($compile_id !== null) {
322: $where[] = 'compile_id = ' . $this->pdo->quote($compile_id);
323: }
324: // for clearing expired caches
325: if ($exp_time === Smarty::CLEAR_EXPIRED) {
326: $where[] = 'expire < CURRENT_TIMESTAMP';
327: } // range test expiration time
328: elseif ($exp_time !== null) {
329: $where[] = 'modified < DATE_SUB(NOW(), INTERVAL ' . intval($exp_time) . ' SECOND)';
330: }
331: // run delete query
332: $query = $this->pdo->query(sprintf($this->deleteStatement, join(' AND ', $where)));
333: return $query->rowCount();
334: }
335:
336: /**
337: * Gets the formatted table name
338: *
339: * @return string
340: * @access protected
341: */
342: protected function getTableName()
343: {
344: return (is_null($this->database)) ? "`{$this->table}`" : "`{$this->database}`.`{$this->table}`";
345: }
346: }
347: