Pessimistic.php 8.39 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
<?php
/**
 * Class for handling the pessimistic offline locking of {@link Doctrine_Record}s.
 * Offline locking of records comes in handy where you need to make sure that
 * a time-consuming task on a record or many records, which is spread over several
 * page requests can't be interfered by other users. 
 * 
 * @author  Roman Borschel <roman@code-factory.org>
 * @license LGPL
 * @since   1.0
 */
class Doctrine_Locking_Manager_Pessimistic
{
    /**
     * The datasource that is used by the locking manager
     *
     * @var PDO object
     */
    private $_dataSource;
    /**
     * The database table name for the lock tracking
     */
doctrine's avatar
doctrine committed
23
    private $_lockTable = 'doctrine_lock_tracking';
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

    /**
     * Constructs a new locking manager object
     * 
     * When the CREATE_TABLES attribute of the session on which the manager
     * is supposed to work on is set to true, the locking table is created.
     *
     * @param Doctrine_Session $dataSource The database session to use
     */
    public function __construct(Doctrine_Session $dataSource)
    {
        $this->_dataSource = $dataSource;
        
        if($this->_dataSource->getAttribute(Doctrine::ATTR_CREATE_TABLES) === true)
        {
            $columns = array();
            $columns['object_type']        = array('string', 50, 'notnull|primary');
            $columns['object_key']         = array('string', 250, 'notnull|primary');
            $columns['user_ident']         = array('string', 50, 'notnull');
            $columns['timestamp_obtained'] = array('integer', 10, 'notnull');
            
            $dataDict = new Doctrine_DataDict($this->_dataSource->getDBH());
doctrine's avatar
doctrine committed
46
            $dataDict->createTable($this->_lockTable, $columns);
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
        }
               
    }

    /**
     * Obtains a lock on a {@link Doctrine_Record}
     *
     * @param  Doctrine_Record $record     The record that has to be locked
     * @param  mixed           $userIdent  A unique identifier of the locking user
     * @return boolean  TRUE if the locking was successful, FALSE if another user
     *                  holds a lock on this record
     * @throws Doctrine_Locking_Exception  If the locking failed due to database errors
     */
    public function getLock(Doctrine_Record $record, $userIdent)
    {
        $objectType = $record->getTable()->getComponentName();
        $key        = $record->getID();
        
        $gotLock = false;
        
        if(is_array($key))
        {
            // Composite key
            $key = implode('|', $key);
        }
        
        try
        {
            $dbh = $this->_dataSource->getDBH();
            $dbh->beginTransaction();
            
doctrine's avatar
doctrine committed
78
            $stmt = $dbh->prepare("INSERT INTO $this->_lockTable
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
                                          (object_type, object_key, user_ident, timestamp_obtained)
                                   VALUES (:object_type, :object_key, :user_ident, :ts_obtained)");
            $stmt->bindParam(':object_type', $objectType);
            $stmt->bindParam(':object_key', $key);
            $stmt->bindParam(':user_ident', $userIdent);
            $stmt->bindParam(':ts_obtained', time());
            
            try {
                $stmt->execute();
                $gotLock = true;
            } catch(PDOException $pkviolation) {
                // PK violation occured => existing lock!
            }
            
            if(!$gotLock)
            {
                $lockingUserIdent = $this->_getLockingUserIdent($objectType, $key);
                if($lockingUserIdent !== null && $lockingUserIdent == $userIdent)
                {
                    $gotLock = true; // The requesting user already has a lock
                    // Update timestamp
doctrine's avatar
doctrine committed
100
                    $stmt = $dbh->prepare("UPDATE $this->_lockTable SET timestamp_obtained = :ts
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
                                           WHERE object_type = :object_type AND
                                                 object_key  = :object_key  AND
                                                 user_ident  = :user_ident");
                    $stmt->bindParam(':ts', time());
                    $stmt->bindParam(':object_type', $objectType);
                    $stmt->bindParam(':object_key', $key);
                    $stmt->bindParam(':user_ident', $lockingUserIdent);
                    $stmt->execute();
                }
            }

            $dbh->commit();
                       
        }
        catch(PDOException $pdoe)
        {
            $dbh->rollBack();
            throw new Doctrine_Locking_Exception($pdoe->getMessage());
        }
        
        return $gotLock;
    }

    /**
     * Releases a lock on a {@link Doctrine_Record}
     *
     * @param  Doctrine_Record $record    The record for which the lock has to be released
     * @param  mixed           $userIdent The unique identifier of the locking user
     * @return boolean  TRUE if a lock was released, FALSE if no lock was released
     * @throws Doctrine_Locking_Exception If the release procedure failed due to database errors
     */
    public function releaseLock(Doctrine_Record $record, $userIdent)
    {
        $objectType = $record->getTable()->getComponentName();
        $key        = $record->getID();
        
        if(is_array($key))
        {
            // Composite key
            $key = implode('|', $key);
        }
        
        try
        {
            $dbh = $this->_dataSource->getDBH();
doctrine's avatar
doctrine committed
146
            $stmt = $dbh->prepare("DELETE FROM $this->_lockTable WHERE
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
                                        object_type = :object_type AND
                                        object_key  = :object_key  AND
                                        user_ident  = :user_ident");
            $stmt->bindParam(':object_type', $objectType);
            $stmt->bindParam(':object_key', $key);
            $stmt->bindParam(':user_ident', $userIdent);
            $stmt->execute();
            
            $count = $stmt->rowCount();
            
            return ($count > 0);
                        
        }
        catch(PDOException $pdoe)
        {
            throw new Doctrine_Locking_Exception($pdoe->getMessage());
        }
    }

    /**
     * Gets the unique user identifier of a lock
     *
     * @param  string $objectType  The type of the object (component name)
     * @param  mixed  $key         The unique key of the object
     * @return mixed  The unique user identifier for the specified lock
     * @throws Doctrine_Locking_Exception If the query failed due to database errors
     */
    private function _getLockingUserIdent($objectType, $key)
    {
        if(is_array($key))
        {
            // Composite key
            $key = implode('|', $key);
        }
        
        try
        {
            $dbh = $this->_dataSource->getDBH();
            $stmt = $dbh->prepare("SELECT user_ident
doctrine's avatar
doctrine committed
186
                                   FROM $this->_lockTable
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
                                   WHERE object_type = :object_type AND object_key = :object_key");
            $stmt->bindParam(':object_type', $objectType);
            $stmt->bindParam(':object_key', $key);
            $success = $stmt->execute();
            
            if(!$success)
            {
                throw new Doctrine_Locking_Exception("Failed to determine locking user");
            }
            
            $user_ident = $stmt->fetchColumn();
        }
        catch(PDOException $pdoe)
        {
            throw new Doctrine_Locking_Exception($pdoe->getMessage());
        }
        
        return $user_ident;
    }

    /**
     * Releases locks older than a defined amount of seconds
     * 
     * When called without parameters all locks older than 15 minutes are released.
     *
     * @param  integer $age  The maximum valid age of locks in seconds
213
     * @return integer The number of locks that have been released
214 215 216 217 218 219 220 221 222
     * @throws Doctrine_Locking_Exception If the release process failed due to database errors
     */
    public function releaseAgedLocks($age = 900)
    {
        $age = time() - $age;
        
        try
        {
            $dbh = $this->_dataSource->getDBH();
doctrine's avatar
doctrine committed
223
            $stmt = $dbh->prepare("DELETE FROM $this->_lockTable WHERE timestamp_obtained < :age");
224 225 226 227 228
            $stmt->bindParam(':age', $age);
            $stmt->execute();
            
            $count = $stmt->rowCount();
            
229
            return $count;
230 231 232 233 234 235 236 237 238 239
        }
        catch(PDOException $pdoe)
        {
            throw new Doctrine_Locking_Exception($pdoe->getMessage());
        }
    }

}


240
?>