Coverage for Doctrine_Collection

Back to coverage report

1 <?php
2 /*
3  *  $Id: Collection.php 2963 2007-10-21 06:23:59Z Jonathan.Wage $
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.com>.
20  */
21 Doctrine::autoload('Doctrine_Access');
22 /**
23  * Doctrine_Collection
24  * Collection of Doctrine_Record objects.
25  *
26  * @package     Doctrine
27  * @subpackage  Collection
28  * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
29  * @link        www.phpdoctrine.com
30  * @since       1.0
31  * @version     $Revision: 2963 $
32  * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
33  */
34 class Doctrine_Collection extends Doctrine_Access implements Countable, IteratorAggregate, Serializable
35 {
36     /**
37      * @var array $data                     an array containing the records of this collection
38      */
39     protected $data = array();
40
41     /**
42      * @var Doctrine_Table $table           each collection has only records of specified table
43      */
44     protected $_table;
45
46     /**
47      * @var array $_snapshot                a snapshot of the fetched data
48      */
49     protected $_snapshot = array();
50
51     /**
52      * @var Doctrine_Record $reference      collection can belong to a record
53      */
54     protected $reference;
55
56     /**
57      * @var string $referenceField         the reference field of the collection
58      */
59     protected $referenceField;
60
61     /**
62      * @var Doctrine_Relation               the record this collection is related to, if any
63      */
64     protected $relation;
65
66     /**
67      * @var string $keyColumn               the name of the column that is used for collection key mapping
68      */
69     protected $keyColumn;
70
71     /**
72      * @var Doctrine_Null $null             used for extremely fast null value testing
73      */
74     protected static $null;
75
76
77     /**
78      * constructor
79      *
80      * @param Doctrine_Table|string $table
81      */
82     public function __construct($table, $keyColumn = null)
83     {
84         if ( ! ($table instanceof Doctrine_Table)) {
85             $table = Doctrine_Manager::getInstance()
86                         ->getTable($table);
87         }
88         $this->_table = $table;
89
90         if ($keyColumn === null) {
91             $keyColumn = $table->getBoundQueryPart('indexBy');
92         }
93
94         if ($keyColumn !== null) {
95             $this->keyColumn = $keyColumn;
96         }
97     }
98
99     /**
100      * initNullObject
101      * initializes the null object for this collection
102      *
103      * @return void
104      */
105     public static function initNullObject(Doctrine_Null $null)
106     {
107         self::$null = $null;
108     }
109
110     /**
111      * getTable
112      * returns the table this collection belongs to
113      *
114      * @return Doctrine_Table
115      */
116     public function getTable()
117     {
118         return $this->_table;
119     }
120
121     /**
122      * setData
123      *
124      * @param array $data
125      * @return Doctrine_Collection
126      */
127     public function setData(array $data) 
128     {
129         $this->data = $data;
130     }
131
132     /**
133      * this method is automatically called when this Doctrine_Collection is serialized
134      *
135      * @return array
136      */
137     public function serialize()
138     {
139         $vars = get_object_vars($this);
140
141         unset($vars['reference']);
142         unset($vars['reference_field']);
143         unset($vars['relation']);
144         unset($vars['expandable']);
145         unset($vars['expanded']);
146         unset($vars['generator']);
147
148         $vars['_table'] = $vars['_table']->getComponentName();
149
150         return serialize($vars);
151     }
152
153     /**
154      * unseralize
155      * this method is automatically called everytime a Doctrine_Collection object is unserialized
156      *
157      * @return void
158      */
159     public function unserialize($serialized)
160     {
161         $manager    = Doctrine_Manager::getInstance();
162         $connection    = $manager->getCurrentConnection();
163
164         $array = unserialize($serialized);
165
166         foreach ($array as $name => $values) {
167             $this->$name = $values;
168         }
169
170         $this->_table = $connection->getTable($this->_table);
171
172         if ($keyColumn === null) {
173             $keyColumn = $this->_table->getBoundQueryPart('indexBy');
174         }
175
176         if ($keyColumn !== null) {
177             $this->keyColumn = $keyColumn;
178         }
179     }
180
181     /**
182      * setKeyColumn
183      * sets the key column for this collection
184      *
185      * @param string $column
186      * @return Doctrine_Collection
187      */
188     public function setKeyColumn($column)
189     {
190         $this->keyColumn = $column;
191         
192         return $this;
193     }
194
195     /**
196      * getKeyColumn
197      * returns the name of the key column
198      *
199      * @return string
200      */
201     public function getKeyColumn()
202     {
203         return $this->column;
204     }
205
206     /**
207      * getData
208      * returns all the records as an array
209      *
210      * @return array
211      */
212     public function getData()
213     {
214         return $this->data;
215     }
216
217     /**
218      * getFirst
219      * returns the first record in the collection
220      *
221      * @return mixed
222      */
223     public function getFirst()
224     {
225         return reset($this->data);
226     }
227
228     /**
229      * getLast
230      * returns the last record in the collection
231      *
232      * @return mixed
233      */
234     public function getLast()
235     {
236         return end($this->data);
237     }
238
239     /**
240      * setReference
241      * sets a reference pointer
242      *
243      * @return void
244      */
245     public function setReference(Doctrine_Record $record, Doctrine_Relation $relation)
246     {
247         $this->reference       = $record;
248         $this->relation        = $relation;
249
250         if ($relation instanceof Doctrine_Relation_ForeignKey || 
251             $relation instanceof Doctrine_Relation_LocalKey) {
252
253             $this->referenceField = $relation->getForeign();
254
255             $value = $record->get($relation->getLocal());
256
257             foreach ($this->data as $record) {
258                 if ($value !== null) {
259                     $record->set($this->referenceField, $value, false);
260                 } else {
261                     $record->set($this->referenceField, $this->reference, false);
262                 }
263             }
264         } elseif ($relation instanceof Doctrine_Relation_Association) {
265
266         }
267     }
268
269     /**
270      * getReference
271      *
272      * @return mixed
273      */
274     public function getReference()
275     {
276         return $this->reference;
277     }
278
279     /**
280      * remove
281      * removes a specified collection element
282      *
283      * @param mixed $key
284      * @return boolean
285      */
286     public function remove($key)
287     {
288         $removed = $this->data[$key];
289
290         unset($this->data[$key]);
291         return $removed;
292     }
293
294     /**
295      * contains
296      * whether or not this collection contains a specified element
297      *
298      * @param mixed $key                    the key of the element
299      * @return boolean
300      */
301     public function contains($key)
302     {
303         return isset($this->data[$key]);
304     }
305     public function search(Doctrine_Record $record)
306     {
307         return array_search($record, $this->data, true);
308     }
309
310     /**
311      * get
312      * returns a record for given key
313      *
314      * There are two special cases:
315      *
316      * 1. if null is given as a key a new record is created and attached
317      * at the end of the collection
318      *
319      * 2. if given key does not exist, then a new record is create and attached
320      * to the given key
321      *
322      * Collection also maps referential information to newly created records
323      *
324      * @param mixed $key                    the key of the element
325      * @return Doctrine_Record              return a specified record
326      */
327     public function get($key)
328     {
329         if ( ! isset($this->data[$key])) {
330             $record = $this->_table->create();
331
332             if (isset($this->referenceField)) {
333                 $value = $this->reference->get($this->relation->getLocal());
334
335                 if ($value !== null) {
336                     $record->set($this->referenceField, $value, false);
337                 } else {
338                     $record->set($this->referenceField, $this->reference, false);
339                 }
340             }
341             if ($key === null) {
342                 $this->data[] = $record;
343             } else {
344                 $this->data[$key] = $record;      
345             }
346
347             if (isset($this->keyColumn)) {
348
349                 $record->set($this->keyColumn, $key);
350             }
351
352             return $record;
353         }
354
355         return $this->data[$key];
356     }
357
358     /**
359      * @return array                an array containing all primary keys
360      */
361     public function getPrimaryKeys()
362     {
363         $list = array();
364         $name = $this->_table->getIdentifier();
365
366         foreach ($this->data as $record) {
367             if (is_array($record) && isset($record[$name])) {
368                 $list[] = $record[$name];
369             } else {
370                 $list[] = $record->getIncremented();
371             }
372         }
373         return $list;
374     }
375
376     /**
377      * returns all keys
378      * @return array
379      */
380     public function getKeys()
381     {
382         return array_keys($this->data);
383     }
384
385     /**
386      * count
387      * this class implements interface countable
388      * returns the number of records in this collection
389      *
390      * @return integer
391      */
392     public function count()
393     {
394         return count($this->data);
395     }
396
397     /**
398      * set
399      * @param integer $key
400      * @param Doctrine_Record $record
401      * @return void
402      */
403     public function set($key, Doctrine_Record $record)
404     {
405         if (isset($this->referenceField)) {
406             $record->set($this->referenceField, $this->reference, false);
407         }
408
409         $this->data[$key] = $record;
410     }
411
412     /**
413      * adds a record to collection
414      * @param Doctrine_Record $record              record to be added
415      * @param string $key                          optional key for the record
416      * @return boolean
417      */
418     public function add(Doctrine_Record $record, $key = null)
419     {
420         if (isset($this->referenceField)) {
421             $value = $this->reference->get($this->relation->getLocal());
422
423             if ($value !== null) {
424                 $record->set($this->referenceField, $value, false);
425             } else {
426                 $record->set($this->referenceField, $this->reference, false);
427             }
428         }
429         /**
430          * for some weird reason in_array cannot be used here (php bug ?)
431          *
432          * if used it results in fatal error : [ nesting level too deep ]
433          */
434         foreach ($this->data as $val) {
435             if ($val === $record) {
436                 return false;
437             }
438         }
439
440         if (isset($key)) {
441             if (isset($this->data[$key])) {
442                 return false;
443             }
444             $this->data[$key] = $record;
445             return true;
446         }
447
448         if (isset($this->keyColumn)) {
449             $value = $record->get($this->keyColumn);
450             if ($value === null) {
451                 throw new Doctrine_Collection_Exception("Couldn't create collection index. Record field '".$this->keyColumn."' was null.");
452             }
453             $this->data[$value] = $record;
454         } else {
455             $this->data[] = $record;
456         }
457         return true;
458     }
459
460     /**
461      * loadRelated
462      *
463      * @param mixed $name
464      * @return boolean
465      */
466     public function loadRelated($name = null)
467     {
468         $list = array();
469         $query   = new Doctrine_Query($this->_table->getConnection());
470
471         if ( ! isset($name)) {
472             foreach ($this->data as $record) {
473                 $value = $record->getIncremented();
474                 if ($value !== null) {
475                     $list[] = $value;
476                 }
477             }
478             $query->from($this->_table->getComponentName() . '(' . implode(", ",$this->_table->getPrimaryKeys()) . ')');
479             $query->where($this->_table->getComponentName() . '.id IN (' . substr(str_repeat("?, ", count($list)),0,-2) . ')');
480
481             return $query;
482         }
483
484         $rel     = $this->_table->getRelation($name);
485
486         if ($rel instanceof Doctrine_Relation_LocalKey || $rel instanceof Doctrine_Relation_ForeignKey) {
487             foreach ($this->data as $record) {
488                 $list[] = $record[$rel->getLocal()];
489             }
490         } else {
491             foreach ($this->data as $record) {
492                 $value = $record->getIncremented();
493                 if ($value !== null) {
494                     $list[] = $value;
495                 }
496             }
497         }
498
499         $dql     = $rel->getRelationDql(count($list), 'collection');
500
501         $coll    = $query->query($dql, $list);
502
503         $this->populateRelated($name, $coll);
504     }
505
506     /**
507      * populateRelated
508      *
509      * @param string $name
510      * @param Doctrine_Collection $coll
511      * @return void
512      */
513     public function populateRelated($name, Doctrine_Collection $coll)
514     {
515         $rel     = $this->_table->getRelation($name);
516         $table   = $rel->getTable();
517         $foreign = $rel->getForeign();
518         $local   = $rel->getLocal();
519
520         if ($rel instanceof Doctrine_Relation_LocalKey) {
521             foreach ($this->data as $key => $record) {
522                 foreach ($coll as $k => $related) {
523                     if ($related[$foreign] == $record[$local]) {
524                         $this->data[$key]->setRelated($name, $related);
525                     }
526                 }
527             }
528         } elseif ($rel instanceof Doctrine_Relation_ForeignKey) {
529             foreach ($this->data as $key => $record) {
530                 if ( ! $record->exists()) {
531                     continue;
532                 }
533                 $sub = new Doctrine_Collection($table);
534
535                 foreach ($coll as $k => $related) {
536                     if ($related[$foreign] == $record[$local]) {
537                         $sub->add($related);
538                         $coll->remove($k);
539                     }
540                 }
541
542                 $this->data[$key]->setRelated($name, $sub);
543             }
544         } elseif ($rel instanceof Doctrine_Relation_Association) {
545             $identifier = $this->_table->getIdentifier();
546             $asf        = $rel->getAssociationFactory();
547             $name       = $table->getComponentName();
548
549             foreach ($this->data as $key => $record) {
550                 if ( ! $record->exists()) {
551                     continue;
552                 }
553                 $sub = new Doctrine_Collection($table);
554                 foreach ($coll as $k => $related) {
555                     if ($related->get($local) == $record[$identifier]) {
556                         $sub->add($related->get($name));
557                     }
558                 }
559                 $this->data[$key]->setRelated($name, $sub);
560
561             }
562         }
563     }
564
565     /**
566      * getNormalIterator
567      * returns normal iterator - an iterator that will not expand this collection
568      *
569      * @return Doctrine_Iterator_Normal
570      */
571     public function getNormalIterator()
572     {
573         return new Doctrine_Collection_Iterator_Normal($this);
574     }
575
576     /**
577      * takeSnapshot
578      * takes a snapshot from this collection
579      *
580      * snapshots are used for diff processing, for example
581      * when a fetched collection has three elements, then two of those
582      * are being removed the diff would contain one element
583      *
584      * Doctrine_Collection::save() attaches the diff with the help of last
585      * snapshot.
586      *
587      * @return Doctrine_Collection
588      */
589     public function takeSnapshot()
590     {
591         $this->_snapshot = $this->data;
592         
593         return $this;
594     }
595
596     /**
597      * getSnapshot
598      * returns the data of the last snapshot
599      *
600      * @return array    returns the data in last snapshot
601      */
602     public function getSnapshot()
603     {
604         return $this->_snapshot;
605     }
606
607     /**
608      * processDiff
609      * processes the difference of the last snapshot and the current data
610      *
611      * an example:
612      * Snapshot with the objects 1, 2 and 4
613      * Current data with objects 2, 3 and 5
614      *
615      * The process would remove object 4
616      *
617      * @return Doctrine_Collection
618      */
619     public function processDiff() 
620     {
621         foreach (array_udiff($this->_snapshot, $this->data, array($this, "compareRecords")) as $record) {
622             $record->delete();
623         }
624
625         return $this;
626     }
627
628     /**
629      * toArray
630      * Mimics the result of a $query->execute(array(), Doctrine::FETCH_ARRAY);
631      *
632      * @param boolean $deep
633      */
634     public function toArray($deep = false, $prefixKey = false)
635     {
636         $data = array();
637         foreach ($this->data as $key => $record) {
638             
639             $key = $prefixKey ? get_class($record) . '_' .$key:$key;
640             
641             $data[$key] = $record->toArray($deep, $prefixKey);
642         }
643         
644         return $data;
645     }
646     public function fromArray($array)
647     {
648         $data = array();
649         foreach ($array as $row) {
650             $record = $this->_table->getRecord();
651             $record->fromArray($row);
652             
653             $data[] = $record;
654         }
655         
656         $this->data = $data;
657     }
658     public function exportTo($type, $deep = false)
659     {
660         if ($type == 'array') {
661             return $this->toArray($deep);
662         } else {
663             return Doctrine_Parser::dump($this->toArray($deep, true), $type);
664         }
665     }
666     public function importFrom($type, $data)
667     {
668         if ($type == 'array') {
669             return $this->fromArray($data);
670         } else {
671             return $this->fromArray(Doctrine_Parser::load($data, $type));
672         }
673     }
674     public function getDeleteDiff()
675     {
676         return array_udiff($this->_snapshot, $this->data, array($this, "compareRecords"));
677     }
678     public function getInsertDiff()
679     {
680         return array_udiff($this->data, $this->_snapshot, array($this, "compareRecords"));
681     }
682
683     /**
684      * compareRecords
685      * Compares two records. To be used on _snapshot diffs using array_udiff
686      */
687     protected function compareRecords($a, $b)
688     {
689         if ($a->getOid() == $b->getOid()) return 0;
690         return ($a->getOid() > $b->getOid()) ? 1 : -1;
691     }
692
693     /**
694      * save
695      * saves all records of this collection and processes the 
696      * difference of the last snapshot and the current data
697      *
698      * @param Doctrine_Connection $conn     optional connection parameter
699      * @return Doctrine_Collection
700      */
701     public function save(Doctrine_Connection $conn = null)
702     {
703         if ($conn == null) {
704             $conn = $this->_table->getConnection();
705         }
706         $conn->beginTransaction();
707
708         $conn->transaction->addCollection($this);
709
710         $this->processDiff();
711
712         foreach ($this->getData() as $key => $record) {
713             $record->save($conn);
714         }
715
716         $conn->commit();
717
718         return $this;
719     }
720
721     /**
722      * delete
723      * single shot delete
724      * deletes all records from this collection
725      * and uses only one database query to perform this operation
726      *
727      * @return Doctrine_Collection
728      */
729     public function delete(Doctrine_Connection $conn = null)
730     {
731         if ($conn == null) {
732             $conn = $this->_table->getConnection();
733         }
734
735         $conn->beginTransaction();
736         $conn->transaction->addCollection($this);
737
738         foreach ($this as $key => $record) {
739             $record->delete($conn);
740         }
741
742         $conn->commit();
743
744         $this->data = array();
745         
746         return $this;
747     }
748
749     /**
750      * getIterator
751      * @return object ArrayIterator
752      */
753     public function getIterator()
754     {
755         $data = $this->data;
756         return new ArrayIterator($data);
757     }
758
759     /**
760      * returns a string representation of this object
761      */
762     public function __toString()
763     {
764         return Doctrine_Lib::getCollectionAsString($this);
765     }
766 }