Coverage for Doctrine_Collection

Back to coverage report

1 <?php
2 /*
3  *  $Id: Collection.php 3093 2007-11-08 20:52:49Z wolfpakz $
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: 3093 $
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         $keyColumn = isset($array['keyColumn']) ? $array['keyColumn'] : null;
173         if ($keyColumn === null) {
174             $keyColumn = $this->_table->getBoundQueryPart('indexBy');
175         }
176
177         if ($keyColumn !== null) {
178             $this->keyColumn = $keyColumn;
179         }
180     }
181
182     /**
183      * setKeyColumn
184      * sets the key column for this collection
185      *
186      * @param string $column
187      * @return Doctrine_Collection
188      */
189     public function setKeyColumn($column)
190     {
191         $this->keyColumn = $column;
192         
193         return $this;
194     }
195
196     /**
197      * getKeyColumn
198      * returns the name of the key column
199      *
200      * @return string
201      */
202     public function getKeyColumn()
203     {
204         return $this->column;
205     }
206
207     /**
208      * getData
209      * returns all the records as an array
210      *
211      * @return array
212      */
213     public function getData()
214     {
215         return $this->data;
216     }
217
218     /**
219      * getFirst
220      * returns the first record in the collection
221      *
222      * @return mixed
223      */
224     public function getFirst()
225     {
226         return reset($this->data);
227     }
228
229     /**
230      * getLast
231      * returns the last record in the collection
232      *
233      * @return mixed
234      */
235     public function getLast()
236     {
237         return end($this->data);
238     }
239     /**
240      * returns the last record in the collection
241      *
242      * @return mixed
243      */
244     public function end()
245     {
246         return end($this->data);
247     }
248     /**
249      * returns the current key
250      *
251      * @return mixed
252      */
253     public function key()
254     {
255         return key($this->data);
256     }
257     /**
258      * setReference
259      * sets a reference pointer
260      *
261      * @return void
262      */
263     public function setReference(Doctrine_Record $record, Doctrine_Relation $relation)
264     {
265         $this->reference       = $record;
266         $this->relation        = $relation;
267
268         if ($relation instanceof Doctrine_Relation_ForeignKey || 
269             $relation instanceof Doctrine_Relation_LocalKey) {
270
271             $this->referenceField = $relation->getForeign();
272
273             $value = $record->get($relation->getLocal());
274
275             foreach ($this->data as $record) {
276                 if ($value !== null) {
277                     $record->set($this->referenceField, $value, false);
278                 } else {
279                     $record->set($this->referenceField, $this->reference, false);
280                 }
281             }
282         } elseif ($relation instanceof Doctrine_Relation_Association) {
283
284         }
285     }
286
287     /**
288      * getReference
289      *
290      * @return mixed
291      */
292     public function getReference()
293     {
294         return $this->reference;
295     }
296
297     /**
298      * remove
299      * removes a specified collection element
300      *
301      * @param mixed $key
302      * @return boolean
303      */
304     public function remove($key)
305     {
306         $removed = $this->data[$key];
307
308         unset($this->data[$key]);
309         return $removed;
310     }
311
312     /**
313      * contains
314      * whether or not this collection contains a specified element
315      *
316      * @param mixed $key                    the key of the element
317      * @return boolean
318      */
319     public function contains($key)
320     {
321         return isset($this->data[$key]);
322     }
323     public function search(Doctrine_Record $record)
324     {
325         return array_search($record, $this->data, true);
326     }
327
328     /**
329      * get
330      * returns a record for given key
331      *
332      * There are two special cases:
333      *
334      * 1. if null is given as a key a new record is created and attached
335      * at the end of the collection
336      *
337      * 2. if given key does not exist, then a new record is create and attached
338      * to the given key
339      *
340      * Collection also maps referential information to newly created records
341      *
342      * @param mixed $key                    the key of the element
343      * @return Doctrine_Record              return a specified record
344      */
345     public function get($key)
346     {
347         if ( ! isset($this->data[$key])) {
348             $record = $this->_table->create();
349
350             if (isset($this->referenceField)) {
351                 $value = $this->reference->get($this->relation->getLocal());
352
353                 if ($value !== null) {
354                     $record->set($this->referenceField, $value, false);
355                 } else {
356                     $record->set($this->referenceField, $this->reference, false);
357                 }
358             }
359             if ($key === null) {
360                 $this->data[] = $record;
361             } else {
362                 $this->data[$key] = $record;      
363             }
364
365             if (isset($this->keyColumn)) {
366
367                 $record->set($this->keyColumn, $key);
368             }
369
370             return $record;
371         }
372
373         return $this->data[$key];
374     }
375
376     /**
377      * @return array                an array containing all primary keys
378      */
379     public function getPrimaryKeys()
380     {
381         $list = array();
382         $name = $this->_table->getIdentifier();
383
384         foreach ($this->data as $record) {
385             if (is_array($record) && isset($record[$name])) {
386                 $list[] = $record[$name];
387             } else {
388                 $list[] = $record->getIncremented();
389             }
390         }
391         return $list;
392     }
393
394     /**
395      * returns all keys
396      * @return array
397      */
398     public function getKeys()
399     {
400         return array_keys($this->data);
401     }
402
403     /**
404      * count
405      * this class implements interface countable
406      * returns the number of records in this collection
407      *
408      * @return integer
409      */
410     public function count()
411     {
412         return count($this->data);
413     }
414
415     /**
416      * set
417      * @param integer $key
418      * @param Doctrine_Record $record
419      * @return void
420      */
421     public function set($key, Doctrine_Record $record)
422     {
423         if (isset($this->referenceField)) {
424             $record->set($this->referenceField, $this->reference, false);
425         }
426
427         $this->data[$key] = $record;
428     }
429
430     /**
431      * adds a record to collection
432      * @param Doctrine_Record $record              record to be added
433      * @param string $key                          optional key for the record
434      * @return boolean
435      */
436     public function add(Doctrine_Record $record, $key = null)
437     {
438         if (isset($this->referenceField)) {
439             $value = $this->reference->get($this->relation->getLocal());
440
441             if ($value !== null) {
442                 $record->set($this->referenceField, $value, false);
443             } else {
444                 $record->set($this->referenceField, $this->reference, false);
445             }
446         }
447         /**
448          * for some weird reason in_array cannot be used here (php bug ?)
449          *
450          * if used it results in fatal error : [ nesting level too deep ]
451          */
452         foreach ($this->data as $val) {
453             if ($val === $record) {
454                 return false;
455             }
456         }
457
458         if (isset($key)) {
459             if (isset($this->data[$key])) {
460                 return false;
461             }
462             $this->data[$key] = $record;
463             return true;
464         }
465
466         if (isset($this->keyColumn)) {
467             $value = $record->get($this->keyColumn);
468             if ($value === null) {
469                 throw new Doctrine_Collection_Exception("Couldn't create collection index. Record field '".$this->keyColumn."' was null.");
470             }
471             $this->data[$value] = $record;
472         } else {
473             $this->data[] = $record;
474         }
475         return true;
476     }
477
478     /**
479      * loadRelated
480      *
481      * @param mixed $name
482      * @return boolean
483      */
484     public function loadRelated($name = null)
485     {
486         $list = array();
487         $query   = new Doctrine_Query($this->_table->getConnection());
488
489         if ( ! isset($name)) {
490             foreach ($this->data as $record) {
491                 $value = $record->getIncremented();
492                 if ($value !== null) {
493                     $list[] = $value;
494                 }
495             }
496             $query->from($this->_table->getComponentName() . '(' . implode(", ",$this->_table->getPrimaryKeys()) . ')');
497             $query->where($this->_table->getComponentName() . '.id IN (' . substr(str_repeat("?, ", count($list)),0,-2) . ')');
498
499             return $query;
500         }
501
502         $rel     = $this->_table->getRelation($name);
503
504         if ($rel instanceof Doctrine_Relation_LocalKey || $rel instanceof Doctrine_Relation_ForeignKey) {
505             foreach ($this->data as $record) {
506                 $list[] = $record[$rel->getLocal()];
507             }
508         } else {
509             foreach ($this->data as $record) {
510                 $value = $record->getIncremented();
511                 if ($value !== null) {
512                     $list[] = $value;
513                 }
514             }
515         }
516
517         $dql     = $rel->getRelationDql(count($list), 'collection');
518
519         $coll    = $query->query($dql, $list);
520
521         $this->populateRelated($name, $coll);
522     }
523
524     /**
525      * populateRelated
526      *
527      * @param string $name
528      * @param Doctrine_Collection $coll
529      * @return void
530      */
531     public function populateRelated($name, Doctrine_Collection $coll)
532     {
533         $rel     = $this->_table->getRelation($name);
534         $table   = $rel->getTable();
535         $foreign = $rel->getForeign();
536         $local   = $rel->getLocal();
537
538         if ($rel instanceof Doctrine_Relation_LocalKey) {
539             foreach ($this->data as $key => $record) {
540                 foreach ($coll as $k => $related) {
541                     if ($related[$foreign] == $record[$local]) {
542                         $this->data[$key]->setRelated($name, $related);
543                     }
544                 }
545             }
546         } elseif ($rel instanceof Doctrine_Relation_ForeignKey) {
547             foreach ($this->data as $key => $record) {
548                 if ( ! $record->exists()) {
549                     continue;
550                 }
551                 $sub = new Doctrine_Collection($table);
552
553                 foreach ($coll as $k => $related) {
554                     if ($related[$foreign] == $record[$local]) {
555                         $sub->add($related);
556                         $coll->remove($k);
557                     }
558                 }
559
560                 $this->data[$key]->setRelated($name, $sub);
561             }
562         } elseif ($rel instanceof Doctrine_Relation_Association) {
563             $identifier = $this->_table->getIdentifier();
564             $asf        = $rel->getAssociationFactory();
565             $name       = $table->getComponentName();
566
567             foreach ($this->data as $key => $record) {
568                 if ( ! $record->exists()) {
569                     continue;
570                 }
571                 $sub = new Doctrine_Collection($table);
572                 foreach ($coll as $k => $related) {
573                     if ($related->get($local) == $record[$identifier]) {
574                         $sub->add($related->get($name));
575                     }
576                 }
577                 $this->data[$key]->setRelated($name, $sub);
578
579             }
580         }
581     }
582
583     /**
584      * getNormalIterator
585      * returns normal iterator - an iterator that will not expand this collection
586      *
587      * @return Doctrine_Iterator_Normal
588      */
589     public function getNormalIterator()
590     {
591         return new Doctrine_Collection_Iterator_Normal($this);
592     }
593
594     /**
595      * takeSnapshot
596      * takes a snapshot from this collection
597      *
598      * snapshots are used for diff processing, for example
599      * when a fetched collection has three elements, then two of those
600      * are being removed the diff would contain one element
601      *
602      * Doctrine_Collection::save() attaches the diff with the help of last
603      * snapshot.
604      *
605      * @return Doctrine_Collection
606      */
607     public function takeSnapshot()
608     {
609         $this->_snapshot = $this->data;
610         
611         return $this;
612     }
613
614     /**
615      * getSnapshot
616      * returns the data of the last snapshot
617      *
618      * @return array    returns the data in last snapshot
619      */
620     public function getSnapshot()
621     {
622         return $this->_snapshot;
623     }
624
625     /**
626      * processDiff
627      * processes the difference of the last snapshot and the current data
628      *
629      * an example:
630      * Snapshot with the objects 1, 2 and 4
631      * Current data with objects 2, 3 and 5
632      *
633      * The process would remove object 4
634      *
635      * @return Doctrine_Collection
636      */
637     public function processDiff() 
638     {
639         foreach (array_udiff($this->_snapshot, $this->data, array($this, "compareRecords")) as $record) {
640             $record->delete();
641         }
642
643         return $this;
644     }
645
646     /**
647      * toArray
648      * Mimics the result of a $query->execute(array(), Doctrine::FETCH_ARRAY);
649      *
650      * @param boolean $deep
651      */
652     public function toArray($deep = false, $prefixKey = false)
653     {
654         $data = array();
655         foreach ($this as $key => $record) {
656             
657             $key = $prefixKey ? get_class($record) . '_' .$key:$key;
658             
659             $data[$key] = $record->toArray($deep, $prefixKey);
660         }
661         
662         return $data;
663     }
664     public function fromArray($array)
665     {
666         $data = array();
667         foreach ($array as $row) {
668             $record = $this->_table->getRecord();
669             $record->fromArray($row);
670             
671             $this[] = $record;
672         }
673     }
674     public function exportTo($type, $deep = false)
675     {
676         if ($type == 'array') {
677             return $this->toArray($deep);
678         } else {
679             return Doctrine_Parser::dump($this->toArray($deep, true), $type);
680         }
681     }
682     public function importFrom($type, $data)
683     {
684         if ($type == 'array') {
685             return $this->fromArray($data);
686         } else {
687             return $this->fromArray(Doctrine_Parser::load($data, $type));
688         }
689     }
690     public function getDeleteDiff()
691     {
692         return array_udiff($this->_snapshot, $this->data, array($this, "compareRecords"));
693     }
694     public function getInsertDiff()
695     {
696         return array_udiff($this->data, $this->_snapshot, array($this, "compareRecords"));
697     }
698
699     /**
700      * compareRecords
701      * Compares two records. To be used on _snapshot diffs using array_udiff
702      */
703     protected function compareRecords($a, $b)
704     {
705         if ($a->getOid() == $b->getOid()) return 0;
706         return ($a->getOid() > $b->getOid()) ? 1 : -1;
707     }
708
709     /**
710      * save
711      * saves all records of this collection and processes the 
712      * difference of the last snapshot and the current data
713      *
714      * @param Doctrine_Connection $conn     optional connection parameter
715      * @return Doctrine_Collection
716      */
717     public function save(Doctrine_Connection $conn = null)
718     {
719         if ($conn == null) {
720             $conn = $this->_table->getConnection();
721         }
722         $conn->beginTransaction();
723
724         $conn->transaction->addCollection($this);
725
726         $this->processDiff();
727
728         foreach ($this->getData() as $key => $record) {
729             $record->save($conn);
730         }
731
732         $conn->commit();
733
734         return $this;
735     }
736
737     /**
738      * delete
739      * single shot delete
740      * deletes all records from this collection
741      * and uses only one database query to perform this operation
742      *
743      * @return Doctrine_Collection
744      */
745     public function delete(Doctrine_Connection $conn = null)
746     {
747         if ($conn == null) {
748             $conn = $this->_table->getConnection();
749         }
750
751         $conn->beginTransaction();
752         $conn->transaction->addCollection($this);
753
754         foreach ($this as $key => $record) {
755             $record->delete($conn);
756         }
757
758         $conn->commit();
759
760         $this->data = array();
761         
762         return $this;
763     }
764
765     /**
766      * getIterator
767      * @return object ArrayIterator
768      */
769     public function getIterator()
770     {
771         $data = $this->data;
772         return new ArrayIterator($data);
773     }
774
775     /**
776      * returns a string representation of this object
777      */
778     public function __toString()
779     {
780         return Doctrine_Lib::getCollectionAsString($this);
781     }
782     
783     /**
784      * returns the relation object
785      * @return object Doctrine_Relation
786      */
787     public function getRelation()
788     {
789         return $this->relation;
790     }
791 }