1
|
|
|
<?php |
2
|
|
|
namespace Agavi\Storage; |
3
|
|
|
|
4
|
|
|
// +---------------------------------------------------------------------------+ |
5
|
|
|
// | This file is part of the Agavi package. | |
6
|
|
|
// | Copyright (c) 2005-2011 the Agavi Project. | |
7
|
|
|
// | | |
8
|
|
|
// | For the full copyright and license information, please view the LICENSE | |
9
|
|
|
// | file that was distributed with this source code. You can also view the | |
10
|
|
|
// | LICENSE file online at http://www.agavi.org/LICENSE.txt | |
11
|
|
|
// | vi: set noexpandtab: | |
12
|
|
|
// | Local Variables: | |
13
|
|
|
// | indent-tabs-mode: t | |
14
|
|
|
// | End: | |
15
|
|
|
// +---------------------------------------------------------------------------+ |
16
|
|
|
use Agavi\Core\Context; |
17
|
|
|
use Agavi\Exception\DatabaseException; |
18
|
|
|
use Agavi\Exception\InitializationException; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* Provides support for session storage using a PDO database abstraction |
22
|
|
|
* layer. |
23
|
|
|
* |
24
|
|
|
* <b>Required parameters:</b> |
25
|
|
|
* |
26
|
|
|
* # <b>db_table</b> - [none] - The database table in which session data will be |
27
|
|
|
* stored. |
28
|
|
|
* |
29
|
|
|
* <b>Optional parameters:</b> |
30
|
|
|
* |
31
|
|
|
* # <b>database</b> - [default] - The database connection to use |
32
|
|
|
* (see databases.xml). |
33
|
|
|
* # <b>db_id_col</b> - [sess_id] - The database column in which the |
34
|
|
|
* session id will be stored. |
35
|
|
|
* # <b>db_data_col</b> - [sess_data] - The database column in which the |
36
|
|
|
* session data will be stored. |
37
|
|
|
* # <b>db_time_col</b> - [sess_time] - The database column in which the |
38
|
|
|
* session timestamp will be stored. |
39
|
|
|
* # <b>data_as_lob</b> - [true] - If true, data is stored as a LOB |
40
|
|
|
* other wise as a string. |
41
|
|
|
* (Note: with Oracle LOBs are always |
42
|
|
|
* used) |
43
|
|
|
* # <b>date_format</b> - [U] - The format string passed to date() to |
44
|
|
|
* format timestamps. Defaults to "U", |
45
|
|
|
* which means a Unix Timestamp again. |
46
|
|
|
* |
47
|
|
|
* @package agavi |
48
|
|
|
* @subpackage storage |
49
|
|
|
* |
50
|
|
|
* @author Sean Kerr <[email protected]> |
51
|
|
|
* @author Veikko Mäkinen <[email protected]> |
52
|
|
|
* @author Dominik del Bondio <[email protected]> |
53
|
|
|
* @author David Zülke <[email protected]> |
54
|
|
|
* @copyright Authors |
55
|
|
|
* @copyright The Agavi Project |
56
|
|
|
* |
57
|
|
|
* @since 0.11.0 |
58
|
|
|
* |
59
|
|
|
* @version $Id$ |
60
|
|
|
*/ |
61
|
|
|
class PdoSessionStorage extends SessionStorage |
62
|
|
|
{ |
63
|
|
|
/** |
64
|
|
|
* @var \PDO A Database Connection. |
65
|
|
|
*/ |
66
|
|
|
protected $connection; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Initialize this Storage. |
70
|
|
|
* |
71
|
|
|
* @param Context $context An Context instance. |
72
|
|
|
* @param array $parameters An associative array of initialization parameters. |
73
|
|
|
* |
74
|
|
|
* @throws InitializationException If an error occurs while |
75
|
|
|
* initializing this Storage. |
76
|
|
|
* |
77
|
|
|
* @author Sean Kerr <[email protected]> |
78
|
|
|
* @author Veikko Mäkinen <[email protected]> |
79
|
|
|
* @author Dominik del Bondio <[email protected]> |
80
|
|
|
* @since 0.10.0 |
81
|
|
|
*/ |
82
|
|
View Code Duplication |
public function initialize(Context $context, array $parameters = array()) |
|
|
|
|
83
|
|
|
{ |
84
|
|
|
// initialize the parent |
85
|
|
|
parent::initialize($context, $parameters); |
86
|
|
|
|
87
|
|
|
if (!$this->hasParameter('db_table')) { |
88
|
|
|
// missing required 'db_table' parameter |
89
|
|
|
$error = 'Factory configuration file is missing required "db_table" parameter for the Storage category'; |
90
|
|
|
throw new InitializationException($error); |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
// use this object as the session handler |
94
|
|
|
session_set_save_handler( |
95
|
|
|
array($this, 'sessionOpen'), |
96
|
|
|
array($this, 'sessionClose'), |
97
|
|
|
array($this, 'sessionRead'), |
98
|
|
|
array($this, 'sessionWrite'), |
99
|
|
|
array($this, 'sessionDestroy'), |
100
|
|
|
array($this, 'sessionGC') |
101
|
|
|
); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Close a session. |
106
|
|
|
* |
107
|
|
|
* @return bool true, if the session was closed, otherwise false. |
108
|
|
|
* |
109
|
|
|
* @author Sean Kerr <[email protected]> |
110
|
|
|
* @author Dominik del Bondio <[email protected]> |
111
|
|
|
* @since 0.11.0 |
112
|
|
|
*/ |
113
|
|
|
public function sessionClose() |
114
|
|
|
{ |
115
|
|
|
if ($this->connection) { |
116
|
|
|
return true; |
117
|
|
|
} else { |
118
|
|
|
return false; |
119
|
|
|
} |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Destroy a session. |
124
|
|
|
* |
125
|
|
|
* @param string $id A session ID. |
126
|
|
|
* |
127
|
|
|
* @return bool true, if the session was destroyed, otherwise an |
128
|
|
|
* exception is thrown. |
129
|
|
|
* |
130
|
|
|
* @throws <b>AgaviDatabaseException</b> If the session cannot be |
131
|
|
|
* destroyed. |
132
|
|
|
* |
133
|
|
|
* @author Sean Kerr <[email protected]> |
134
|
|
|
* @author Veikko Mäkinen <[email protected]> |
135
|
|
|
* @author Dominik del Bondio <[email protected]> |
136
|
|
|
* @since 0.11.0 |
137
|
|
|
*/ |
138
|
|
|
public function sessionDestroy($id) |
139
|
|
|
{ |
140
|
|
|
if (!$this->connection) { |
141
|
|
|
return false; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
// get table/column |
145
|
|
|
$db_table = $this->getParameter('db_table'); |
146
|
|
|
$db_id_col = $this->getParameter('db_id_col', 'sess_id'); |
147
|
|
|
|
148
|
|
|
// delete the record associated with this id |
149
|
|
|
$sql = sprintf('DELETE FROM %s WHERE %s = ?', $db_table, $db_id_col); |
150
|
|
|
|
151
|
|
|
try { |
152
|
|
|
$stmt = $this->connection->prepare($sql); |
153
|
|
|
$result = $stmt->execute(array($id)); |
154
|
|
View Code Duplication |
if (!$result) { |
|
|
|
|
155
|
|
|
$errorInfo = $stmt->errorInfo(); |
156
|
|
|
$e = new \PDOException($errorInfo[2], $errorInfo[0]); |
157
|
|
|
$e->errorInfo = $errorInfo; |
158
|
|
|
throw $e; |
159
|
|
|
} |
160
|
|
|
return true; |
161
|
|
|
} catch (\PDOException $e) { |
162
|
|
|
$error = sprintf('PDOException was thrown when trying to manipulate session data. Message: "%s"', $e->getMessage()); |
163
|
|
|
throw new DatabaseException($error, 0, $e); |
164
|
|
|
} |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* Cleanup old sessions. |
169
|
|
|
* |
170
|
|
|
* @param int $lifetime The lifetime of a session. |
171
|
|
|
* |
172
|
|
|
* @return bool true, if old sessions have been cleaned, otherwise an |
173
|
|
|
* exception is thrown. |
174
|
|
|
* |
175
|
|
|
* @throws DatabaseException If old sessions cannot be |
176
|
|
|
* cleaned. |
177
|
|
|
* |
178
|
|
|
* @author Sean Kerr <[email protected]> |
179
|
|
|
* @author Veikko Mäkinen <[email protected]> |
180
|
|
|
* @author Dominik del Bondio <[email protected]> |
181
|
|
|
* @since 0.11.0 |
182
|
|
|
*/ |
183
|
|
|
public function sessionGC($lifetime) |
184
|
|
|
{ |
185
|
|
|
if (!$this->connection) { |
186
|
|
|
return false; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
// determine deletable session time |
190
|
|
|
$time = time() - $lifetime; |
191
|
|
|
$time = date($this->getParameter('date_format', 'U'), $time); |
192
|
|
|
|
193
|
|
|
// get table/column |
194
|
|
|
$db_table = $this->getParameter('db_table'); |
195
|
|
|
$db_time_col = $this->getParameter('db_time_col', 'sess_time'); |
196
|
|
|
|
197
|
|
|
// delete the records that are expired |
198
|
|
|
$sql = sprintf('DELETE FROM %s WHERE %s < :time', $db_table, $db_time_col); |
199
|
|
|
|
200
|
|
|
try { |
201
|
|
|
$stmt = $this->connection->prepare($sql); |
202
|
|
|
if (is_numeric($time)) { |
203
|
|
|
$time = (int)$time; |
204
|
|
|
$stmt->bindValue(':time', $time, PDO::PARAM_INT); |
205
|
|
|
} else { |
206
|
|
|
$stmt->bindValue(':time', $time, PDO::PARAM_STR); |
207
|
|
|
} |
208
|
|
|
$result = $stmt->execute(); |
209
|
|
|
|
210
|
|
View Code Duplication |
if (!$result) { |
|
|
|
|
211
|
|
|
$errorInfo = $stmt->errorInfo(); |
212
|
|
|
$e = new \PDOException($errorInfo[2], $errorInfo[0]); |
213
|
|
|
$e->errorInfo = $errorInfo; |
214
|
|
|
throw $e; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
return true; |
218
|
|
|
} catch (\PDOException $e) { |
219
|
|
|
$error = sprintf('PDOException was thrown when trying to manipulate session data. Message: "%s"', $e->getMessage()); |
220
|
|
|
throw new DatabaseException($error, 0, $e); |
221
|
|
|
} |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Open a session. |
226
|
|
|
* |
227
|
|
|
* @param string $path The path is ignored. |
228
|
|
|
* @param string $name The name is ignored. |
229
|
|
|
* |
230
|
|
|
* @return bool true, if the session was opened, otherwise an exception |
231
|
|
|
* is thrown. |
232
|
|
|
* |
233
|
|
|
* @throws <b>AgaviDatabaseException</b> If a connection with the database |
234
|
|
|
* does not exist or cannot be |
235
|
|
|
* created. |
236
|
|
|
* |
237
|
|
|
* @author Sean Kerr <[email protected]> |
238
|
|
|
* @author Veikko Mäkinen <[email protected]> |
239
|
|
|
* @author Dominik del Bondio <[email protected]> |
240
|
|
|
* @since 0.11.0 |
241
|
|
|
*/ |
242
|
|
|
public function sessionOpen($path, $name) |
|
|
|
|
243
|
|
|
{ |
244
|
|
|
// what database are we using? |
245
|
|
|
$database = $this->getParameter('database', null); |
246
|
|
|
|
247
|
|
|
$this->connection = $this->getContext()->getDatabaseConnection($database); |
248
|
|
|
if ($this->connection === null || !$this->connection instanceof \PDO) { |
249
|
|
|
$error = 'Database connection "' . $database . '" could not be found or is not a PDO database connection.'; |
250
|
|
|
throw new DatabaseException($error); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
return true; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* Read a session. |
258
|
|
|
* |
259
|
|
|
* @param string $id A session ID. |
260
|
|
|
* |
261
|
|
|
* @return bool true, if the session was read, otherwise an exception is |
262
|
|
|
* thrown. |
263
|
|
|
* |
264
|
|
|
* @throws DatabaseException If the session cannot be read. |
265
|
|
|
* |
266
|
|
|
* @author Sean Kerr <[email protected]> |
267
|
|
|
* @author Veikko Mäkinen <[email protected]> |
268
|
|
|
* @author Dominik del Bondio <[email protected]> |
269
|
|
|
* @since 0.11.0 |
270
|
|
|
*/ |
271
|
|
|
public function sessionRead($id) |
272
|
|
|
{ |
273
|
|
|
if (!$this->connection) { |
274
|
|
|
return false; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
// get table/columns |
278
|
|
|
$db_table = $this->getParameter('db_table'); |
279
|
|
|
$db_data_col = $this->getParameter('db_data_col', 'sess_data'); |
280
|
|
|
$db_id_col = $this->getParameter('db_id_col', 'sess_id'); |
281
|
|
|
$db_time_col = $this->getParameter('db_time_col', 'sess_time'); |
|
|
|
|
282
|
|
|
|
283
|
|
|
try { |
284
|
|
|
$sql = sprintf('SELECT %s FROM %s WHERE %s = ?', $db_data_col, $db_table, $db_id_col); |
285
|
|
|
|
286
|
|
|
$stmt = $this->connection->prepare($sql); |
287
|
|
|
$result = $stmt->execute(array($id)); |
288
|
|
|
|
289
|
|
View Code Duplication |
if (!$result) { |
|
|
|
|
290
|
|
|
$errorInfo = $stmt->errorInfo(); |
291
|
|
|
$e = new \PDOException($errorInfo[2], $errorInfo[0]); |
292
|
|
|
$e->errorInfo = $errorInfo; |
293
|
|
|
throw $e; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
if ($result = $stmt->fetch(\PDO::FETCH_NUM)) { |
297
|
|
|
$result = $result[0]; |
298
|
|
|
// pdo is returning the LOB as stream, so check if we had a lob (this seems to differ from db to db) |
299
|
|
|
if (is_resource($result)) { |
300
|
|
|
$result = stream_get_contents($result); |
301
|
|
|
} |
302
|
|
|
return $result; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
return ''; |
|
|
|
|
306
|
|
|
} catch (\PDOException $e) { |
307
|
|
|
$error = sprintf('PDOException was thrown when trying to manipulate session data. Message: "%s"', $e->getMessage()); |
308
|
|
|
throw new DatabaseException($error, 0, $e); |
309
|
|
|
} |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* Write session data. |
314
|
|
|
* |
315
|
|
|
* @param string $id session ID. |
316
|
|
|
* @param string $data A serialized chunk of session data. |
317
|
|
|
* |
318
|
|
|
* @return bool true, if the session was written, otherwise an exception |
319
|
|
|
* is thrown. |
320
|
|
|
* |
321
|
|
|
* @throws <b>AgaviDatabaseException</b> If session data cannot be |
322
|
|
|
* written. |
323
|
|
|
* |
324
|
|
|
* @author Sean Kerr <[email protected]> |
325
|
|
|
* @author Veikko Mäkinen <[email protected]> |
326
|
|
|
* @author Dominik del Bondio <[email protected]> |
327
|
|
|
* @since 0.11.0 |
328
|
|
|
*/ |
329
|
|
|
public function sessionWrite($id, $data) |
330
|
|
|
{ |
331
|
|
|
if (!$this->connection) { |
332
|
|
|
return false; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
// get table/column |
336
|
|
|
$db_table = $this->getParameter('db_table'); |
337
|
|
|
$db_data_col = $this->getParameter('db_data_col', 'sess_data'); |
338
|
|
|
$db_id_col = $this->getParameter('db_id_col', 'sess_id'); |
339
|
|
|
$db_time_col = $this->getParameter('db_time_col', 'sess_time'); |
340
|
|
|
|
341
|
|
|
$isOracle = $this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'oracle'; |
342
|
|
|
$useLob = $this->getParameter('data_as_lob', true); |
343
|
|
|
$columnType = ($isOracle || $useLob) ? \PDO::PARAM_LOB : \PDO::PARAM_STR; |
344
|
|
|
|
345
|
|
|
if ($isOracle) { |
346
|
|
|
$sp = fopen('php://memory', 'r+'); |
347
|
|
|
fwrite($sp, $data); |
348
|
|
|
rewind($sp); |
349
|
|
|
} else { |
350
|
|
|
$sp = $data; |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
$ts = date($this->getParameter('date_format', 'U')); |
354
|
|
|
if (is_numeric($ts)) { |
355
|
|
|
$ts = (int)$ts; |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
try { |
359
|
|
|
// pretend the session does not exist and attempt to create it first |
360
|
|
|
$sql = sprintf('INSERT INTO %s (%s, %s, %s) VALUES (:id, :data, :time)', $db_table, $db_id_col, $db_data_col, $db_time_col); |
361
|
|
|
|
362
|
|
|
$stmt = $this->connection->prepare($sql); |
363
|
|
|
$stmt->bindParam(':id', $id); |
364
|
|
|
$stmt->bindParam(':data', $sp, $columnType); |
365
|
|
View Code Duplication |
if (is_int($ts)) { |
|
|
|
|
366
|
|
|
$stmt->bindValue(':time', $ts, \PDO::PARAM_INT); |
367
|
|
|
} else { |
368
|
|
|
$stmt->bindValue(':time', $ts, \PDO::PARAM_STR); |
369
|
|
|
} |
370
|
|
|
$this->connection->beginTransaction(); |
371
|
|
View Code Duplication |
if (!$stmt->execute()) { |
|
|
|
|
372
|
|
|
$errorInfo = $stmt->errorInfo(); |
373
|
|
|
$e = new \PDOException($errorInfo[2], $errorInfo[0]); |
374
|
|
|
$e->errorInfo = $errorInfo; |
375
|
|
|
throw $e; |
376
|
|
|
} |
377
|
|
View Code Duplication |
if (!$this->connection->commit()) { |
|
|
|
|
378
|
|
|
$errorInfo = $stmt->errorInfo(); |
379
|
|
|
$e = new \PDOException($errorInfo[2], $errorInfo[0]); |
380
|
|
|
$e->errorInfo = $errorInfo; |
381
|
|
|
throw $e; |
382
|
|
|
} |
383
|
|
|
} catch (\PDOException $e) { |
384
|
|
|
// something went wrong; probably a key collision, which means this session already exists |
385
|
|
|
$this->connection->rollback(); |
386
|
|
|
|
387
|
|
|
if ($isOracle) { |
388
|
|
|
$sql = sprintf('UPDATE %s SET %s = EMPTY_BLOB(), %s = :time WHERE %s = :id RETURNING %s INTO :data', $db_table, $db_data_col, $db_time_col, $db_id_col, $db_data_col); |
389
|
|
|
} else { |
390
|
|
|
$sql = sprintf('UPDATE %s SET %s = :data, %s = :time WHERE %s = :id', $db_table, $db_data_col, $db_time_col, $db_id_col); |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
$stmt = $this->connection->prepare($sql); |
394
|
|
|
$stmt->bindParam(':data', $sp, $columnType); |
395
|
|
View Code Duplication |
if (is_int($ts)) { |
|
|
|
|
396
|
|
|
$stmt->bindValue(':time', $ts, \PDO::PARAM_INT); |
397
|
|
|
} else { |
398
|
|
|
$stmt->bindValue(':time', $ts, \PDO::PARAM_STR); |
399
|
|
|
} |
400
|
|
|
$stmt->bindParam(':id', $id); |
401
|
|
|
$this->connection->beginTransaction(); |
402
|
|
View Code Duplication |
if (!$stmt->execute()) { |
|
|
|
|
403
|
|
|
$errorInfo = $stmt->errorInfo(); |
404
|
|
|
$e = new \PDOException($errorInfo[2], $errorInfo[0]); |
405
|
|
|
$e->errorInfo = $errorInfo; |
406
|
|
|
throw $e; |
407
|
|
|
} |
408
|
|
View Code Duplication |
if (!$this->connection->commit()) { |
|
|
|
|
409
|
|
|
$errorInfo = $stmt->errorInfo(); |
410
|
|
|
$e = new \PDOException($errorInfo[2], $errorInfo[0]); |
411
|
|
|
$e->errorInfo = $errorInfo; |
412
|
|
|
throw $e; |
413
|
|
|
} |
414
|
|
|
} catch (\PDOException $e) { |
415
|
|
|
$this->connection->rollback(); |
416
|
|
|
$error = sprintf('PDOException was thrown when trying to manipulate session data. Message: "%s"', $e->getMessage()); |
417
|
|
|
throw new DatabaseException($error, 0, $e); |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
return true; |
421
|
|
|
} |
422
|
|
|
} |
423
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.