Coverage for Doctrine_Locking_Manager_Pessimistic

Back to coverage report

1 <?php
2 /*
3  *  $Id: Pessimistic.php 4104 2008-03-24 01:06:27Z jwage $
4  *
5  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
6  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
7  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
8  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
9  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
11  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
12  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
13  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
14  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
15  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16  *
17  * This software consists of voluntary contributions made by many individuals
18  * and is licensed under the LGPL. For more information, see
19  * <http://www.phpdoctrine.org>.
20  */
21
22 /**
23  * Offline locking of records comes in handy where you need to make sure that
24  * a time-consuming task on a record or many records, which is spread over several
25  * page requests can't be interfered by other users.
26  *
27  * @package     Doctrine
28  * @subpackage  Locking
29  * @link        www.phpdoctrine.org
30  * @author      Roman Borschel <roman@code-factory.org>
31  * @author      Pierre Minnieur <pm@pierre-minnieur.de>
32  * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
33  * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
34  * @since       1.0
35  * @version     $Revision: 4104 $
36  */
37 class Doctrine_Locking_Manager_Pessimistic
38 {
39     /**
40      * The conn that is used by the locking manager
41      *
42      * @var Doctrine_Connection object
43      */
44     private $conn;
45
46     /**
47      * The database table name for the lock tracking
48      */
49     private $_lockTable = 'doctrine_lock_tracking';
50
51     /**
52      * Constructs a new locking manager object
53      *
54      * When the CREATE_TABLES attribute of the connection on which the manager
55      * is supposed to work on is set to true, the locking table is created.
56      *
57      * @param Doctrine_Connection $conn The database connection to use
58      */
59     public function __construct(Doctrine_Connection $conn)
60     {
61         $this->conn = $conn;
62
63         if ($this->conn->getAttribute(Doctrine::ATTR_EXPORT) & Doctrine::EXPORT_TABLES) {
64             $columns = array();
65             $columns['object_type']        = array('type'    => 'string',
66                                                    'length'  => 50,
67                                                    'notnull' => true,
68                                                    'primary' => true);
69
70             $columns['object_key']         = array('type'    => 'string',
71                                                    'length'  => 250,
72                                                    'notnull' => true,
73                                                    'primary' => true);
74
75             $columns['user_ident']         = array('type'    => 'string',
76                                                    'length'  => 50,
77                                                    'notnull' => true);
78
79             $columns['timestamp_obtained'] = array('type'    => 'integer',
80                                                    'length'  => 10,
81                                                    'notnull' => true);
82
83             $options = array('primary' => array('object_type', 'object_key'));
84             try {
85                 $this->conn->export->createTable($this->_lockTable, $columns, $options);
86             } catch(Exception $e) {
87
88             }
89         }
90     }
91
92     /**
93      * Obtains a lock on a {@link Doctrine_Record}
94      *
95      * @param  Doctrine_Record $record     The record that has to be locked
96      * @param  mixed           $userIdent  A unique identifier of the locking user
97      * @return boolean  TRUE if the locking was successful, FALSE if another user
98      *                  holds a lock on this record
99      * @throws Doctrine_Locking_Exception  If the locking failed due to database errors
100      */
101     public function getLock(Doctrine_Record $record, $userIdent)
102     {
103         $objectType = $record->getTable()->getComponentName();
104         $key        = $record->getTable()->getIdentifier();
105
106         $gotLock = false;
107         $time = time();
108
109         if (is_array($key)) {
110             // Composite key
111             $key = implode('|', $key);
112         }
113
114         try {
115             $dbh = $this->conn->getDbh();
116             $this->conn->beginTransaction();
117
118             $stmt = $dbh->prepare('INSERT INTO ' . $this->_lockTable
119                                   . ' (object_type, object_key, user_ident, timestamp_obtained)'
120                                   . ' VALUES (:object_type, :object_key, :user_ident, :ts_obtained)');
121
122             $stmt->bindParam(':object_type', $objectType);
123             $stmt->bindParam(':object_key', $key);
124             $stmt->bindParam(':user_ident', $userIdent);
125             $stmt->bindParam(':ts_obtained', $time);
126
127             try {
128                 $stmt->execute();
129                 $gotLock = true;
130
131             // we catch an Exception here instead of PDOException since we might also be catching Doctrine_Exception
132             } catch(Exception $pkviolation) {
133                 // PK violation occured => existing lock!
134             }
135
136             if ( ! $gotLock) {
137                 $lockingUserIdent = $this->_getLockingUserIdent($objectType, $key);
138                 if ($lockingUserIdent !== null && $lockingUserIdent == $userIdent) {
139                     $gotLock = true; // The requesting user already has a lock
140                     // Update timestamp
141                     $stmt = $dbh->prepare('UPDATE ' . $this->_lockTable 
142                                           . ' SET timestamp_obtained = :ts'
143                                           . ' WHERE object_type = :object_type AND'
144                                           . ' object_key  = :object_key  AND'
145                                           . ' user_ident  = :user_ident');
146                     $stmt->bindParam(':ts', $time);
147                     $stmt->bindParam(':object_type', $objectType);
148                     $stmt->bindParam(':object_key', $key);
149                     $stmt->bindParam(':user_ident', $lockingUserIdent);
150                     $stmt->execute();
151                 }
152             }
153             $this->conn->commit();
154         } catch (Exception $pdoe) {
155             $this->conn->rollback();
156             throw new Doctrine_Locking_Exception($pdoe->getMessage());
157         }
158
159         return $gotLock;
160     }
161
162     /**
163      * Releases a lock on a {@link Doctrine_Record}
164      *
165      * @param  Doctrine_Record $record    The record for which the lock has to be released
166      * @param  mixed           $userIdent The unique identifier of the locking user
167      * @return boolean  TRUE if a lock was released, FALSE if no lock was released
168      * @throws Doctrine_Locking_Exception If the release procedure failed due to database errors
169      */
170     public function releaseLock(Doctrine_Record $record, $userIdent)
171     {
172         $objectType = $record->getTable()->getComponentName();
173         $key        = $record->getTable()->getIdentifier();
174
175         if (is_array($key)) {
176             // Composite key
177             $key = implode('|', $key);
178         }
179
180         try {
181             $dbh = $this->conn->getDbh();
182             $stmt = $dbh->prepare("DELETE FROM $this->_lockTable WHERE
183                                         object_type = :object_type AND
184                                         object_key  = :object_key  AND
185                                         user_ident  = :user_ident");
186             $stmt->bindParam(':object_type', $objectType);
187             $stmt->bindParam(':object_key', $key);
188             $stmt->bindParam(':user_ident', $userIdent);
189             $stmt->execute();
190
191             $count = $stmt->rowCount();
192
193             return ($count > 0);
194         } catch (PDOException $pdoe) {
195             throw new Doctrine_Locking_Exception($pdoe->getMessage());
196         }
197     }
198
199     /**
200      * Gets the unique user identifier of a lock
201      *
202      * @param  string $objectType  The type of the object (component name)
203      * @param  mixed  $key         The unique key of the object
204      * @return mixed  The unique user identifier for the specified lock
205      * @throws Doctrine_Locking_Exception If the query failed due to database errors
206      */
207     private function _getLockingUserIdent($objectType, $key)
208     {
209         if (is_array($key)) {
210             // Composite key
211             $key = implode('|', $key);
212         }
213
214         try {
215             $dbh = $this->conn->getDbh();
216             $stmt = $dbh->prepare('SELECT user_ident FROM ' . $this->_lockTable
217                                   . ' WHERE object_type = :object_type AND object_key = :object_key');
218             $stmt->bindParam(':object_type', $objectType);
219             $stmt->bindParam(':object_key', $key);
220             $success = $stmt->execute();
221
222             if ( ! $success) {
223                 throw new Doctrine_Locking_Exception("Failed to determine locking user");
224             }
225
226             $userIdent = $stmt->fetchColumn();
227         } catch (PDOException $pdoe) {
228             throw new Doctrine_Locking_Exception($pdoe->getMessage());
229         }
230
231         return $userIdent;
232     }
233
234     /**
235      * Gets the identifier that identifies the owner of the lock on the given
236      * record.
237      *
238      * @param Doctrine_Record $lockedRecord  The record.
239      * @return mixed The unique user identifier that identifies the owner of the lock.
240      */
241     public function getLockOwner($lockedRecord)
242     {
243         $objectType = $lockedRecord->getTable()->getComponentName();
244         $key        = $lockedRecord->getTable()->getIdentifier();
245         return $this->_getLockingUserIdent($objectType, $key);
246     }
247
248     /**
249      * Releases locks older than a defined amount of seconds
250      *
251      * When called without parameters all locks older than 15 minutes are released.
252      *
253      * @param  integer $age  The maximum valid age of locks in seconds
254      * @param  string  $objectType  The type of the object (component name)
255      * @param  mixed   $userIdent The unique identifier of the locking user
256      * @return integer The number of locks that have been released
257      * @throws Doctrine_Locking_Exception If the release process failed due to database errors
258      */
259     public function releaseAgedLocks($age = 900, $objectType = null, $userIdent = null)
260     {
261         $age = time() - $age;
262
263         try {
264             $dbh = $this->conn->getDbh();
265             $stmt = $dbh->prepare('DELETE FROM ' . $this->_lockTable . ' WHERE timestamp_obtained < :age');
266             $stmt->bindParam(':age', $age);
267             $query = 'DELETE FROM ' . $this->_lockTable . ' WHERE timestamp_obtained < :age';
268             if ($objectType) {
269                 $query .= ' AND object_type = :object_type';
270             }
271             if ($userIdent) {
272                 $query .= ' AND user_ident = :user_ident';
273             }
274             $stmt = $dbh->prepare($query);
275             $stmt->bindParam(':age', $age);
276             if ($objectType) {
277                 $stmt->bindParam(':object_type', $objectType);
278             }
279             if ($userIdent) {
280                 $stmt->bindParam(':user_ident', $userIdent);
281             }
282             $stmt->execute();
283
284             $count = $stmt->rowCount();
285
286             return $count;
287         } catch (PDOException $pdoe) {
288             throw new Doctrine_Locking_Exception($pdoe->getMessage());
289         }
290     }
291 }