Coverage for Doctrine_Collection

Back to coverage report

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