2f316d48a10cf988f8124f65e5d5d47b06d4de6b
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Cache / Backend / PdoBackend.php
1 <?php
2 namespace TYPO3\CMS\Core\Cache\Backend;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 /**
18 * A PDO database cache backend
19 * @api
20 */
21 class PdoBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend implements \TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface
22 {
23 /**
24 * @var string
25 */
26 protected $dataSourceName;
27
28 /**
29 * @var string
30 */
31 protected $username;
32
33 /**
34 * @var string
35 */
36 protected $password;
37
38 /**
39 * @var \PDO
40 */
41 protected $databaseHandle;
42
43 /**
44 * @var string
45 */
46 protected $pdoDriver;
47
48 /**
49 * Sets the DSN to use
50 *
51 * @param string $DSN The DSN to use for connecting to the DB
52 * @api
53 */
54 public function setDataSourceName($DSN)
55 {
56 $this->dataSourceName = $DSN;
57 }
58
59 /**
60 * Sets the username to use
61 *
62 * @param string $username The username to use for connecting to the DB
63 * @api
64 */
65 public function setUsername($username)
66 {
67 $this->username = $username;
68 }
69
70 /**
71 * Sets the password to use
72 *
73 * @param string $password The password to use for connecting to the DB
74 * @api
75 */
76 public function setPassword($password)
77 {
78 $this->password = $password;
79 }
80
81 /**
82 * Initialize the cache backend.
83 */
84 public function initializeObject()
85 {
86 $this->connect();
87 }
88
89 /**
90 * Saves data in the cache.
91 *
92 * @param string $entryIdentifier An identifier for this specific cache entry
93 * @param string $data The data to be stored
94 * @param array $tags Tags to associate with this cache entry
95 * @param int $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
96 * @throws \TYPO3\CMS\Core\Cache\Exception if no cache frontend has been set.
97 * @throws \InvalidArgumentException if the identifier is not valid
98 * @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException if $data is not a string
99 * @api
100 */
101 public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
102 {
103 if (!$this->cache instanceof \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface) {
104 throw new \TYPO3\CMS\Core\Cache\Exception('No cache frontend has been set yet via setCache().', 1259515600);
105 }
106 if (!is_string($data)) {
107 throw new \TYPO3\CMS\Core\Cache\Exception\InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1259515601);
108 }
109 $this->remove($entryIdentifier);
110 $lifetime = $lifetime === null ? $this->defaultLifetime : $lifetime;
111 $statementHandle = $this->databaseHandle->prepare('INSERT INTO "cache" ("identifier", "context", "cache", "created", "lifetime", "content") VALUES (?, ?, ?, ?, ?, ?)');
112 $result = $statementHandle->execute([$entryIdentifier, $this->context, $this->cacheIdentifier, $GLOBALS['EXEC_TIME'], $lifetime, $data]);
113 if ($result === false) {
114 throw new \TYPO3\CMS\Core\Cache\Exception('The cache entry "' . $entryIdentifier . '" could not be written.', 1259530791);
115 }
116 $statementHandle = $this->databaseHandle->prepare('INSERT INTO "tags" ("identifier", "context", "cache", "tag") VALUES (?, ?, ?, ?)');
117 foreach ($tags as $tag) {
118 $result = $statementHandle->execute([$entryIdentifier, $this->context, $this->cacheIdentifier, $tag]);
119 if ($result === false) {
120 throw new \TYPO3\CMS\Core\Cache\Exception('The tag "' . $tag . ' for cache entry "' . $entryIdentifier . '" could not be written.', 1259530751);
121 }
122 }
123 }
124
125 /**
126 * Loads data from the cache.
127 *
128 * @param string $entryIdentifier An identifier which describes the cache entry to load
129 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
130 * @api
131 */
132 public function get($entryIdentifier)
133 {
134 $statementHandle = $this->databaseHandle->prepare('SELECT "content" FROM "cache" WHERE "identifier"=? AND "context"=? AND "cache"=?' . $this->getNotExpiredStatement());
135 $statementHandle->execute([$entryIdentifier, $this->context, $this->cacheIdentifier]);
136 return $statementHandle->fetchColumn();
137 }
138
139 /**
140 * Checks if a cache entry with the specified identifier exists.
141 *
142 * @param string $entryIdentifier An identifier specifying the cache entry
143 * @return bool TRUE if such an entry exists, FALSE if not
144 * @api
145 */
146 public function has($entryIdentifier)
147 {
148 $statementHandle = $this->databaseHandle->prepare('SELECT COUNT("identifier") FROM "cache" WHERE "identifier"=? AND "context"=? AND "cache"=?' . $this->getNotExpiredStatement());
149 $statementHandle->execute([$entryIdentifier, $this->context, $this->cacheIdentifier]);
150 return $statementHandle->fetchColumn() > 0;
151 }
152
153 /**
154 * Removes all cache entries matching the specified identifier.
155 * Usually this only affects one entry but if - for what reason ever -
156 * old entries for the identifier still exist, they are removed as well.
157 *
158 * @param string $entryIdentifier Specifies the cache entry to remove
159 * @return bool TRUE if (at least) an entry could be removed or FALSE if no entry was found
160 * @api
161 */
162 public function remove($entryIdentifier)
163 {
164 $statementHandle = $this->databaseHandle->prepare('DELETE FROM "tags" WHERE "identifier"=? AND "context"=? AND "cache"=?');
165 $statementHandle->execute([$entryIdentifier, $this->context, $this->cacheIdentifier]);
166 $statementHandle = $this->databaseHandle->prepare('DELETE FROM "cache" WHERE "identifier"=? AND "context"=? AND "cache"=?');
167 $statementHandle->execute([$entryIdentifier, $this->context, $this->cacheIdentifier]);
168 return $statementHandle->rowCount() > 0;
169 }
170
171 /**
172 * Removes all cache entries of this cache.
173 *
174 * @api
175 */
176 public function flush()
177 {
178 $statementHandle = $this->databaseHandle->prepare('DELETE FROM "tags" WHERE "context"=? AND "cache"=?');
179 $statementHandle->execute([$this->context, $this->cacheIdentifier]);
180 $statementHandle = $this->databaseHandle->prepare('DELETE FROM "cache" WHERE "context"=? AND "cache"=?');
181 $statementHandle->execute([$this->context, $this->cacheIdentifier]);
182 }
183
184 /**
185 * Removes all cache entries of this cache which are tagged by the specified tag.
186 *
187 * @param string $tag The tag the entries must have
188 * @api
189 */
190 public function flushByTag($tag)
191 {
192 $statementHandle = $this->databaseHandle->prepare('DELETE FROM "cache" WHERE "context"=? AND "cache"=? AND "identifier" IN (SELECT "identifier" FROM "tags" WHERE "context"=? AND "cache"=? AND "tag"=?)');
193 $statementHandle->execute([$this->context, $this->cacheIdentifier, $this->context, $this->cacheIdentifier, $tag]);
194 $statementHandle = $this->databaseHandle->prepare('DELETE FROM "tags" WHERE "context"=? AND "cache"=? AND "tag"=?');
195 $statementHandle->execute([$this->context, $this->cacheIdentifier, $tag]);
196 }
197
198 /**
199 * Finds and returns all cache entry identifiers which are tagged by the
200 * specified tag.
201 *
202 * @param string $tag The tag to search for
203 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
204 * @api
205 */
206 public function findIdentifiersByTag($tag)
207 {
208 $statementHandle = $this->databaseHandle->prepare('SELECT "identifier" FROM "tags" WHERE "context"=? AND "cache"=? AND "tag"=?');
209 $statementHandle->execute([$this->context, $this->cacheIdentifier, $tag]);
210 return $statementHandle->fetchAll(\PDO::FETCH_COLUMN);
211 }
212
213 /**
214 * Does garbage collection
215 *
216 * @api
217 */
218 public function collectGarbage()
219 {
220 $statementHandle = $this->databaseHandle->prepare('DELETE FROM "tags" WHERE "context"=? AND "cache"=? AND "identifier" IN ' . '(SELECT "identifier" FROM "cache" WHERE "context"=? AND "cache"=? AND "lifetime" > 0 AND "created" + "lifetime" < ' . $GLOBALS['EXEC_TIME'] . ')');
221 $statementHandle->execute([$this->context, $this->cacheIdentifier, $this->context, $this->cacheIdentifier]);
222 $statementHandle = $this->databaseHandle->prepare('DELETE FROM "cache" WHERE "context"=? AND "cache"=? AND "lifetime" > 0 AND "created" + "lifetime" < ' . $GLOBALS['EXEC_TIME']);
223 $statementHandle->execute([$this->context, $this->cacheIdentifier]);
224 }
225
226 /**
227 * Returns an SQL statement that evaluates to TRUE if the entry is not expired.
228 *
229 * @return string
230 */
231 protected function getNotExpiredStatement()
232 {
233 return ' AND ("lifetime" = 0 OR "created" + "lifetime" >= ' . $GLOBALS['EXEC_TIME'] . ')';
234 }
235
236 /**
237 * Connect to the database
238 *
239 * @throws \RuntimeException if something goes wrong
240 */
241 protected function connect()
242 {
243 try {
244 $splitdsn = explode(':', $this->dataSourceName, 2);
245 $this->pdoDriver = $splitdsn[0];
246 if ($this->pdoDriver === 'sqlite' && !file_exists($splitdsn[1])) {
247 $this->databaseHandle = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\PDO::class, $this->dataSourceName, $this->username, $this->password);
248 $this->createCacheTables();
249 } else {
250 $this->databaseHandle = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\PDO::class, $this->dataSourceName, $this->username, $this->password);
251 }
252 $this->databaseHandle->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
253 if (substr($this->pdoDriver, 0, 5) === 'mysql') {
254 $this->databaseHandle->exec('SET SESSION sql_mode=\'ANSI\';');
255 }
256 } catch (\PDOException $e) {
257 throw new \RuntimeException('Could not connect to cache table with DSN "' . $this->dataSourceName . '". PDO error: ' . $e->getMessage(), 1334736164);
258 }
259 }
260
261 /**
262 * Creates the tables needed for the cache backend.
263 *
264 * @throws \RuntimeException if something goes wrong
265 */
266 protected function createCacheTables()
267 {
268 try {
269 \TYPO3\CMS\Core\Database\PdoHelper::importSql(
270 $this->databaseHandle,
271 $this->pdoDriver,
272 \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('core') .
273 'Resources/Private/Sql/Cache/Backend/PdoBackendCacheAndTags.sql'
274 );
275 } catch (\PDOException $e) {
276 throw new \RuntimeException('Could not create cache tables with DSN "' . $this->dataSourceName . '". PDO error: ' . $e->getMessage(), 1259576985);
277 }
278 }
279 }