1212namespace Symfony \Component \Lock \Store ;
1313
1414use Doctrine \DBAL \Connection ;
15+ use Doctrine \DBAL \DriverManager ;
1516use Doctrine \DBAL \Exception as DBALException ;
1617use Doctrine \DBAL \Schema \Schema ;
1718use Symfony \Component \Lock \Exception \InvalidArgumentException ;
1819use Symfony \Component \Lock \Exception \InvalidTtlException ;
1920use Symfony \Component \Lock \Exception \LockConflictedException ;
20- use Symfony \Component \Lock \Exception \NotSupportedException ;
2121use Symfony \Component \Lock \Key ;
2222use Symfony \Component \Lock \PersistingStoreInterface ;
2323
3434 *
3535 * @author Jérémy Derussé <jeremy@derusse.com>
3636 */
37- class DbalStore implements PersistingStoreInterface
37+ class DoctrineDbalStore implements PersistingStoreInterface
3838{
3939use ExpiringStoreTrait;
4040
4141private $ conn ;
42+ private $ dsn ;
4243private $ driver ;
4344private $ table ='lock_keys ' ;
4445private $ idCol ='key_id ' ;
@@ -54,15 +55,15 @@ class DbalStore implements PersistingStoreInterface
5455 * * db_token_col: The column where to store the lock token [default: key_token]
5556 * * db_expiration_col: The column where to store the expiration [default: key_expiration]
5657 *
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
6162 *
6263 * @throws InvalidArgumentException When namespace contains invalid characters
6364 * @throws InvalidArgumentException When the initial ttl is not valid
6465 */
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 )
6667 {
6768if ($ gcProbability <0 ||$ gcProbability >1 ) {
6869throw 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
7172throw new InvalidTtlException (sprintf ('"%s()" expects a strictly positive TTL, "%d" given. ' ,__METHOD__ ,$ initialTtl ));
7273 }
7374
74- $ this ->conn =$ conn ;
75+ if ($ connOrDsninstanceof 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+ }
7582
7683$ this ->table =$ options ['db_table ' ] ??$ this ->table ;
7784$ this ->idCol =$ options ['db_id_col ' ] ??$ this ->idCol ;
@@ -90,13 +97,12 @@ public function save(Key $key)
9097$ key ->reduceLifetime ($ this ->initialTtl );
9198
9299$ 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 ));
97100
98101try {
99- $ stmt ->executeStatement ();
102+ $ this ->getConnection ()->executeStatement ($ sql , [
103+ 'id ' =>$ this ->getHashedKey ($ key ),
104+ 'token ' =>$ this ->getUniqueToken ($ key ),
105+ ]);
100106 }catch (DBALException $ e ) {
101107// the lock is already acquired. It could be us. Let's try to put off.
102108$ this ->putOffExpiration ($ key ,$ this ->initialTtl );
@@ -121,13 +127,13 @@ public function putOffExpiration(Key $key, $ttl)
121127$ key ->reduceLifetime ($ ttl );
122128
123129$ 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-
126130$ 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+ ]);
131137
132138// 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
133139if (!$ result ->rowCount () && !$ this ->exists ($ key )) {
@@ -142,12 +148,10 @@ public function putOffExpiration(Key $key, $ttl)
142148 */
143149public function delete (Key $ key )
144150 {
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+ ]);
151155 }
152156
153157/**
@@ -156,13 +160,12 @@ public function delete(Key $key)
156160public function exists (Key $ key )
157161 {
158162$ 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+ ]);
164167
165- return (bool )$ result-> fetchOne () ;
168+ return (bool )$ result ;
166169 }
167170
168171/**
@@ -186,8 +189,15 @@ private function getUniqueToken(Key $key): string
186189/**
187190 * @return Connection
188191 */
189- private function getConnection ()
192+ private function getConnection (): object
190193 {
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+
191201return $ this ->conn ;
192202 }
193203
@@ -198,29 +208,40 @@ private function getConnection()
198208 */
199209public function createTable ():void
200210 {
211+ $ schema =new Schema ();
212+ $ this ->configureSchema ($ schema );
213+
201214$ 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+ }
202228
203- $ schema =new Schema ();
204229$ table =$ schema ->createTable ($ this ->table );
205230$ table ->addColumn ($ this ->idCol ,'string ' , ['length ' =>64 ]);
206231$ table ->addColumn ($ this ->tokenCol ,'string ' , ['length ' =>44 ]);
207232$ table ->addColumn ($ this ->expirationCol ,'integer ' , ['unsigned ' =>true ]);
208233$ table ->setPrimaryKey ([$ this ->idCol ]);
209-
210- foreach ($ schema ->toSql ($ conn ->getDatabasePlatform ())as $ sql ) {
211- $ conn ->executeStatement ($ sql );
212- }
213234 }
214235
236+
215237/**
216238 * Cleans up the table by removing all expired locks.
217239 */
218240private function prune ():void
219241 {
220242$ sql ="DELETE FROM $ this ->table WHERE $ this ->expirationCol <= {$ this ->getCurrentTimestampStatement ()}" ;
221243
222- $ conn =$ this ->getConnection ();
223- $ conn ->executeStatement ($ sql );
244+ $ this ->getConnection ()->executeStatement ($ sql );
224245 }
225246
226247private function getDriver ():string
@@ -280,7 +301,7 @@ private function getCurrentTimestampStatement(): string
280301case 'sqlsrv ' :
281302return 'DATEDIFF(s, \'1970-01-01 \', GETUTCDATE()) ' ;
282303default :
283- return time ();
304+ return ( string ) time ();
284305 }
285306 }
286307}