Created
December 8, 2010 12:28
-
-
Save ludofleury/733218 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
* $Id: Pgsql.php 7490 2010-03-29 19:53:27Z jwage $ | |
* | |
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
* | |
* This software consists of voluntary contributions made by many individuals | |
* and is licensed under the LGPL. For more information, see | |
* <http://www.doctrine-project.org>. | |
*/ | |
/** | |
* Doctrine_Sequence_Pgsql | |
* | |
* @package Doctrine | |
* @subpackage Sequence | |
* @author Konsta Vesterinen <kvesteri@cc.hut.fi> | |
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL | |
* @link www.doctrine-project.org | |
* @since 1.0 | |
* @version $Revision: 7490 $ | |
*/ | |
class Doctrine_Sequence_Pgsql extends Doctrine_Sequence | |
{ | |
/** | |
* Initialize or redefines a specific value for a sequence | |
* | |
* @param string $seqName name of the sequence | |
* @param int $value | |
* @return int the value of the sequence | |
* | |
* @author Vincent Paulin <vincent.paulin@simple-it.fr> | |
* @author Ludovic Fleury <ludovic.fleury@simple-it.fr> | |
*/ | |
public function initId($seqName, $value) | |
{ | |
$sequenceName = $this->conn->quoteIdentifier($this->conn->formatter->getSequenceName($seqName), true); | |
$this->conn->fetchOne("SELECT setval('" . $sequenceName . "', ".$value.")"); | |
return $this->currId($seqName); | |
} | |
/** | |
* Returns the next free id of a sequence | |
* | |
* @param string $seqName name of the sequence | |
* @param bool onDemand when true missing sequences are automatic created | |
* | |
* @return integer next id in the given sequence | |
*/ | |
public function nextId($seqName, $onDemand = true) | |
{ | |
$sequenceName = $this->conn->quoteIdentifier($this->conn->formatter->getSequenceName($seqName), true); | |
$query = "SELECT NEXTVAL('" . $sequenceName . "')"; | |
try { | |
$result = (int) $this->conn->fetchOne($query); | |
} catch(Doctrine_Connection_Exception $e) { | |
if ($onDemand && $e->getPortableCode() == Doctrine_Core::ERR_NOSUCHTABLE) { | |
try { | |
$result = $this->conn->export->createSequence($seqName); | |
} catch(Doctrine_Exception $e) { | |
throw new Doctrine_Sequence_Exception('on demand sequence ' . $seqName . ' could not be created'); | |
} | |
return $this->nextId($seqName, false); | |
} else { | |
throw new Doctrine_Sequence_Exception('sequence ' .$seqName . ' does not exist'); | |
} | |
} | |
return $result; | |
} | |
/** | |
* lastInsertId | |
* | |
* Returns the autoincrement ID if supported or $id or fetches the current | |
* ID in a sequence called: $table.(empty($field) ? '' : '_'.$field) | |
* | |
* @param string name of the table into which a new row was inserted | |
* @param string name of the field into which a new row was inserted | |
* @return integer the autoincremented id | |
*/ | |
public function lastInsertId($table = null, $field = null) | |
{ | |
$seqName = $table . (empty($field) ? '' : '_' . $field); | |
$sequenceName = $this->conn->quoteIdentifier($this->conn->formatter->getSequenceName($seqName), true); | |
return (int) $this->conn->fetchOne("SELECT CURRVAL('" . $sequenceName . "')"); | |
} | |
/** | |
* Returns the current id of a sequence | |
* | |
* @param string $seqName name of the sequence | |
* | |
* @return integer current id in the given sequence | |
*/ | |
public function currId($seqName) | |
{ | |
$sequenceName = $this->conn->quoteIdentifier($this->conn->formatter->getSequenceName($seqName), true); | |
return (int) $this->conn->fetchOne('SELECT last_value FROM ' . $sequenceName); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
* $Id: Sequence.php 7490 2010-03-29 19:53:27Z jwage $ | |
* | |
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
* | |
* This software consists of voluntary contributions made by many individuals | |
* and is licensed under the LGPL. For more information, see | |
* <http://www.doctrine-project.org>. | |
*/ | |
/** | |
* Doctrine_Sequence | |
* The base class for sequence handling drivers. | |
* | |
* @package Doctrine | |
* @subpackage Sequence | |
* @author Konsta Vesterinen <kvesteri@cc.hut.fi> | |
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL | |
* @link www.doctrine-project.org | |
* @since 1.0 | |
* @version $Revision: 7490 $ | |
*/ | |
class Doctrine_Sequence extends Doctrine_Connection_Module | |
{ | |
/** | |
* Defines sequence value | |
* | |
* @param string $seqName name of the sequence | |
* @param int $value | |
* @return int the value of the sequence | |
* | |
* @author Vincent Paulin <vincent.paulin@simple-it.fr> | |
* @author Ludovic Fleury <ludovic.fleury@simple-it.fr> | |
*/ | |
public function initId($seqName, $value) | |
{ | |
throw new Doctrine_Sequence_Exception('method not implemented'); | |
} | |
/** | |
* Returns the next free id of a sequence | |
* | |
* @param string $seqName name of the sequence | |
* @param bool when true missing sequences are automatic created | |
* | |
* @return integer next id in the given sequence | |
* @throws Doctrine_Sequence_Exception | |
*/ | |
public function nextId($seqName, $ondemand = true) | |
{ | |
throw new Doctrine_Sequence_Exception('method not implemented'); | |
} | |
/** | |
* Returns the autoincrement ID if supported or $id or fetches the current | |
* ID in a sequence called: $table.(empty($field) ? '' : '_'.$field) | |
* | |
* @param string name of the table into which a new row was inserted | |
* @param string name of the field into which a new row was inserted | |
*/ | |
public function lastInsertId($table = null, $field = null) | |
{ | |
throw new Doctrine_Sequence_Exception('method not implemented'); | |
} | |
/** | |
* Returns the current id of a sequence | |
* | |
* @param string $seqName name of the sequence | |
* | |
* @return integer current id in the given sequence | |
*/ | |
public function currId($seqName) | |
{ | |
$this->warnings[] = 'database does not support getting current | |
sequence value, the sequence value was incremented'; | |
return $this->nextId($seqName); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
* $Id: UnitOfWork.php 7684 2010-08-24 16:34:16Z jwage $ | |
* | |
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
* | |
* This software consists of voluntary contributions made by many individuals | |
* and is licensed under the LGPL. For more information, see | |
* <http://www.doctrine-project.org>. | |
*/ | |
/** | |
* Doctrine_Connection_UnitOfWork | |
* | |
* Note: This class does not have the semantics of a real "Unit of Work" in 0.10/1.0. | |
* Database operations are not queued. All changes to objects are immediately written | |
* to the database. You can think of it as a unit of work in auto-flush mode. | |
* | |
* Referential integrity is currently not always ensured. | |
* | |
* @package Doctrine | |
* @subpackage Connection | |
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL | |
* @link www.doctrine-project.org | |
* @since 1.0 | |
* @version $Revision: 7684 $ | |
* @author Konsta Vesterinen <kvesteri@cc.hut.fi> | |
* @author Roman Borschel <roman@code-factory.org> | |
*/ | |
class Doctrine_Connection_UnitOfWork extends Doctrine_Connection_Module | |
{ | |
/** | |
* Saves the given record and all associated records. | |
* (The save() operation is always cascaded in 0.10/1.0). | |
* | |
* @param Doctrine_Record $record | |
* @return void | |
*/ | |
public function saveGraph(Doctrine_Record $record, $replace = false) | |
{ | |
$record->assignInheritanceValues(); | |
$conn = $this->getConnection(); | |
$conn->connect(); | |
$state = $record->state(); | |
if ($state === Doctrine_Record::STATE_LOCKED || $state === Doctrine_Record::STATE_TLOCKED) { | |
return false; | |
} | |
$record->state($record->exists() ? Doctrine_Record::STATE_LOCKED : Doctrine_Record::STATE_TLOCKED); | |
try { | |
$conn->beginInternalTransaction(); | |
$record->state($state); | |
$event = $record->invokeSaveHooks('pre', 'save'); | |
$state = $record->state(); | |
$isValid = true; | |
if ( ! $event->skipOperation) { | |
$this->saveRelatedLocalKeys($record); | |
switch ($state) { | |
case Doctrine_Record::STATE_TDIRTY: | |
case Doctrine_Record::STATE_TCLEAN: | |
if ($replace) { | |
$isValid = $this->replace($record); | |
} else { | |
$isValid = $this->insert($record); | |
} | |
break; | |
case Doctrine_Record::STATE_DIRTY: | |
case Doctrine_Record::STATE_PROXY: | |
if ($replace) { | |
$isValid = $this->replace($record); | |
} else { | |
$isValid = $this->update($record); | |
} | |
break; | |
case Doctrine_Record::STATE_CLEAN: | |
// do nothing | |
break; | |
} | |
$aliasesUnlinkInDb = array(); | |
if ($isValid) { | |
// NOTE: what about referential integrity issues? | |
foreach ($record->getPendingDeletes() as $pendingDelete) { | |
$pendingDelete->delete(); | |
} | |
foreach ($record->getPendingUnlinks() as $alias => $ids) { | |
if ($ids === false) { | |
$record->unlinkInDb($alias, array()); | |
$aliasesUnlinkInDb[] = $alias; | |
} else if ($ids) { | |
$record->unlinkInDb($alias, array_keys($ids)); | |
$aliasesUnlinkInDb[] = $alias; | |
} | |
} | |
$record->resetPendingUnlinks(); | |
$record->invokeSaveHooks('post', 'save', $event); | |
} else { | |
$conn->transaction->addInvalid($record); | |
} | |
$state = $record->state(); | |
$record->state($record->exists() ? Doctrine_Record::STATE_LOCKED : Doctrine_Record::STATE_TLOCKED); | |
if ($isValid) { | |
$saveLater = $this->saveRelatedForeignKeys($record); | |
foreach ($saveLater as $fk) { | |
$alias = $fk->getAlias(); | |
if ($record->hasReference($alias)) { | |
$obj = $record->$alias; | |
// check that the related object is not an instance of Doctrine_Null | |
if ($obj && ! ($obj instanceof Doctrine_Null)) { | |
$processDiff = !in_array($alias, $aliasesUnlinkInDb); | |
$obj->save($conn, $processDiff); | |
} | |
} | |
} | |
// save the MANY-TO-MANY associations | |
$this->saveAssociations($record); | |
} | |
} | |
$record->state($state); | |
$conn->commit(); | |
} catch (Exception $e) { | |
// Make sure we roll back our internal transaction | |
//$record->state($state); | |
$conn->rollback(); | |
throw $e; | |
} | |
$record->clearInvokedSaveHooks(); | |
return true; | |
} | |
/** | |
* Deletes the given record and all the related records that participate | |
* in an application-level delete cascade. | |
* | |
* this event can be listened by the onPreDelete and onDelete listeners | |
* | |
* @return boolean true on success, false on failure | |
*/ | |
public function delete(Doctrine_Record $record) | |
{ | |
$deletions = array(); | |
$this->_collectDeletions($record, $deletions); | |
return $this->_executeDeletions($deletions); | |
} | |
/** | |
* Collects all records that need to be deleted by applying defined | |
* application-level delete cascades. | |
* | |
* @param array $deletions Map of the records to delete. Keys=Oids Values=Records. | |
*/ | |
private function _collectDeletions(Doctrine_Record $record, array &$deletions) | |
{ | |
if ( ! $record->exists()) { | |
return; | |
} | |
$deletions[$record->getOid()] = $record; | |
$this->_cascadeDelete($record, $deletions); | |
} | |
/** | |
* Executes the deletions for all collected records during a delete operation | |
* (usually triggered through $record->delete()). | |
* | |
* @param array $deletions Map of the records to delete. Keys=Oids Values=Records. | |
*/ | |
private function _executeDeletions(array $deletions) | |
{ | |
// collect class names | |
$classNames = array(); | |
foreach ($deletions as $record) { | |
$classNames[] = $record->getTable()->getComponentName(); | |
} | |
$classNames = array_unique($classNames); | |
// order deletes | |
$executionOrder = $this->buildFlushTree($classNames); | |
// execute | |
try { | |
$this->conn->beginInternalTransaction(); | |
for ($i = count($executionOrder) - 1; $i >= 0; $i--) { | |
$className = $executionOrder[$i]; | |
$table = $this->conn->getTable($className); | |
// collect identifiers | |
$identifierMaps = array(); | |
$deletedRecords = array(); | |
foreach ($deletions as $oid => $record) { | |
if ($record->getTable()->getComponentName() == $className) { | |
$veto = $this->_preDelete($record); | |
if ( ! $veto) { | |
$identifierMaps[] = $record->identifier(); | |
$deletedRecords[] = $record; | |
unset($deletions[$oid]); | |
} | |
} | |
} | |
if (count($deletedRecords) < 1) { | |
continue; | |
} | |
// extract query parameters (only the identifier values are of interest) | |
$params = array(); | |
$columnNames = array(); | |
foreach ($identifierMaps as $idMap) { | |
while (list($fieldName, $value) = each($idMap)) { | |
$params[] = $value; | |
$columnNames[] = $table->getColumnName($fieldName); | |
} | |
} | |
$columnNames = array_unique($columnNames); | |
// delete | |
$tableName = $table->getTableName(); | |
$sql = "DELETE FROM " . $this->conn->quoteIdentifier($tableName) . " WHERE "; | |
if ($table->isIdentifierComposite()) { | |
$sql .= $this->_buildSqlCompositeKeyCondition($columnNames, count($identifierMaps)); | |
$this->conn->exec($sql, $params); | |
} else { | |
$sql .= $this->_buildSqlSingleKeyCondition($columnNames, count($params)); | |
$this->conn->exec($sql, $params); | |
} | |
// adjust state, remove from identity map and inform postDelete listeners | |
foreach ($deletedRecords as $record) { | |
// currently just for bc! | |
$this->_deleteCTIParents($table, $record); | |
//-- | |
$record->state(Doctrine_Record::STATE_TCLEAN); | |
$record->getTable()->removeRecord($record); | |
$this->_postDelete($record); | |
} | |
} | |
// trigger postDelete for records skipped during the deletion (veto!) | |
foreach ($deletions as $skippedRecord) { | |
$this->_postDelete($skippedRecord); | |
} | |
$this->conn->commit(); | |
return true; | |
} catch (Exception $e) { | |
$this->conn->rollback(); | |
throw $e; | |
} | |
} | |
/** | |
* Builds the SQL condition to target multiple records who have a single-column | |
* primary key. | |
* | |
* @param Doctrine_Table $table The table from which the records are going to be deleted. | |
* @param integer $numRecords The number of records that are going to be deleted. | |
* @return string The SQL condition "pk = ? OR pk = ? OR pk = ? ..." | |
*/ | |
private function _buildSqlSingleKeyCondition($columnNames, $numRecords) | |
{ | |
$idColumn = $this->conn->quoteIdentifier($columnNames[0]); | |
return implode(' OR ', array_fill(0, $numRecords, "$idColumn = ?")); | |
} | |
/** | |
* Builds the SQL condition to target multiple records who have a composite primary key. | |
* | |
* @param Doctrine_Table $table The table from which the records are going to be deleted. | |
* @param integer $numRecords The number of records that are going to be deleted. | |
* @return string The SQL condition "(pk1 = ? AND pk2 = ?) OR (pk1 = ? AND pk2 = ?) ..." | |
*/ | |
private function _buildSqlCompositeKeyCondition($columnNames, $numRecords) | |
{ | |
$singleCondition = ""; | |
foreach ($columnNames as $columnName) { | |
$columnName = $this->conn->quoteIdentifier($columnName); | |
if ($singleCondition === "") { | |
$singleCondition .= "($columnName = ?"; | |
} else { | |
$singleCondition .= " AND $columnName = ?"; | |
} | |
} | |
$singleCondition .= ")"; | |
$fullCondition = implode(' OR ', array_fill(0, $numRecords, $singleCondition)); | |
return $fullCondition; | |
} | |
/** | |
* Cascades an ongoing delete operation to related objects. Applies only on relations | |
* that have 'delete' in their cascade options. | |
* This is an application-level cascade. Related objects that participate in the | |
* cascade and are not yet loaded are fetched from the database. | |
* Exception: many-valued relations are always (re-)fetched from the database to | |
* make sure we have all of them. | |
* | |
* @param Doctrine_Record The record for which the delete operation will be cascaded. | |
* @throws PDOException If something went wrong at database level | |
* @return void | |
*/ | |
protected function _cascadeDelete(Doctrine_Record $record, array &$deletions) | |
{ | |
foreach ($record->getTable()->getRelations() as $relation) { | |
if ($relation->isCascadeDelete()) { | |
$fieldName = $relation->getAlias(); | |
// if it's a xToOne relation and the related object is already loaded | |
// we don't need to refresh. | |
if ( ! ($relation->getType() == Doctrine_Relation::ONE && isset($record->$fieldName))) { | |
$record->refreshRelated($relation->getAlias()); | |
} | |
$relatedObjects = $record->get($relation->getAlias()); | |
if ($relatedObjects instanceof Doctrine_Record && $relatedObjects->exists() | |
&& ! isset($deletions[$relatedObjects->getOid()])) { | |
$this->_collectDeletions($relatedObjects, $deletions); | |
} else if ($relatedObjects instanceof Doctrine_Collection && count($relatedObjects) > 0) { | |
// cascade the delete to the other objects | |
foreach ($relatedObjects as $object) { | |
if ( ! isset($deletions[$object->getOid()])) { | |
$this->_collectDeletions($object, $deletions); | |
} | |
} | |
} | |
} | |
} | |
} | |
/** | |
* saveRelatedForeignKeys | |
* saves all related (through ForeignKey) records to $record | |
* | |
* @throws PDOException if something went wrong at database level | |
* @param Doctrine_Record $record | |
*/ | |
public function saveRelatedForeignKeys(Doctrine_Record $record) | |
{ | |
$saveLater = array(); | |
foreach ($record->getReferences() as $k => $v) { | |
$rel = $record->getTable()->getRelation($k); | |
if ($rel instanceof Doctrine_Relation_ForeignKey) { | |
$saveLater[$k] = $rel; | |
} | |
} | |
return $saveLater; | |
} | |
/** | |
* saveRelatedLocalKeys | |
* saves all related (through LocalKey) records to $record | |
* | |
* @throws PDOException if something went wrong at database level | |
* @param Doctrine_Record $record | |
*/ | |
public function saveRelatedLocalKeys(Doctrine_Record $record) | |
{ | |
$state = $record->state(); | |
$record->state($record->exists() ? Doctrine_Record::STATE_LOCKED : Doctrine_Record::STATE_TLOCKED); | |
foreach ($record->getReferences() as $k => $v) { | |
$rel = $record->getTable()->getRelation($k); | |
$local = $rel->getLocal(); | |
$foreign = $rel->getForeign(); | |
if ($rel instanceof Doctrine_Relation_LocalKey) { | |
// ONE-TO-ONE relationship | |
$obj = $record->get($rel->getAlias()); | |
// Protection against infinite function recursion before attempting to save | |
if ($obj instanceof Doctrine_Record && $obj->isModified()) { | |
$obj->save($this->conn); | |
$id = array_values($obj->identifier()); | |
if ( ! empty($id)) { | |
foreach ((array) $rel->getLocal() as $k => $columnName) { | |
$field = $record->getTable()->getFieldName($columnName); | |
if (isset($id[$k]) && $id[$k] && $record->getTable()->hasField($field)) { | |
$record->set($field, $id[$k]); | |
} | |
} | |
} | |
} | |
} | |
} | |
$record->state($state); | |
} | |
/** | |
* saveAssociations | |
* | |
* this method takes a diff of one-to-many / many-to-many original and | |
* current collections and applies the changes | |
* | |
* for example if original many-to-many related collection has records with | |
* primary keys 1,2 and 3 and the new collection has records with primary keys | |
* 3, 4 and 5, this method would first destroy the associations to 1 and 2 and then | |
* save new associations to 4 and 5 | |
* | |
* @throws Doctrine_Connection_Exception if something went wrong at database level | |
* @param Doctrine_Record $record | |
* @return void | |
*/ | |
public function saveAssociations(Doctrine_Record $record) | |
{ | |
foreach ($record->getReferences() as $k => $v) { | |
$rel = $record->getTable()->getRelation($k); | |
if ($rel instanceof Doctrine_Relation_Association) { | |
if ($this->conn->getAttribute(Doctrine_Core::ATTR_CASCADE_SAVES) || $v->isModified()) { | |
$v->save($this->conn, false); | |
} | |
$assocTable = $rel->getAssociationTable(); | |
foreach ($v->getDeleteDiff() as $r) { | |
$query = 'DELETE FROM ' . $assocTable->getTableName() | |
. ' WHERE ' . $rel->getForeignRefColumnName() . ' = ?' | |
. ' AND ' . $rel->getLocalRefColumnName() . ' = ?'; | |
$this->conn->execute($query, array($r->getIncremented(), $record->getIncremented())); | |
} | |
foreach ($v->getInsertDiff() as $r) { | |
$assocRecord = $assocTable->create(); | |
$assocRecord->set($assocTable->getFieldName($rel->getForeign()), $r); | |
$assocRecord->set($assocTable->getFieldName($rel->getLocal()), $record); | |
$this->saveGraph($assocRecord); | |
} | |
// take snapshot of collection state, so that we know when its modified again | |
$v->takeSnapshot(); | |
} | |
} | |
} | |
/** | |
* Invokes preDelete event listeners. | |
* | |
* @return boolean Whether a listener has used it's veto (don't delete!). | |
*/ | |
private function _preDelete(Doctrine_Record $record) | |
{ | |
$event = new Doctrine_Event($record, Doctrine_Event::RECORD_DELETE); | |
$record->preDelete($event); | |
$record->getTable()->getRecordListener()->preDelete($event); | |
return $event->skipOperation; | |
} | |
/** | |
* Invokes postDelete event listeners. | |
*/ | |
private function _postDelete(Doctrine_Record $record) | |
{ | |
$event = new Doctrine_Event($record, Doctrine_Event::RECORD_DELETE); | |
$record->postDelete($event); | |
$record->getTable()->getRecordListener()->postDelete($event); | |
} | |
/** | |
* saveAll | |
* persists all the pending records from all tables | |
* | |
* @throws PDOException if something went wrong at database level | |
* @return void | |
*/ | |
public function saveAll() | |
{ | |
// get the flush tree | |
$tree = $this->buildFlushTree($this->conn->getTables()); | |
// save all records | |
foreach ($tree as $name) { | |
$table = $this->conn->getTable($name); | |
foreach ($table->getRepository() as $record) { | |
$this->saveGraph($record); | |
} | |
} | |
} | |
/** | |
* updates given record | |
* | |
* @param Doctrine_Record $record record to be updated | |
* @return boolean whether or not the update was successful | |
*/ | |
public function update(Doctrine_Record $record) | |
{ | |
$event = $record->invokeSaveHooks('pre', 'update');; | |
if ($record->isValid(false, false)) { | |
$table = $record->getTable(); | |
if ( ! $event->skipOperation) { | |
$identifier = $record->identifier(); | |
if ($table->getOption('joinedParents')) { | |
// currrently just for bc! | |
$this->_updateCTIRecord($table, $record); | |
//-- | |
} else { | |
$array = $record->getPrepared(); | |
$this->conn->update($table, $array, $identifier); | |
} | |
$record->assignIdentifier(true); | |
} | |
$record->invokeSaveHooks('post', 'update', $event); | |
return true; | |
} | |
return false; | |
} | |
/** | |
* Inserts a record into database. | |
* | |
* This method inserts a transient record in the database, and adds it | |
* to the identity map of its correspondent table. It proxies to @see | |
* processSingleInsert(), trigger insert hooks and validation of data | |
* if required. | |
* | |
* @param Doctrine_Record $record | |
* @return boolean false if record is not valid | |
*/ | |
public function insert(Doctrine_Record $record) | |
{ | |
$event = $record->invokeSaveHooks('pre', 'insert'); | |
if ($record->isValid(false, false)) { | |
$table = $record->getTable(); | |
if ( ! $event->skipOperation) { | |
if ($table->getOption('joinedParents')) { | |
// just for bc! | |
$this->_insertCTIRecord($table, $record); | |
//-- | |
} else { | |
$this->processSingleInsert($record); | |
} | |
} | |
$table->addRecord($record); | |
$record->invokeSaveHooks('post', 'insert', $event); | |
return true; | |
} | |
return false; | |
} | |
/** | |
* Replaces a record into database. | |
* | |
* @param Doctrine_Record $record | |
* @return boolean false if record is not valid | |
*/ | |
public function replace(Doctrine_Record $record) | |
{ | |
if ($record->exists()) { | |
return $this->update($record); | |
} else { | |
if ($record->isValid()) { | |
$this->_assignSequence($record); | |
$saveEvent = $record->invokeSaveHooks('pre', 'save'); | |
$insertEvent = $record->invokeSaveHooks('pre', 'insert'); | |
$table = $record->getTable(); | |
$identifier = (array) $table->getIdentifier(); | |
$data = $record->getPrepared(); | |
foreach ($data as $key => $value) { | |
if ($value instanceof Doctrine_Expression) { | |
$data[$key] = $value->getSql(); | |
} | |
} | |
$result = $this->conn->replace($table, $data, $identifier); | |
$record->invokeSaveHooks('post', 'insert', $insertEvent); | |
$record->invokeSaveHooks('post', 'save', $saveEvent); | |
$this->_assignIdentifier($record); | |
return true; | |
} else { | |
return false; | |
} | |
} | |
} | |
/** | |
* Inserts a transient record in its table. | |
* | |
* This method inserts the data of a single record in its assigned table, | |
* assigning to it the autoincrement primary key (if any is defined). | |
* | |
* @param Doctrine_Record $record | |
* @return void | |
*/ | |
public function processSingleInsert(Doctrine_Record $record) | |
{ | |
$fields = $record->getPrepared(); | |
$table = $record->getTable(); | |
// Populate fields with a blank array so that a blank records can be inserted | |
if (empty($fields)) { | |
foreach ($table->getFieldNames() as $field) { | |
$fields[$field] = null; | |
} | |
} | |
$this->_assignSequence($record, $fields); | |
$this->conn->insert($table, $fields); | |
$this->_assignIdentifier($record); | |
} | |
/** | |
* buildFlushTree | |
* builds a flush tree that is used in transactions | |
* | |
* The returned array has all the initialized components in | |
* 'correct' order. Basically this means that the records of those | |
* components can be saved safely in the order specified by the returned array. | |
* | |
* @param array $tables an array of Doctrine_Table objects or component names | |
* @return array an array of component names in flushing order | |
*/ | |
public function buildFlushTree(array $tables) | |
{ | |
// determine classes to order. only necessary because the $tables param | |
// can contain strings or table objects... | |
$classesToOrder = array(); | |
foreach ($tables as $table) { | |
if ( ! ($table instanceof Doctrine_Table)) { | |
$table = $this->conn->getTable($table, false); | |
} | |
$classesToOrder[] = $table->getComponentName(); | |
} | |
$classesToOrder = array_unique($classesToOrder); | |
if (count($classesToOrder) < 2) { | |
return $classesToOrder; | |
} | |
// build the correct order | |
$flushList = array(); | |
foreach ($classesToOrder as $class) { | |
$table = $this->conn->getTable($class, false); | |
$currentClass = $table->getComponentName(); | |
$index = array_search($currentClass, $flushList); | |
if ($index === false) { | |
//echo "adding $currentClass to flushlist"; | |
$flushList[] = $currentClass; | |
$index = max(array_keys($flushList)); | |
} | |
$rels = $table->getRelations(); | |
// move all foreignkey relations to the beginning | |
foreach ($rels as $key => $rel) { | |
if ($rel instanceof Doctrine_Relation_ForeignKey) { | |
unset($rels[$key]); | |
array_unshift($rels, $rel); | |
} | |
} | |
foreach ($rels as $rel) { | |
$relatedClassName = $rel->getTable()->getComponentName(); | |
if ( ! in_array($relatedClassName, $classesToOrder)) { | |
continue; | |
} | |
$relatedCompIndex = array_search($relatedClassName, $flushList); | |
$type = $rel->getType(); | |
// skip self-referenced relations | |
if ($relatedClassName === $currentClass) { | |
continue; | |
} | |
if ($rel instanceof Doctrine_Relation_ForeignKey) { | |
// the related component needs to come after this component in | |
// the list (since it holds the fk) | |
if ($relatedCompIndex !== false) { | |
// the component is already in the list | |
if ($relatedCompIndex >= $index) { | |
// it's already in the right place | |
continue; | |
} | |
unset($flushList[$index]); | |
// the related comp has the fk. so put "this" comp immediately | |
// before it in the list | |
array_splice($flushList, $relatedCompIndex, 0, $currentClass); | |
$index = $relatedCompIndex; | |
} else { | |
$flushList[] = $relatedClassName; | |
} | |
} else if ($rel instanceof Doctrine_Relation_LocalKey) { | |
// the related component needs to come before the current component | |
// in the list (since this component holds the fk). | |
if ($relatedCompIndex !== false) { | |
// already in flush list | |
if ($relatedCompIndex <= $index) { | |
// it's in the right place | |
continue; | |
} | |
unset($flushList[$relatedCompIndex]); | |
// "this" comp has the fk. so put the related comp before it | |
// in the list | |
array_splice($flushList, $index, 0, $relatedClassName); | |
} else { | |
array_unshift($flushList, $relatedClassName); | |
$index++; | |
} | |
} else if ($rel instanceof Doctrine_Relation_Association) { | |
// the association class needs to come after both classes | |
// that are connected through it in the list (since it holds | |
// both fks) | |
$assocTable = $rel->getAssociationFactory(); | |
$assocClassName = $assocTable->getComponentName(); | |
if ($relatedCompIndex !== false) { | |
unset($flushList[$relatedCompIndex]); | |
} | |
array_splice($flushList, $index, 0, $relatedClassName); | |
$index++; | |
$index3 = array_search($assocClassName, $flushList); | |
if ($index3 !== false) { | |
if ($index3 >= $index) { | |
continue; | |
} | |
unset($flushList[$index3]); | |
array_splice($flushList, $index - 1, 0, $assocClassName); | |
$index = $relatedCompIndex; | |
} else { | |
$flushList[] = $assocClassName; | |
} | |
} | |
} | |
} | |
return array_values($flushList); | |
} | |
/* The following is all the Class Table Inheritance specific code. Support dropped | |
for 0.10/1.0. */ | |
/** | |
* Class Table Inheritance code. | |
* Support dropped for 0.10/1.0. | |
* | |
* Note: This is flawed. We also need to delete from subclass tables. | |
*/ | |
private function _deleteCTIParents(Doctrine_Table $table, $record) | |
{ | |
if ($table->getOption('joinedParents')) { | |
foreach (array_reverse($table->getOption('joinedParents')) as $parent) { | |
$parentTable = $table->getConnection()->getTable($parent); | |
$this->conn->delete($parentTable, $record->identifier()); | |
} | |
} | |
} | |
/** | |
* Class Table Inheritance code. | |
* Support dropped for 0.10/1.0. | |
*/ | |
private function _insertCTIRecord(Doctrine_Table $table, Doctrine_Record $record) | |
{ | |
$dataSet = $this->_formatDataSet($record); | |
$component = $table->getComponentName(); | |
$classes = $table->getOption('joinedParents'); | |
$classes[] = $component; | |
foreach ($classes as $k => $parent) { | |
if ($k === 0) { | |
$rootRecord = new $parent(); | |
$rootRecord->merge($dataSet[$parent]); | |
$this->processSingleInsert($rootRecord); | |
$record->assignIdentifier($rootRecord->identifier()); | |
} else { | |
foreach ((array) $rootRecord->identifier() as $id => $value) { | |
$dataSet[$parent][$id] = $value; | |
} | |
$this->conn->insert($this->conn->getTable($parent), $dataSet[$parent]); | |
} | |
} | |
} | |
/** | |
* Class Table Inheritance code. | |
* Support dropped for 0.10/1.0. | |
*/ | |
private function _updateCTIRecord(Doctrine_Table $table, Doctrine_Record $record) | |
{ | |
$identifier = $record->identifier(); | |
$dataSet = $this->_formatDataSet($record); | |
$component = $table->getComponentName(); | |
$classes = $table->getOption('joinedParents'); | |
$classes[] = $component; | |
foreach ($record as $field => $value) { | |
if ($value instanceof Doctrine_Record) { | |
if ( ! $value->exists()) { | |
$value->save(); | |
} | |
$record->set($field, $value->getIncremented()); | |
} | |
} | |
foreach ($classes as $class) { | |
$parentTable = $this->conn->getTable($class); | |
if ( ! array_key_exists($class, $dataSet)) { | |
continue; | |
} | |
$this->conn->update($this->conn->getTable($class), $dataSet[$class], $identifier); | |
} | |
} | |
/** | |
* Class Table Inheritance code. | |
* Support dropped for 0.10/1.0. | |
*/ | |
private function _formatDataSet(Doctrine_Record $record) | |
{ | |
$table = $record->getTable(); | |
$dataSet = array(); | |
$component = $table->getComponentName(); | |
$array = $record->getPrepared(); | |
foreach ($table->getColumns() as $columnName => $definition) { | |
if ( ! isset($dataSet[$component])) { | |
$dataSet[$component] = array(); | |
} | |
if ( isset($definition['owner']) && ! isset($dataSet[$definition['owner']])) { | |
$dataSet[$definition['owner']] = array(); | |
} | |
$fieldName = $table->getFieldName($columnName); | |
if (isset($definition['primary']) && $definition['primary']) { | |
continue; | |
} | |
if ( ! array_key_exists($fieldName, $array)) { | |
continue; | |
} | |
if (isset($definition['owner'])) { | |
$dataSet[$definition['owner']][$fieldName] = $array[$fieldName]; | |
} else { | |
$dataSet[$component][$fieldName] = $array[$fieldName]; | |
} | |
} | |
return $dataSet; | |
} | |
/** | |
* Assigns sequence | |
* Manage specified/forced sequence value for table with a unique identifier field | |
* (i.e. multiple primary key / sequence can't be forced) | |
* | |
* @author Ludovic Fleury <ludovic.fleury@simple-it.fr> | |
*/ | |
protected function _assignSequence(Doctrine_Record $record, &$fields = null) | |
{ | |
// adds a record with a specific (forced) value for the identifier field | |
$tab = $record->toArray(); | |
if(count($record->getTable()->getIdentifierColumnNames()) == 1 | |
&& !empty($tab[$record->getTable()->getIdentifier()]) | |
&& $record->isNew()) { | |
$table = $record->getTable(); | |
$seq = $table->sequenceName; | |
if ( ! empty($seq)) { | |
# Update the sequence value if specified (forced) sequence is higher than the actual sequence | |
if($tab[$record->getTable()->getIdentifier()] > $this->conn->sequence->currId($seq)) { | |
$this->conn->sequence->initId($seq,$tab[$record->getTable()->getIdentifier()]); | |
} | |
} | |
return null; | |
} | |
else | |
{ | |
# original behavior | |
$table = $record->getTable(); | |
$seq = $table->sequenceName; | |
if ( ! empty($seq)) { | |
$id = $this->conn->sequence->nextId($seq); | |
$seqName = $table->getIdentifier(); | |
if ($fields) { | |
$fields[$seqName] = $id; | |
} | |
$record->assignIdentifier($id); | |
return $id; | |
} | |
} | |
} | |
protected function _assignIdentifier(Doctrine_Record $record) | |
{ | |
$table = $record->getTable(); | |
$identifier = $table->getIdentifier(); | |
$seq = $table->sequenceName; | |
if (empty($seq) && !is_array($identifier) && | |
$table->getIdentifierType() != Doctrine_Core::IDENTIFIER_NATURAL) { | |
$id = false; | |
if ($record->$identifier == null) { | |
if (($driver = strtolower($this->conn->getDriverName())) == 'pgsql') { | |
$seq = $table->getTableName() . '_' . $table->getColumnName($identifier); | |
} elseif ($driver == 'oracle' || $driver == 'mssql') { | |
$seq = $table->getTableName(); | |
} | |
$id = $this->conn->sequence->lastInsertId($seq); | |
} else { | |
$id = $record->$identifier; | |
} | |
if ( ! $id) { | |
throw new Doctrine_Connection_Exception("Couldn't get last insert identifier."); | |
} | |
$record->assignIdentifier($id); | |
} else { | |
$record->assignIdentifier(true); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment