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