8000 [Cache] Add PDO adapter + tag aware adapter · symfony/symfony@bb2dba9 · GitHub
[go: up one dir, main page]

Skip to content

Commit bb2dba9

Browse files
[Cache] Add PDO adapter + tag aware adapter
1 parent e408b50 commit bb2dba9

File tree

7 files changed

+677
-0
lines changed

7 files changed

+677
-0
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Cache\Adapter;
13+
14+
class PdoAdapter extends AbstractAdapter
15+
{
16+
use PdoAdapterTrait;
17+
18+
/**
19+
* Constructor.
20+
*
21+
* You can either pass an existing database connection as PDO instance or
22+
* pass a DSN string that will be used to lazy-connect to the database
23+
* when the cache is actually used.
24+
*
25+
* List of available options:
26+
* * db_table: The name of the table [default: cache_items]
27+
* * db_id_col: The column where to store the cache id [default: item_id]
28+
* * db_data_col: The column where to store the cache data [default: item_data]
29+
* * db_expiry_col: The column where to store the expiration timestamp [default: item_expiry]
30+
* * db_username: The username when lazy-connect [default: '']
31+
* * db_password: The password when lazy-connect [default: '']
32+
* * db_connection_options: An array of driver-specific connection options [default: array()]
33+
*
34+
* @param \PDO|string $pdoOrDsn A \PDO instance or DSN string or null
35+
* @param string $namespace
36+
* @param int $defaultLifetime
37+
* @param array $options An associative array of options
38+
*
39+
* @throws InvalidArgumentException When first argument is not PDO nor string
40+
* @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
41+
* @throws InvalidArgumentException When namespace contains invalid characters
42+
*/
43+
public function __construct($pdoOrDsn, $namespace = '', $defaultLifetime = 0, array $options = array())
44+
{
45+
$this->init($pdoOrDsn, $namespace, $defaultLifetime, $options);
46+
47+
parent::__construct($namespace, $defaultLifetime);
48+
}
49+
}
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Cache\Adapter;
13+
14+
use Symfony\Component\Cache\Exception\InvalidArgumentException;
15+
16+
/**
17+
* @internal
18+
*
19+
* @author Nicolas Grekas <p@tchwork.com>
20+
*/
21+
trait PdoAdapterTrait
22+
{
23+
/**
24+
* @var \PDO|null PDO instance or null when not connected yet
25+
*/
26+
private $pdo;
27+
28+
/**
29+
* @var string|null DSN string or null when lazy connection disabled
30+
*/
31+
private $dsn;
32+
33+
/**
34+
* @var string Database driver
35+
*/
36+
private $driver;
37+
38+
/**
39+
* @var string Table name
40+
*/
41+
private $table = 'cache_items';
42+
43+
/**
44+
* @var string Column for item id
45+
*/
46+
private $idCol = 'item_id';
47+
48+
/**
49+
* @var string Column for item data
50+
*/
51+
private $dataCol = 'item_data';
52+
53+
/**
54+
* @var string Column for expiration timestamp
55+
*/
56+
private $expiryCol = 'item_expiry';
57+
58+
/**
59+
* @var string Username when lazy-connect
60+
*/
61+
private $username = '';
62+
63+
/**
64+
* @var string Password when lazy-connect
65+
*/
66+
private $password = '';
67+
68+
/**
69+
* @var array Connection options when lazy-connect
70+
*/
71+
private $connectionOptions = array();
72+
73+
private function init($pdoOrDsn, $namespace, $defaultLifetime, array $options)
74+
{
75+
if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) {
76+
throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0]));
77+
}
78+
79+
if ($pdoOrDsn instanceof \PDO) {
80+
if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
81+
throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __CLASS__));
82+
}
83+
84+
$this->pdo = $pdoOrDsn;
85+
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
86+
} elseif (is_string($pdoOrDsn)) {
87+
$this->dsn = $pdoOrDsn;
88+
} else {
89+
throw new InvalidArgumentException(sprintf('"%s" requires PDO instance or DSN string as first argument, "%s" given.', __CLASS__, is_object($pdoOrDsn) ? get_class($pdoOrDsn) : gettype($pdoOrDsn)));
90+
}
91+
92+
$this->table = isset($options['db_table']) ? $options['db_table'] : $this->table;
93+
$this->idCol = isset($options['db_id_col']) ? $options['db_id_col'] : $this->idCol;
94+
$this->dataCol = isset($options['db_data_col']) ? $options['db_data_col'] : $this->dataCol;
95+
$this->expiryCol = isset($options['db_expiry_col']) ? $options['db_expiry_col'] : $this->expiryCol;
96+
$this->username = isset($options['db_username']) ? $options['db_username'] : $this->username;
97+
$this->password = isset($options['db_password']) ? $options['db_password'] : $this->password;
98+
$this->connectionOptions = isset($options['db_connection_options']) ? $options['db_connection_options'] : $this->connectionOptions;
99+
}
100+
101+
/**
102+
* Creates the table to store cache items which can be called once for setup.
103+
*
104+
* Cache ID are saved in a column of maximum length 256. Cache data is
105+
* saved in a BLOB.
106+
*
107+
* @throws \PDOException When the table already exists
108+
* @throws \DomainException When an unsupported PDO driver is used
109+
*/
110+
public function createTable()
111+
{
112+
// connect if we are not yet
113+
$this->getConnection();
114+
115+
switch ($this->driver) {
116+
case 'mysql':
117+
// We use varbinary for the ID column because it prevents unwanted conversions:
118+
// - character set conversions between server and client
119+
// - trailing space removal
120+
// - case-insensitivity
121+
// - language processing like é == e
122+
$sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(256) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->expiryCol INTEGER UNSIGNED) COLLATE utf8_bin, ENGINE = InnoDB";
123+
break;
124+
case 'sqlite':
125+
$sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->expiryCol INTEGER)";
126+
break;
127+
case 'pgsql':
128+
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(256) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->expiryCol INTEGER)";
129+
break;
130+
case 'oci':
131+
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(256) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->expiryCol INTEGER)";
132+
break;
133+
case 'sqlsrv':
134+
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(256) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->expiryCol INTEGER)";
135+
break;
136+
default:
137+
throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $this->driver));
138+
}
139+
140+
$this->pdo->exec($sql);
141+
}
142+
143+
public function __destruct()
144+
{
145+
parent::__destruct();
146+
147+
if (null !== $this->dsn) {
148+
// only close lazy-connection
149+
$this->pdo = null;
150+
}
151+
}
152+
153+
/**
154+
* {@inheritdoc}
155+
*/
156+
protected function doFetch(array $ids)
157+
{
158+
$now = time();
159+
$values = array();
160+
$expired = array();
161+
162+
$sql = str_pad('', (count($ids) << 1) - 1, '?,');
163+
$sql = "SELECT $this->idCol, CASE WHEN $this->expiryCol IS NULL OR $this->expiryCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN ($sql)";
164+
$stmt = $this->getConnection()->prepare($sql);
165+
$stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
166+
foreach ($ids as $id) {
167+
$stmt->bindValue(++$i, $id);
168+
}
169+
$stmt->execute();
170+
171+
while ($row = $stmt->fetch(\PDO::FETCH_NUM)) {
172+
if (null === $row[1]) {
173+
$expired[] = $row[0];
174+
} else {
175+
$values[$row[0]] = $row[1];
176+
}
177+
}
178+
179+
if ($expired) {
180+
$sql = str_pad('', (count($expired) << 1) - 1, '?,');
181+
$sql = "DELETE FROM $this->table WHERE $this->expiryCol <= ? AND $this->idCol IN ($sql)";
182+
$stmt = $this->getConnection()->prepare($sql);
183+
$stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
184+
foreach ($expired as $id) {
185+
$stmt->bindValue(++$i, $id);
186+
}
187+
$stmt->execute($expired);
188+
}
189+
190+
return array_map('unserialize', $values);
191+
}
192+
193+
/**
194+
* {@inheritdoc}
195+
*/
196+
protected function doHave($id)
197+
{
198+
$sql = "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND ($this->expiryCol IS NULL OR $this->expiryCol > :time)";
199+
$stmt = $this->getConnection()->prepare($sql);
200+
201+
$stmt->bindValue(':id', $id);
202+
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
203+
$stmt->execute();
204+
205+
return (bool) $stmt->fetchColumn();
206+
}
207+
208+
/**
209+
* {@inheritdoc}
210+
*/
211+
protected function doClear($namespace)
212+
{
213+
if ('' === $namespace) {
214+
if ('sqlite' === $this->driver) {
215+
$sql = "DELETE FROM $this->table";
216+
} else {
217+
$sql = "TRUNCATE TABLE $this->table";
218+
}
219+
} else {
220+
$sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'";
221+
}
222+
223+
$this->getConnection()->exec($sql);
224+
225+
return true;
226+
}
227+
228+
/**
229+
* {@inheritdoc}
230+
*/
231+
protected function doDelete(array $ids)
232+
{
233+
$sql = str_pad('', (count($ids) << 1) - 1, '?,');
234+
$sql = "DELETE FROM $this->table WHERE $this->idCol IN ($sql)";
235+
$stmt = $this->getConnection()->prepare($sql);
236+
$stmt->execute(array_values($ids));
237+
238+
return true;
239+
}
240+
241+
/**
242+
* {@inheritdoc}
243+
*/
244+
protected function doSave(array $values, $lifetime)
245+
{
246+
$serialized = array();
247+
$failed = array();
248+
249+
foreach ($values as $id => $value) {
250+
try {
251+
$serialized[$id] = serialize($value);
252+
} catch (\Exception $e) {
253+
$failed[] = $id;
254+
}
255+
}
256+
257+
if (!$serialized) {
258+
return $failed;
259+
}
260+
261+
$sql = null;
262+
switch (true) {
263+
case 'mysql' === $this->driver:
264+
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->expiryCol) VALUES (:id, :data, :expiry) ".
265+
"ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->expiry = VALUES($this->expiryCol)";
266+
break;
267+
case 'oci' === $this->driver:
268+
// DUAL is Oracle specific dummy table
269+
$sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ".
270+
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->expiryCol) VALUES (?, ?, ?) ".
271+
"WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->expiryCol = ?";
272+
break;
273+
case 'sqlsrv' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='):
274+
// MERGE is only available since SQL Server 2008 and must be terminated by semicolon
275+
// It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
276+
$sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
277+
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->expiryCol) VALUES (?, ?, ?) ".
278+
"WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->expiryCol = ?;";
279+
break;
280+
case 'sqlite' === $this->driver:
281+
$sql = "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->expiryCol) VALUES (:id, :data, :expiry)";
282+
break;
283+
case 'pgsql' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '9.5', '>='):
284+
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->expiryCol) VALUES (:id, :data, :expiry) ".
285+
"ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->expiryCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->expiryCol)";
286+
break;
287+
}
288+
289+
$expiry = $lifetime ? time() + $lifetime : null;
290+
$stmt = $this->getConnection()->prepare($sql);
291+
292+
if ('sqlsrv' === $this->driver || 'oci' === $this->driver) {
293+
$stmt->bindParam(1, $id);
294+
$stmt->bindParam(2, $id);
295+
$stmt->bindParam(3, $data, \PDO::PARAM_LOB);
296+
$stmt->bindValue(4, $expiry, \PDO::PARAM_INT);
297+
$stmt->bindParam(5, $data, \PDO::PARAM_LOB);
298+
$stmt->bindValue(6, $expiry, \PDO::PARAM_INT);
299+
} else {
300+
$stmt->bindParam(':id', $id, \PDO::PARAM_STR);
301+
$stmt->bindParam(':data', $data, \PDO::PARAM_LOB);
302+
$stmt->bindValue(':expiry', $expiry, \PDO::PARAM_INT);
303+
}
304+
305+
foreach ($serialized as $id => $data) {
306+
$stmt->execute();
307+
}
308+
309+
return $failed;
310+
}
311+
312+
/**
313+
* @return \PDO
314+
*/
315+
private function getConnection()
316+
{
317+
if (null === $this->pdo) {
318+
$this->pdo = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions);
319+
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
320+
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
321+
}
322+
323+
return $this->pdo;
324+
}
325+
}

0 commit comments

Comments
 (0)
0