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