12
12
namespace Symfony \Component \Lock \Store ;
13
13
14
14
use Doctrine \DBAL \Connection ;
15
+ use Doctrine \DBAL \DriverManager ;
15
16
use Doctrine \DBAL \Exception as DBALException ;
16
17
use Doctrine \DBAL \Schema \Schema ;
17
18
use Symfony \Component \Lock \Exception \InvalidArgumentException ;
18
19
use Symfony \Component \Lock \Exception \InvalidTtlException ;
19
20
use Symfony \Component \Lock \Exception \LockConflictedException ;
20
- use Symfony \Component \Lock \Exception \NotSupportedException ;
21
21
use Symfony \Component \Lock \Key ;
22
22
use Symfony \Component \Lock \PersistingStoreInterface ;
23
23
34
34
*
35
35
* @author Jérémy Derussé <jeremy@derusse.com>
36
36
*/
37
- class DbalStore implements PersistingStoreInterface
37
+ class DoctrineDbalStore implements PersistingStoreInterface
38
38
{
39
39
use ExpiringStoreTrait;
40
40
41
41
private $ conn ;
42
+ private $ dsn ;
42
43
private $ driver ;
43
44
private $ table = 'lock_keys ' ;
44
45
private $ idCol = 'key_id ' ;
@@ -54,15 +55,15 @@ class DbalStore implements PersistingStoreInterface
54
55
* * db_token_col: The column where to store the lock token [default: key_token]
55
56
* * db_expiration_col: The column where to store the expiration [default: key_expiration]
56
57
*
57
- * @param Connection $conn A DBAL Connection instance
58
- * @param array $options An associative array of options
59
- * @param float $gcProbability Probability expressed as floating number between 0 and 1 to clean old locks
60
- * @param int $initialTtl The expiration delay of locks in seconds
58
+ * @param Connection|String $connOrDsn A DBAL Connection instance
59
+ * @param array $options An associative array of options
60
+ * @param float $gcProbability Probability expressed as floating number between 0 and 1 to clean old locks
61
+ * @param int $initialTtl The expiration delay of locks in seconds
61
62
*
62
63
* @throws InvalidArgumentException When namespace contains invalid characters
63
64
* @throws InvalidArgumentException When the initial ttl is not valid
64
65
*/
65
- public function __construct (Connection $ conn , array $ options = [], float $ gcProbability = 0.01 , int $ initialTtl = 300 )
66
+ public function __construct ($ connOrDsn , array $ options = [], float $ gcProbability = 0.01 , int $ initialTtl = 300 )
66
67
{
67
68
if ($ gcProbability < 0 || $ gcProbability > 1 ) {
68
69
throw new InvalidArgumentException (sprintf ('"%s" requires gcProbability between 0 and 1, "%f" given. ' , __METHOD__ , $ gcProbability ));
@@ -71,7 +72,13 @@ public function __construct(Connection $conn, array $options = [], float $gcProb
71
72
throw new InvalidTtlException (sprintf ('"%s()" expects a strictly positive TTL, "%d" given. ' , __METHOD__ , $ initialTtl ));
72
73
}
73
74
74
- $ this ->conn = $ conn ;
75
+ if ($ connOrDsn instanceof Connection) {
76
+ $ this ->conn = $ connOrDsn ;
77
+ } elseif (\is_string ($ connOrDsn )) {
78
+ $ this ->dsn = $ connOrDsn ;
79
+ } else {
80
+ throw new InvalidArgumentException (sprintf ('"%s" requires Doctrine\DBAL\Connection instance or DSN string as first argument, "%s" given. ' , __CLASS__ , get_debug_type ($ connOrDsn )));
81
+ }
75
82
76
83
$ this ->table = $ options ['db_table ' ] ?? $ this ->table ;
77
84
$ this ->idCol = $ options ['db_id_col ' ] ?? $ this ->idCol ;
@@ -90,13 +97,12 @@ public function save(Key $key)
90
97
$ key ->reduceLifetime ($ this ->initialTtl );
91
98
92
99
$ sql = "INSERT INTO $ this ->table ( $ this ->idCol , $ this ->tokenCol , $ this ->expirationCol ) VALUES (:id, :token, {$ this ->getCurrentTimestampStatement ()} + $ this ->initialTtl ) " ;
93
- $ stmt = $ this ->getConnection ()->prepare ($ sql );
94
-
95
- $ stmt ->bindValue (':id ' , $ this ->getHashedKey ($ key ));
96
- $ stmt ->bindValue (':token ' , $ this ->getUniqueToken ($ key ));
97
100
98
101
try {
99
- $ stmt ->executeStatement ();
102
+ $ this ->getConnection ()->executeStatement ($ sql , [
103
+ 'id ' => $ this ->getHashedKey ($ key ),
104
+ 'token ' => $ this ->getUniqueToken ($ key ),
105
+ ]);
100
106
} catch (DBALException $ e ) {
101
107
// the lock is already acquired. It could be us. Let's try to put off.
102
108
$ this ->putOffExpiration ($ key , $ this ->initialTtl );
@@ -121,13 +127,13 @@ public function putOffExpiration(Key $key, $ttl)
121
127
$ key ->reduceLifetime ($ ttl );
122
128
123
129
$ sql = "UPDATE $ this ->table SET $ this ->expirationCol = {$ this ->getCurrentTimestampStatement ()} + $ ttl, $ this ->tokenCol = :token1 WHERE $ this ->idCol = :id AND ( $ this ->tokenCol = :token2 OR $ this ->expirationCol <= {$ this ->getCurrentTimestampStatement ()}) " ;
124
- $ stmt = $ this ->getConnection ()->prepare ($ sql );
125
-
126
130
$ uniqueToken = $ this ->getUniqueToken ($ key );
127
- $ stmt ->bindValue (':id ' , $ this ->getHashedKey ($ key ));
128
- $ stmt ->bindValue (':token1 ' , $ uniqueToken );
129
- $ stmt ->bindValue (':token2 ' , $ uniqueToken );
130
- $ result = $ stmt ->executeQuery ();
131
+
132
+ $ result = $ this ->getConnection ()->executeQuery ($ sql , [
133
+ 'id ' => $ this ->getHashedKey ($ key ),
134
+ 'token1 ' => $ uniqueToken ,
135
+ 'token2 ' => $ uniqueToken ,
136
+ ]);
131
137
132
138
// If this method is called twice in the same second, the row wouldn't be updated. We have to call exists to know if we are the owner
133
139
if (!$ result ->rowCount () && !$ this ->exists ($ key )) {
@@ -142,12 +148,10 @@ public function putOffExpiration(Key $key, $ttl)
142
148
*/
143
149
public function delete (Key $ key )
144
150
{
145
- $ sql = "DELETE FROM $ this ->table WHERE $ this ->idCol = :id AND $ this ->tokenCol = :token " ;
146
- $ stmt = $ this ->getConnection ()->prepare ($ sql );
147
-
148
- $ stmt ->bindValue (':id ' , $ this ->getHashedKey ($ key ));
149
- $ stmt ->bindValue (':token ' , $ this ->getUniqueToken ($ key ));
150
- $ stmt ->executeStatement ();
151
+ $ this ->getConnection ()->delete ($ this ->table , [
152
+ $ this ->idCol => $ this ->getHashedKey ($ key ),
153
+ $ this ->tokenCol => $ this ->getUniqueToken ($ key ),
154
+ ]);
151
155
}
152
156
153
157
/**
@@ -156,13 +160,12 @@ public function delete(Key $key)
156
160
public function exists (Key $ key )
157
161
{
158
162
$ sql = "SELECT 1 FROM $ this ->table WHERE $ this ->idCol = :id AND $ this ->tokenCol = :token AND $ this ->expirationCol > {$ this ->getCurrentTimestampStatement ()}" ;
159
- $ stmt = $ this ->getConnection ()->prepare ($ sql );
160
-
161
- $ stmt ->bindValue (':id ' , $ this ->getHashedKey ($ key ));
162
- $ stmt ->bindValue (':token ' , $ this ->getUniqueToken ($ key ));
163
- $ result = $ stmt ->executeQuery ();
163
+ $ result = $ this ->getConnection ()->fetchOne ($ sql , [
164
+ 'id ' => $ this ->getHashedKey ($ key ),
165
+ 'token ' => $ this ->getUniqueToken ($ key ),
166
+ ]);
164
167
165
- return (bool ) $ result-> fetchOne () ;
168
+ return (bool ) $ result ;
166
169
}
167
170
168
171
/**
@@ -186,8 +189,15 @@ private function getUniqueToken(Key $key): string
186
189
/**
187
190
* @return Connection
188
191
*/
189
- private function getConnection ()
192
+ private function getConnection (): object
190
193
{
194
+ if (null === $ this ->conn ) {
195
+ if (!class_exists (DriverManager::class)) {
196
+ throw new InvalidArgumentException (sprintf ('Failed to parse the DSN "%s". Try running "composer require doctrine/dbal". ' , $ this ->dsn ));
197
+ }
198
+ $ this ->conn = DriverManager::getConnection (['url ' => $ this ->dsn ]);
199
+ }
200
+
191
201
return $ this ->conn ;
192
202
}
193
203
@@ -198,29 +208,40 @@ private function getConnection()
198
208
*/
199
209
public function createTable (): void
200
210
{
211
+ $ schema = new Schema ();
212
+ $ this ->configureSchema ($ schema );
213
+
201
214
$ conn = $ this ->getConnection ();
215
+ foreach ($ schema ->toSql ($ conn ->getDatabasePlatform ()) as $ sql ) {
216
+ $ conn ->executeStatement ($ sql );
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Adds the Table to the Schema if it doesn't exist.
222
+ */
223
+ public function configureSchema (Schema $ schema ): void
224
+ {
225
+ if ($ schema ->hasTable ($ this ->table )) {
226
+ return ;
227
+ }
202
228
203
- $ schema = new Schema ();
204
229
$ table = $ schema ->createTable ($ this ->table );
205
230
$ table ->addColumn ($ this ->idCol , 'string ' , ['length ' => 64 ]);
206
231
$ table ->addColumn ($ this ->tokenCol , 'string ' , ['length ' => 44 ]);
207
232
$ table ->addColumn ($ this ->expirationCol , 'integer ' , ['unsigned ' => true ]);
208
233
$ table ->setPrimaryKey ([$ this ->idCol ]);
209
-
210
- foreach ($ schema ->toSql ($ conn ->getDatabasePlatform ()) as $ sql ) {
211
- $ conn ->executeStatement ($ sql );
212
- }
213
234
}
214
235
236
+
215
237
/**
216
238
* Cleans up the table by removing all expired locks.
217
239
*/
218
240
private function prune (): void
219
241
{
220
242
$ sql = "DELETE FROM $ this ->table WHERE $ this ->expirationCol <= {$ this ->getCurrentTimestampStatement ()}" ;
221
243
222
- $ conn = $ this ->getConnection ();
223
- $ conn ->executeStatement ($ sql );
244
+ $ this ->getConnection ()->executeStatement ($ sql );
224
245
}
225
246
226
247
private function getDriver (): string
@@ -280,7 +301,7 @@ private function getCurrentTimestampStatement(): string
280
301
case 'sqlsrv ' :
281
302
return 'DATEDIFF(s, \'1970-01-01 \', GETUTCDATE()) ' ;
282
303
default :
283
- return time ();
304
+ return ( string ) time ();
284
305
}
285
306
}
286
307
}
0 commit comments