Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/*
3
 *  $Id: UnitOfWork.php 7684 2010-08-24 16:34:16Z 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.doctrine-project.org>.
20
 */
21
 
22
/**
23
 * Doctrine_Connection_UnitOfWork
24
 *
25
 * Note: This class does not have the semantics of a real "Unit of Work" in 0.10/1.0.
26
 * Database operations are not queued. All changes to objects are immediately written
27
 * to the database. You can think of it as a unit of work in auto-flush mode.
28
 *
29
 * Referential integrity is currently not always ensured.
30
 *
31
 * @package     Doctrine
32
 * @subpackage  Connection
33
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
34
 * @link        www.doctrine-project.org
35
 * @since       1.0
36
 * @version     $Revision: 7684 $
37
 * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
38
 * @author      Roman Borschel <roman@code-factory.org>
39
 */
40
class Doctrine_Connection_UnitOfWork extends Doctrine_Connection_Module
41
{
42
    /**
43
     * Saves the given record and all associated records.
44
     * (The save() operation is always cascaded in 0.10/1.0).
45
     *
46
     * @param Doctrine_Record $record
47
     * @return void
48
     */
49
    public function saveGraph(Doctrine_Record $record, $replace = false)
50
    {
51
        $record->assignInheritanceValues();
52
 
53
        $conn = $this->getConnection();
54
        $conn->connect();
55
 
56
        $state = $record->state();
57
        if ($state === Doctrine_Record::STATE_LOCKED || $state === Doctrine_Record::STATE_TLOCKED) {
58
            return false;
59
        }
60
 
61
        $record->state($record->exists() ? Doctrine_Record::STATE_LOCKED : Doctrine_Record::STATE_TLOCKED);
62
 
63
        try {
64
            $conn->beginInternalTransaction();
65
            $record->state($state);
66
 
67
            $event = $record->invokeSaveHooks('pre', 'save');
68
            $state = $record->state();
69
 
70
            $isValid = true;
71
 
72
            if ( ! $event->skipOperation) {
73
                $this->saveRelatedLocalKeys($record);
74
 
75
                switch ($state) {
76
                    case Doctrine_Record::STATE_TDIRTY:
77
                    case Doctrine_Record::STATE_TCLEAN:
78
                        if ($replace) {
79
                            $isValid = $this->replace($record);
80
                        } else {
81
                            $isValid = $this->insert($record);
82
                        }
83
                        break;
84
                    case Doctrine_Record::STATE_DIRTY:
85
                    case Doctrine_Record::STATE_PROXY:
86
                        if ($replace) {
87
                            $isValid = $this->replace($record);
88
                        } else {
89
                            $isValid = $this->update($record);
90
                        }
91
                        break;
92
                    case Doctrine_Record::STATE_CLEAN:
93
                        // do nothing
94
                        break;
95
                }
96
 
97
                $aliasesUnlinkInDb = array();
98
 
99
                if ($isValid) {
100
                    // NOTE: what about referential integrity issues?
101
                    foreach ($record->getPendingDeletes() as $pendingDelete) {
102
                        $pendingDelete->delete();
103
                    }
104
 
105
                    foreach ($record->getPendingUnlinks() as $alias => $ids) {
106
                        if ($ids === false) {
107
                            $record->unlinkInDb($alias, array());
108
                            $aliasesUnlinkInDb[] = $alias;
109
                        } else if ($ids) {
110
                            $record->unlinkInDb($alias, array_keys($ids));
111
                            $aliasesUnlinkInDb[] = $alias;
112
                        }
113
                    }
114
                    $record->resetPendingUnlinks();
115
 
116
                    $record->invokeSaveHooks('post', 'save', $event);
117
                } else {
118
                    $conn->transaction->addInvalid($record);
119
                }
120
 
121
                $state = $record->state();
122
 
123
                $record->state($record->exists() ? Doctrine_Record::STATE_LOCKED : Doctrine_Record::STATE_TLOCKED);
124
 
125
                if ($isValid) {
126
                    $saveLater = $this->saveRelatedForeignKeys($record);
127
                    foreach ($saveLater as $fk) {
128
                        $alias = $fk->getAlias();
129
 
130
                        if ($record->hasReference($alias)) {
131
                            $obj = $record->$alias;
132
 
133
                            // check that the related object is not an instance of Doctrine_Null
134
                            if ($obj && ! ($obj instanceof Doctrine_Null)) {
135
                                $processDiff = !in_array($alias, $aliasesUnlinkInDb);
136
                                $obj->save($conn, $processDiff);
137
                            }
138
                        }
139
                    }
140
 
141
                    // save the MANY-TO-MANY associations
142
                    $this->saveAssociations($record);
143
                }
144
            }
145
 
146
            $record->state($state);
147
 
148
            $conn->commit();
149
        } catch (Exception $e) {
150
            // Make sure we roll back our internal transaction
151
            //$record->state($state);
152
            $conn->rollback();
153
            throw $e;
154
        }
155
 
156
        $record->clearInvokedSaveHooks();
157
 
158
        return true;
159
    }
160
 
161
    /**
162
     * Deletes the given record and all the related records that participate
163
     * in an application-level delete cascade.
164
     *
165
     * this event can be listened by the onPreDelete and onDelete listeners
166
     *
167
     * @return boolean      true on success, false on failure
168
     */
169
    public function delete(Doctrine_Record $record)
170
    {
171
        $deletions = array();
172
        $this->_collectDeletions($record, $deletions);
173
        return $this->_executeDeletions($deletions);
174
    }
175
 
176
    /**
177
     * Collects all records that need to be deleted by applying defined
178
     * application-level delete cascades.
179
     *
180
     * @param array $deletions  Map of the records to delete. Keys=Oids Values=Records.
181
     */
182
    private function _collectDeletions(Doctrine_Record $record, array &$deletions)
183
    {
184
        if ( ! $record->exists()) {
185
            return;
186
        }
187
 
188
        $deletions[$record->getOid()] = $record;
189
        $this->_cascadeDelete($record, $deletions);
190
    }
191
 
192
    /**
193
     * Executes the deletions for all collected records during a delete operation
194
     * (usually triggered through $record->delete()).
195
     *
196
     * @param array $deletions  Map of the records to delete. Keys=Oids Values=Records.
197
     */
198
    private function _executeDeletions(array $deletions)
199
    {
200
        // collect class names
201
        $classNames = array();
202
        foreach ($deletions as $record) {
203
            $classNames[] = $record->getTable()->getComponentName();
204
        }
205
        $classNames = array_unique($classNames);
206
 
207
        // order deletes
208
        $executionOrder = $this->buildFlushTree($classNames);
209
 
210
        // execute
211
        try {
212
            $this->conn->beginInternalTransaction();
213
 
214
            for ($i = count($executionOrder) - 1; $i >= 0; $i--) {
215
                $className = $executionOrder[$i];
216
                $table = $this->conn->getTable($className);
217
 
218
                // collect identifiers
219
                $identifierMaps = array();
220
                $deletedRecords = array();
221
                foreach ($deletions as $oid => $record) {
222
                    if ($record->getTable()->getComponentName() == $className) {
223
                        $veto = $this->_preDelete($record);
224
                        if ( ! $veto) {
225
                            $identifierMaps[] = $record->identifier();
226
                            $deletedRecords[] = $record;
227
                            unset($deletions[$oid]);
228
                        }
229
                    }
230
                }
231
 
232
                if (count($deletedRecords) < 1) {
233
                    continue;
234
                }
235
 
236
                // extract query parameters (only the identifier values are of interest)
237
                $params = array();
238
                $columnNames = array();
239
                foreach ($identifierMaps as $idMap) {
240
                    while (list($fieldName, $value) = each($idMap)) {
241
                        $params[] = $value;
242
                        $columnNames[] = $table->getColumnName($fieldName);
243
                    }
244
                }
245
                $columnNames = array_unique($columnNames);
246
 
247
                // delete
248
                $tableName = $table->getTableName();
249
                $sql = "DELETE FROM " . $this->conn->quoteIdentifier($tableName) . " WHERE ";
250
 
251
                if ($table->isIdentifierComposite()) {
252
                    $sql .= $this->_buildSqlCompositeKeyCondition($columnNames, count($identifierMaps));
253
                    $this->conn->exec($sql, $params);
254
                } else {
255
                    $sql .= $this->_buildSqlSingleKeyCondition($columnNames, count($params));
256
                    $this->conn->exec($sql, $params);
257
                }
258
 
259
                // adjust state, remove from identity map and inform postDelete listeners
260
                foreach ($deletedRecords as $record) {
261
                    // currently just for bc!
262
                    $this->_deleteCTIParents($table, $record);
263
                    //--
264
                    $record->state(Doctrine_Record::STATE_TCLEAN);
265
                    $record->getTable()->removeRecord($record);
266
                    $this->_postDelete($record);
267
                }
268
            }
269
 
270
            // trigger postDelete for records skipped during the deletion (veto!)
271
            foreach ($deletions as $skippedRecord) {
272
                $this->_postDelete($skippedRecord);
273
            }
274
 
275
            $this->conn->commit();
276
 
277
            return true;
278
        } catch (Exception $e) {
279
            $this->conn->rollback();
280
            throw $e;
281
        }
282
    }
283
 
284
    /**
285
     * Builds the SQL condition to target multiple records who have a single-column
286
     * primary key.
287
     *
288
     * @param Doctrine_Table $table  The table from which the records are going to be deleted.
289
     * @param integer $numRecords  The number of records that are going to be deleted.
290
     * @return string  The SQL condition "pk = ? OR pk = ? OR pk = ? ..."
291
     */
292
    private function _buildSqlSingleKeyCondition($columnNames, $numRecords)
293
    {
294
        $idColumn = $this->conn->quoteIdentifier($columnNames[0]);
295
        return implode(' OR ', array_fill(0, $numRecords, "$idColumn = ?"));
296
    }
297
 
298
    /**
299
     * Builds the SQL condition to target multiple records who have a composite primary key.
300
     *
301
     * @param Doctrine_Table $table  The table from which the records are going to be deleted.
302
     * @param integer $numRecords  The number of records that are going to be deleted.
303
     * @return string  The SQL condition "(pk1 = ? AND pk2 = ?) OR (pk1 = ? AND pk2 = ?) ..."
304
     */
305
    private function _buildSqlCompositeKeyCondition($columnNames, $numRecords)
306
    {
307
        $singleCondition = "";
308
        foreach ($columnNames as $columnName) {
309
            $columnName = $this->conn->quoteIdentifier($columnName);
310
            if ($singleCondition === "") {
311
                $singleCondition .= "($columnName = ?";
312
            } else {
313
                $singleCondition .= " AND $columnName = ?";
314
            }
315
        }
316
        $singleCondition .= ")";
317
        $fullCondition = implode(' OR ', array_fill(0, $numRecords, $singleCondition));
318
 
319
        return $fullCondition;
320
    }
321
 
322
    /**
323
     * Cascades an ongoing delete operation to related objects. Applies only on relations
324
     * that have 'delete' in their cascade options.
325
     * This is an application-level cascade. Related objects that participate in the
326
     * cascade and are not yet loaded are fetched from the database.
327
     * Exception: many-valued relations are always (re-)fetched from the database to
328
     * make sure we have all of them.
329
     *
330
     * @param Doctrine_Record  The record for which the delete operation will be cascaded.
331
     * @throws PDOException    If something went wrong at database level
332
     * @return void
333
     */
334
     protected function _cascadeDelete(Doctrine_Record $record, array &$deletions)
335
     {
336
         foreach ($record->getTable()->getRelations() as $relation) {
337
             if ($relation->isCascadeDelete()) {
338
                 $fieldName = $relation->getAlias();
339
                 // if it's a xToOne relation and the related object is already loaded
340
                 // we don't need to refresh.
341
                 if ( ! ($relation->getType() == Doctrine_Relation::ONE && isset($record->$fieldName))) {
342
                     $record->refreshRelated($relation->getAlias());
343
                 }
344
                 $relatedObjects = $record->get($relation->getAlias());
345
                 if ($relatedObjects instanceof Doctrine_Record && $relatedObjects->exists()
346
                        && ! isset($deletions[$relatedObjects->getOid()])) {
347
                     $this->_collectDeletions($relatedObjects, $deletions);
348
                 } else if ($relatedObjects instanceof Doctrine_Collection && count($relatedObjects) > 0) {
349
                     // cascade the delete to the other objects
350
                     foreach ($relatedObjects as $object) {
351
                         if ( ! isset($deletions[$object->getOid()])) {
352
                             $this->_collectDeletions($object, $deletions);
353
                         }
354
                     }
355
                 }
356
             }
357
         }
358
     }
359
 
360
    /**
361
     * saveRelatedForeignKeys
362
     * saves all related (through ForeignKey) records to $record
363
     *
364
     * @throws PDOException         if something went wrong at database level
365
     * @param Doctrine_Record $record
366
     */
367
    public function saveRelatedForeignKeys(Doctrine_Record $record)
368
    {
369
        $saveLater = array();
370
        foreach ($record->getReferences() as $k => $v) {
371
            $rel = $record->getTable()->getRelation($k);
372
            if ($rel instanceof Doctrine_Relation_ForeignKey) {
373
                $saveLater[$k] = $rel;
374
            }
375
        }
376
 
377
        return $saveLater;
378
    }
379
 
380
    /**
381
     * saveRelatedLocalKeys
382
     * saves all related (through LocalKey) records to $record
383
     *
384
     * @throws PDOException         if something went wrong at database level
385
     * @param Doctrine_Record $record
386
     */
387
    public function saveRelatedLocalKeys(Doctrine_Record $record)
388
    {
389
        $state = $record->state();
390
        $record->state($record->exists() ? Doctrine_Record::STATE_LOCKED : Doctrine_Record::STATE_TLOCKED);
391
 
392
        foreach ($record->getReferences() as $k => $v) {
393
            $rel = $record->getTable()->getRelation($k);
394
 
395
            $local = $rel->getLocal();
396
            $foreign = $rel->getForeign();
397
 
398
            if ($rel instanceof Doctrine_Relation_LocalKey) {
399
                // ONE-TO-ONE relationship
400
                $obj = $record->get($rel->getAlias());
401
 
402
                // Protection against infinite function recursion before attempting to save
403
                if ($obj instanceof Doctrine_Record && $obj->isModified()) {
404
                    $obj->save($this->conn);
405
 
406
                    $id = array_values($obj->identifier());
407
 
408
                    if ( ! empty($id)) {
409
                        foreach ((array) $rel->getLocal() as $k => $columnName) {
410
                            $field = $record->getTable()->getFieldName($columnName);
411
 
412
                            if (isset($id[$k]) && $id[$k] && $record->getTable()->hasField($field)) {
413
                                $record->set($field, $id[$k]);
414
                            }
415
                        }
416
                    }
417
                }
418
            }
419
        }
420
        $record->state($state);
421
    }
422
 
423
    /**
424
     * saveAssociations
425
     *
426
     * this method takes a diff of one-to-many / many-to-many original and
427
     * current collections and applies the changes
428
     *
429
     * for example if original many-to-many related collection has records with
430
     * primary keys 1,2 and 3 and the new collection has records with primary keys
431
     * 3, 4 and 5, this method would first destroy the associations to 1 and 2 and then
432
     * save new associations to 4 and 5
433
     *
434
     * @throws Doctrine_Connection_Exception         if something went wrong at database level
435
     * @param Doctrine_Record $record
436
     * @return void
437
     */
438
    public function saveAssociations(Doctrine_Record $record)
439
    {
440
        foreach ($record->getReferences() as $k => $v) {
441
            $rel = $record->getTable()->getRelation($k);
442
 
443
            if ($rel instanceof Doctrine_Relation_Association) {
444
                if ($this->conn->getAttribute(Doctrine_Core::ATTR_CASCADE_SAVES) || $v->isModified()) {
445
                    $v->save($this->conn, false);
446
                }
447
 
448
                $assocTable = $rel->getAssociationTable();
449
                foreach ($v->getDeleteDiff() as $r) {
450
                    $query = 'DELETE FROM ' . $assocTable->getTableName()
451
                           . ' WHERE ' . $rel->getForeignRefColumnName() . ' = ?'
452
                           . ' AND ' . $rel->getLocalRefColumnName() . ' = ?';
453
 
454
                    $this->conn->execute($query, array($r->getIncremented(), $record->getIncremented()));
455
                }
456
 
457
                foreach ($v->getInsertDiff() as $r) {
458
                    $assocRecord = $assocTable->create();
459
                    $assocRecord->set($assocTable->getFieldName($rel->getForeign()), $r);
460
                    $assocRecord->set($assocTable->getFieldName($rel->getLocal()), $record);
461
                    $this->saveGraph($assocRecord);
462
                }
463
                // take snapshot of collection state, so that we know when its modified again
464
                $v->takeSnapshot();
465
            }
466
        }
467
    }
468
 
469
    /**
470
     * Invokes preDelete event listeners.
471
     *
472
     * @return boolean  Whether a listener has used it's veto (don't delete!).
473
     */
474
    private function _preDelete(Doctrine_Record $record)
475
    {
476
        $event = new Doctrine_Event($record, Doctrine_Event::RECORD_DELETE);
477
        $record->preDelete($event);
478
        $record->getTable()->getRecordListener()->preDelete($event);
479
 
480
        return $event->skipOperation;
481
    }
482
 
483
    /**
484
     * Invokes postDelete event listeners.
485
     */
486
    private function _postDelete(Doctrine_Record $record)
487
    {
488
        $event = new Doctrine_Event($record, Doctrine_Event::RECORD_DELETE);
489
        $record->postDelete($event);
490
        $record->getTable()->getRecordListener()->postDelete($event);
491
    }
492
 
493
    /**
494
     * saveAll
495
     * persists all the pending records from all tables
496
     *
497
     * @throws PDOException         if something went wrong at database level
498
     * @return void
499
     */
500
    public function saveAll()
501
    {
502
        // get the flush tree
503
        $tree = $this->buildFlushTree($this->conn->getTables());
504
 
505
        // save all records
506
        foreach ($tree as $name) {
507
            $table = $this->conn->getTable($name);
508
            foreach ($table->getRepository() as $record) {
509
                $this->saveGraph($record);
510
            }
511
        }
512
    }
513
 
514
    /**
515
     * updates given record
516
     *
517
     * @param Doctrine_Record $record   record to be updated
518
     * @return boolean                  whether or not the update was successful
519
     */
520
    public function update(Doctrine_Record $record)
521
    {
522
        $event = $record->invokeSaveHooks('pre', 'update');;
523
 
524
        if ($record->isValid(false, false)) {
525
            $table = $record->getTable();
526
            if ( ! $event->skipOperation) {
527
                $identifier = $record->identifier();
528
                if ($table->getOption('joinedParents')) {
529
                    // currrently just for bc!
530
                    $this->_updateCTIRecord($table, $record);
531
                    //--
532
                } else {
533
                    $array = $record->getPrepared();
534
                    $this->conn->update($table, $array, $identifier);
535
                }
536
                $record->assignIdentifier(true);
537
            }
538
 
539
            $record->invokeSaveHooks('post', 'update', $event);
540
 
541
            return true;
542
        }
543
 
544
        return false;
545
    }
546
 
547
    /**
548
     * Inserts a record into database.
549
     *
550
     * This method inserts a transient record in the database, and adds it
551
     * to the identity map of its correspondent table. It proxies to @see
552
     * processSingleInsert(), trigger insert hooks and validation of data
553
     * if required.
554
     *
555
     * @param Doctrine_Record $record
556
     * @return boolean                  false if record is not valid
557
     */
558
    public function insert(Doctrine_Record $record)
559
    {
560
        $event = $record->invokeSaveHooks('pre', 'insert');
561
 
562
        if ($record->isValid(false, false)) {
563
            $table = $record->getTable();
564
 
565
            if ( ! $event->skipOperation) {
566
                if ($table->getOption('joinedParents')) {
567
                    // just for bc!
568
                    $this->_insertCTIRecord($table, $record);
569
                    //--
570
                } else {
571
                    $this->processSingleInsert($record);
572
                }
573
            }
574
 
575
            $table->addRecord($record);
576
            $record->invokeSaveHooks('post', 'insert', $event);
577
 
578
            return true;
579
        }
580
 
581
        return false;
582
    }
583
 
584
    /**
585
     * Replaces a record into database.
586
     *
587
     * @param Doctrine_Record $record
588
     * @return boolean                  false if record is not valid
589
     */
590
    public function replace(Doctrine_Record $record)
591
    {
592
        if ($record->exists()) {
593
            return $this->update($record);
594
        } else {
595
            if ($record->isValid()) {
596
                $this->_assignSequence($record);
597
 
598
                $saveEvent = $record->invokeSaveHooks('pre', 'save');
599
                $insertEvent = $record->invokeSaveHooks('pre', 'insert');
600
 
601
                $table = $record->getTable();
602
                $identifier = (array) $table->getIdentifier();
603
                $data = $record->getPrepared();
604
 
605
                foreach ($data as $key  => $value) {
606
                    if ($value instanceof Doctrine_Expression) {
607
                        $data[$key] = $value->getSql();
608
                    }
609
                }
610
 
611
                $result = $this->conn->replace($table, $data, $identifier);
612
 
613
                $record->invokeSaveHooks('post', 'insert', $insertEvent);
614
                $record->invokeSaveHooks('post', 'save', $saveEvent);
615
 
616
                $this->_assignIdentifier($record);
617
 
618
                return true;
619
            } else {
620
                return false;
621
            }
622
        }
623
    }
624
 
625
    /**
626
     * Inserts a transient record in its table.
627
     *
628
     * This method inserts the data of a single record in its assigned table,
629
     * assigning to it the autoincrement primary key (if any is defined).
630
     *
631
     * @param Doctrine_Record $record
632
     * @return void
633
     */
634
    public function processSingleInsert(Doctrine_Record $record)
635
    {
636
        $fields = $record->getPrepared();
637
        $table = $record->getTable();
638
 
639
        // Populate fields with a blank array so that a blank records can be inserted
640
        if (empty($fields)) {
641
            foreach ($table->getFieldNames() as $field) {
642
                $fields[$field] = null;
643
            }
644
        }
645
 
646
        $this->_assignSequence($record, $fields);
647
        $this->conn->insert($table, $fields);
648
        $this->_assignIdentifier($record);
649
    }
650
 
651
    /**
652
     * buildFlushTree
653
     * builds a flush tree that is used in transactions
654
     *
655
     * The returned array has all the initialized components in
656
     * 'correct' order. Basically this means that the records of those
657
     * components can be saved safely in the order specified by the returned array.
658
     *
659
     * @param array $tables     an array of Doctrine_Table objects or component names
660
     * @return array            an array of component names in flushing order
661
     */
662
    public function buildFlushTree(array $tables)
663
    {
664
        // determine classes to order. only necessary because the $tables param
665
        // can contain strings or table objects...
666
        $classesToOrder = array();
667
        foreach ($tables as $table) {
668
            if ( ! ($table instanceof Doctrine_Table)) {
669
                $table = $this->conn->getTable($table, false);
670
            }
671
            $classesToOrder[] = $table->getComponentName();
672
        }
673
        $classesToOrder = array_unique($classesToOrder);
674
 
675
        if (count($classesToOrder) < 2) {
676
            return $classesToOrder;
677
        }
678
 
679
        // build the correct order
680
        $flushList = array();
681
        foreach ($classesToOrder as $class) {
682
            $table = $this->conn->getTable($class, false);
683
            $currentClass = $table->getComponentName();
684
 
685
            $index = array_search($currentClass, $flushList);
686
 
687
            if ($index === false) {
688
                //echo "adding $currentClass to flushlist";
689
                $flushList[] = $currentClass;
690
                $index = max(array_keys($flushList));
691
            }
692
 
693
            $rels = $table->getRelations();
694
 
695
            // move all foreignkey relations to the beginning
696
            foreach ($rels as $key => $rel) {
697
                if ($rel instanceof Doctrine_Relation_ForeignKey) {
698
                    unset($rels[$key]);
699
                    array_unshift($rels, $rel);
700
                }
701
            }
702
 
703
            foreach ($rels as $rel) {
704
                $relatedClassName = $rel->getTable()->getComponentName();
705
 
706
                if ( ! in_array($relatedClassName, $classesToOrder)) {
707
                    continue;
708
                }
709
 
710
                $relatedCompIndex = array_search($relatedClassName, $flushList);
711
                $type = $rel->getType();
712
 
713
                // skip self-referenced relations
714
                if ($relatedClassName === $currentClass) {
715
                    continue;
716
                }
717
 
718
                if ($rel instanceof Doctrine_Relation_ForeignKey) {
719
                    // the related component needs to come after this component in
720
                    // the list (since it holds the fk)
721
 
722
                    if ($relatedCompIndex !== false) {
723
                        // the component is already in the list
724
                        if ($relatedCompIndex >= $index) {
725
                            // it's already in the right place
726
                            continue;
727
                        }
728
 
729
                        unset($flushList[$index]);
730
                        // the related comp has the fk. so put "this" comp immediately
731
                        // before it in the list
732
                        array_splice($flushList, $relatedCompIndex, 0, $currentClass);
733
                        $index = $relatedCompIndex;
734
                    } else {
735
                        $flushList[] = $relatedClassName;
736
                    }
737
 
738
                } else if ($rel instanceof Doctrine_Relation_LocalKey) {
739
                    // the related component needs to come before the current component
740
                    // in the list (since this component holds the fk).
741
 
742
                    if ($relatedCompIndex !== false) {
743
                        // already in flush list
744
                        if ($relatedCompIndex <= $index) {
745
                            // it's in the right place
746
                            continue;
747
                        }
748
 
749
                        unset($flushList[$relatedCompIndex]);
750
                        // "this" comp has the fk. so put the related comp before it
751
                        // in the list
752
                        array_splice($flushList, $index, 0, $relatedClassName);
753
                    } else {
754
                        array_unshift($flushList, $relatedClassName);
755
                        $index++;
756
                    }
757
                } else if ($rel instanceof Doctrine_Relation_Association) {
758
                    // the association class needs to come after both classes
759
                    // that are connected through it in the list (since it holds
760
                    // both fks)
761
 
762
                    $assocTable = $rel->getAssociationFactory();
763
                    $assocClassName = $assocTable->getComponentName();
764
 
765
                    if ($relatedCompIndex !== false) {
766
                        unset($flushList[$relatedCompIndex]);
767
                    }
768
 
769
                    array_splice($flushList, $index, 0, $relatedClassName);
770
                    $index++;
771
 
772
                    $index3 = array_search($assocClassName, $flushList);
773
 
774
                    if ($index3 !== false) {
775
                        if ($index3 >= $index) {
776
                            continue;
777
                        }
778
 
779
                        unset($flushList[$index3]);
780
                        array_splice($flushList, $index - 1, 0, $assocClassName);
781
                        $index = $relatedCompIndex;
782
                    } else {
783
                        $flushList[] = $assocClassName;
784
                    }
785
                }
786
            }
787
        }
788
 
789
        return array_values($flushList);
790
    }
791
 
792
 
793
    /* The following is all the Class Table Inheritance specific code. Support dropped
794
       for 0.10/1.0. */
795
 
796
    /**
797
     * Class Table Inheritance code.
798
     * Support dropped for 0.10/1.0.
799
     *
800
     * Note: This is flawed. We also need to delete from subclass tables.
801
     */
802
    private function _deleteCTIParents(Doctrine_Table $table, $record)
803
    {
804
        if ($table->getOption('joinedParents')) {
805
            foreach (array_reverse($table->getOption('joinedParents')) as $parent) {
806
                $parentTable = $table->getConnection()->getTable($parent);
807
                $this->conn->delete($parentTable, $record->identifier());
808
            }
809
        }
810
    }
811
 
812
    /**
813
     * Class Table Inheritance code.
814
     * Support dropped for 0.10/1.0.
815
     */
816
    private function _insertCTIRecord(Doctrine_Table $table, Doctrine_Record $record)
817
    {
818
        $dataSet = $this->_formatDataSet($record);
819
        $component = $table->getComponentName();
820
 
821
        $classes = $table->getOption('joinedParents');
822
        $classes[] = $component;
823
 
824
        foreach ($classes as $k => $parent) {
825
            if ($k === 0) {
826
                $rootRecord = new $parent();
827
                $rootRecord->merge($dataSet[$parent]);
828
                $this->processSingleInsert($rootRecord);
829
                $record->assignIdentifier($rootRecord->identifier());
830
            } else {
831
                foreach ((array) $rootRecord->identifier() as $id => $value) {
832
                    $dataSet[$parent][$id] = $value;
833
                }
834
 
835
                $this->conn->insert($this->conn->getTable($parent), $dataSet[$parent]);
836
            }
837
        }
838
    }
839
 
840
    /**
841
     * Class Table Inheritance code.
842
     * Support dropped for 0.10/1.0.
843
     */
844
    private function _updateCTIRecord(Doctrine_Table $table, Doctrine_Record $record)
845
    {
846
        $identifier = $record->identifier();
847
        $dataSet = $this->_formatDataSet($record);
848
 
849
        $component = $table->getComponentName();
850
 
851
        $classes = $table->getOption('joinedParents');
852
        $classes[] = $component;
853
 
854
        foreach ($record as $field => $value) {
855
            if ($value instanceof Doctrine_Record) {
856
                if ( ! $value->exists()) {
857
                    $value->save();
858
                }
859
                $record->set($field, $value->getIncremented());
860
            }
861
        }
862
 
863
        foreach ($classes as $class) {
864
            $parentTable = $this->conn->getTable($class);
865
 
866
            if ( ! array_key_exists($class, $dataSet)) {
867
                continue;
868
            }
869
 
870
            $this->conn->update($this->conn->getTable($class), $dataSet[$class], $identifier);
871
        }
872
    }
873
 
874
    /**
875
     * Class Table Inheritance code.
876
     * Support dropped for 0.10/1.0.
877
     */
878
    private function _formatDataSet(Doctrine_Record $record)
879
    {
880
        $table = $record->getTable();
881
        $dataSet = array();
882
        $component = $table->getComponentName();
883
        $array = $record->getPrepared();
884
 
885
        foreach ($table->getColumns() as $columnName => $definition) {
886
            if ( ! isset($dataSet[$component])) {
887
                $dataSet[$component] = array();
888
            }
889
 
890
            if ( isset($definition['owner']) && ! isset($dataSet[$definition['owner']])) {
891
                $dataSet[$definition['owner']] = array();
892
            }
893
 
894
            $fieldName = $table->getFieldName($columnName);
895
            if (isset($definition['primary']) && $definition['primary']) {
896
                continue;
897
            }
898
 
899
            if ( ! array_key_exists($fieldName, $array)) {
900
                continue;
901
            }
902
 
903
            if (isset($definition['owner'])) {
904
                $dataSet[$definition['owner']][$fieldName] = $array[$fieldName];
905
            } else {
906
                $dataSet[$component][$fieldName] = $array[$fieldName];
907
            }
908
        }
909
 
910
        return $dataSet;
911
    }
912
 
913
    protected function _assignSequence(Doctrine_Record $record, &$fields = null)
914
    {
915
        $table = $record->getTable();
916
        $seq = $table->sequenceName;
917
 
918
        if ( ! empty($seq)) {
919
            $id = $this->conn->sequence->nextId($seq);
920
            $seqName = $table->getIdentifier();
921
            if ($fields) {
922
                $fields[$seqName] = $id;
923
            }
924
 
925
            $record->assignIdentifier($id);
926
 
927
            return $id;
928
        }
929
    }
930
 
931
    protected function _assignIdentifier(Doctrine_Record $record)
932
    {
933
        $table = $record->getTable();
934
        $identifier = $table->getIdentifier();
935
        $seq = $table->sequenceName;
936
 
937
        if (empty($seq) && !is_array($identifier) &&
938
            $table->getIdentifierType() != Doctrine_Core::IDENTIFIER_NATURAL) {
939
            $id = false;
940
            if ($record->$identifier == null) {
941
                if (($driver = strtolower($this->conn->getDriverName())) == 'pgsql') {
942
                    $seq = $table->getTableName() . '_' . $table->getColumnName($identifier);
943
                } elseif ($driver == 'oracle' || $driver == 'mssql') {
944
                    $seq = $table->getTableName();
945
                }
946
 
947
                $id = $this->conn->sequence->lastInsertId($seq);
948
            } else {
949
                $id = $record->$identifier;
950
            }
951
 
952
            if ( ! $id) {
953
                throw new Doctrine_Connection_Exception("Couldn't get last insert identifier.");
954
            }
955
            $record->assignIdentifier($id);
956
        } else {
957
            $record->assignIdentifier(true);
958
        }
959
    }
960
}