Coverage for Doctrine_Collection

Back to coverage report

1 <?php
2 /*
3  *  $Id: Collection.php 4033 2008-03-19 01:51:50Z 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.phpdoctrine.org>.
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.org
30  * @since       1.0
31  * @version     $Revision: 4033 $
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::getTable($table);
86         }
87
88         $this->_table = $table;
89
90         if ($keyColumn === null) {
91             $keyColumn = $table->getBoundQueryPart('indexBy');
92         }
93
94         if ($keyColumn === null) {
95          $keyColumn = $table->getAttribute(Doctrine::ATTR_COLL_KEY);
96         }
97
98         if ($keyColumn !== null) {
99             $this->keyColumn = $keyColumn;
100         }
101     }
102
103     /**
104      * initNullObject
105      * initializes the null object for this collection
106      *
107      * @return void
108      */
109     public static function initNullObject(Doctrine_Null $null)
110     {
111         self::$null = $null;
112     }
113
114     /**
115      * getTable
116      * returns the table this collection belongs to
117      *
118      * @return Doctrine_Table
119      */
120     public function getTable()
121     {
122         return $this->_table;
123     }
124
125     /**
126      * setData
127      *
128      * @param array $data
129      * @return Doctrine_Collection
130      */
131     public function setData(array $data) 
132     {
133         $this->data = $data;
134     }
135
136     /**
137      * this method is automatically called when this Doctrine_Collection is serialized
138      *
139      * @return array
140      */
141     public function serialize()
142     {
143         $vars = get_object_vars($this);
144
145         unset($vars['reference']);
146         unset($vars['reference_field']);
147         unset($vars['relation']);
148         unset($vars['expandable']);
149         unset($vars['expanded']);
150         unset($vars['generator']);
151
152         $vars['_table'] = $vars['_table']->getComponentName();
153
154         return serialize($vars);
155     }
156
157     /**
158      * unseralize
159      * this method is automatically called everytime a Doctrine_Collection object is unserialized
160      *
161      * @return void
162      */
163     public function unserialize($serialized)
164     {
165         $manager    = Doctrine_Manager::getInstance();
166         $connection    = $manager->getCurrentConnection();
167
168         $array = unserialize($serialized);
169
170         foreach ($array as $name => $values) {
171             $this->$name = $values;
172         }
173
174         $this->_table = $connection->getTable($this->_table);
175
176         $keyColumn = isset($array['keyColumn']) ? $array['keyColumn'] : null;
177         if ($keyColumn === null) {
178             $keyColumn = $this->_table->getBoundQueryPart('indexBy');
179         }
180
181         if ($keyColumn !== null) {
182             $this->keyColumn = $keyColumn;
183         }
184     }
185
186     /**
187      * setKeyColumn
188      * sets the key column for this collection
189      *
190      * @param string $column
191      * @return Doctrine_Collection
192      */
193     public function setKeyColumn($column)
194     {
195         $this->keyColumn = $column;
196         
197         return $this;
198     }
199
200     /**
201      * getKeyColumn
202      * returns the name of the key column
203      *
204      * @return string
205      */
206     public function getKeyColumn()
207     {
208         return $this->column;
209     }
210
211     /**
212      * getData
213      * returns all the records as an array
214      *
215      * @return array
216      */
217     public function getData()
218     {
219         return $this->data;
220     }
221
222     /**
223      * getFirst
224      * returns the first record in the collection
225      *
226      * @return mixed
227      */
228     public function getFirst()
229     {
230         return reset($this->data);
231     }
232
233     /**
234      * getLast
235      * returns the last record in the collection
236      *
237      * @return mixed
238      */
239     public function getLast()
240     {
241         return end($this->data);
242     }
243     /**
244      * returns the last record in the collection
245      *
246      * @return mixed
247      */
248     public function end()
249     {
250         return end($this->data);
251     }
252     /**
253      * returns the current key
254      *
255      * @return mixed
256      */
257     public function key()
258     {
259         return key($this->data);
260     }
261     /**
262      * setReference
263      * sets a reference pointer
264      *
265      * @return void
266      */
267     public function setReference(Doctrine_Record $record, Doctrine_Relation $relation)
268     {
269         $this->reference = $record;
270         $this->relation  = $relation;
271
272         if ($relation instanceof Doctrine_Relation_ForeignKey || 
273                 $relation instanceof Doctrine_Relation_LocalKey) {
274
275             $this->referenceField = $relation->getForeignFieldName();
276
277             $value = $record->get($relation->getLocalFieldName());
278
279             foreach ($this->data as $record) {
280                 if ($value !== null) {
281                     $record->set($this->referenceField, $value, false);
282                 } else {
283                     $record->set($this->referenceField, $this->reference, false);
284                 }
285             }
286         } elseif ($relation instanceof Doctrine_Relation_Association) {
287
288         }
289     }
290
291     /**
292      * getReference
293      *
294      * @return mixed
295      */
296     public function getReference()
297     {
298         return $this->reference;
299     }
300
301     /**
302      * remove
303      * removes a specified collection element
304      *
305      * @param mixed $key
306      * @return boolean
307      */
308     public function remove($key)
309     {
310         $removed = $this->data[$key];
311
312         unset($this->data[$key]);
313         return $removed;
314     }
315
316     /**
317      * contains
318      * whether or not this collection contains a specified element
319      *
320      * @param mixed $key                    the key of the element
321      * @return boolean
322      */
323     public function contains($key)
324     {
325         return isset($this->data[$key]);
326     }
327
328     /**
329      * search
330      *
331      * @param string $Doctrine_Record 
332      * @return void
333      */
334     public function search(Doctrine_Record $record)
335     {
336         return array_search($record, $this->data, true);
337     }
338
339     /**
340      * get
341      * returns a record for given key
342      *
343      * There are two special cases:
344      *
345      * 1. if null is given as a key a new record is created and attached
346      * at the end of the collection
347      *
348      * 2. if given key does not exist, then a new record is create and attached
349      * to the given key
350      *
351      * Collection also maps referential information to newly created records
352      *
353      * @param mixed $key                    the key of the element
354      * @return Doctrine_Record              return a specified record
355      */
356     public function get($key)
357     {
358         if ( ! isset($this->data[$key])) {
359             $record = $this->_table->create();
360
361             if (isset($this->referenceField)) {
362                 $value = $this->reference->get($this->relation->getLocalFieldName());
363
364                 if ($value !== null) {
365                     $record->set($this->referenceField, $value, false);
366                 } else {
367                     $record->set($this->referenceField, $this->reference, false);
368                 }
369             }
370             if ($key === null) {
371                 $this->data[] = $record;
372             } else {
373                 $this->data[$key] = $record;      
374             }
375
376             if (isset($this->keyColumn)) {
377                 $record->set($this->keyColumn, $key);
378             }
379
380             return $record;
381         }
382
383         return $this->data[$key];
384     }
385
386     /**
387      * @return array                an array containing all primary keys
388      */
389     public function getPrimaryKeys()
390     {
391         $list = array();
392         $name = $this->_table->getIdentifier();
393
394         foreach ($this->data as $record) {
395             if (is_array($record) && isset($record[$name])) {
396                 $list[] = $record[$name];
397             } else {
398                 $list[] = $record->getIncremented();
399             }
400         }
401         return $list;
402     }
403
404     /**
405      * returns all keys
406      * @return array
407      */
408     public function getKeys()
409     {
410         return array_keys($this->data);
411     }
412
413     /**
414      * count
415      * this class implements interface countable
416      * returns the number of records in this collection
417      *
418      * @return integer
419      */
420     public function count()
421     {
422         return count($this->data);
423     }
424
425     /**
426      * set
427      * @param integer $key
428      * @param Doctrine_Record $record
429      * @return void
430      */
431     public function set($key, Doctrine_Record $record)
432     {
433         if (isset($this->referenceField)) {
434             $record->set($this->referenceField, $this->reference, false);
435         }
436
437         $this->data[$key] = $record;
438     }
439
440     /**
441      * adds a record to collection
442      * @param Doctrine_Record $record              record to be added
443      * @param string $key                          optional key for the record
444      * @return boolean
445      */
446     public function add(Doctrine_Record $record, $key = null)
447     {
448         if (isset($this->referenceField)) {
449             $value = $this->reference->get($this->relation->getLocalFieldName());
450
451             if ($value !== null) {
452                 $record->set($this->referenceField, $value, false);
453             } else {
454                 $record->set($this->referenceField, $this->reference, false);
455             }
456         }
457         /**
458          * for some weird reason in_array cannot be used here (php bug ?)
459          *
460          * if used it results in fatal error : [ nesting level too deep ]
461          */
462         foreach ($this->data as $val) {
463             if ($val === $record) {
464                 return false;
465             }
466         }
467
468         if (isset($key)) {
469             if (isset($this->data[$key])) {
470                 return false;
471             }
472             $this->data[$key] = $record;
473             return true;
474         }
475
476         if (isset($this->keyColumn)) {
477             $value = $record->get($this->keyColumn);
478             if ($value === null) {
479                 throw new Doctrine_Collection_Exception("Couldn't create collection index. Record field '".$this->keyColumn."' was null.");
480             }
481             $this->data[$value] = $record;
482         } else {
483             $this->data[] = $record;
484         }
485
486         return true;
487     }
488
489     /**
490      * loadRelated
491      *
492      * @param mixed $name
493      * @return boolean
494      */
495     public function loadRelated($name = null)
496     {
497         $list = array();
498         $query   = new Doctrine_Query($this->_table->getConnection());
499
500         if ( ! isset($name)) {
501             foreach ($this->data as $record) {
502                 $value = $record->getIncremented();
503                 if ($value !== null) {
504                     $list[] = $value;
505                 }
506             }
507             $query->from($this->_table->getComponentName());
508             $query->where($this->_table->getComponentName() . '.id IN (' . substr(str_repeat("?, ", count($list)),0,-2) . ')');
509
510             return $query;
511         }
512
513         $rel     = $this->_table->getRelation($name);
514
515         if ($rel instanceof Doctrine_Relation_LocalKey || $rel instanceof Doctrine_Relation_ForeignKey) {
516             foreach ($this->data as $record) {
517                 $list[] = $record[$rel->getLocal()];
518             }
519         } else {
520             foreach ($this->data as $record) {
521                 $value = $record->getIncremented();
522                 if ($value !== null) {
523                     $list[] = $value;
524                 }
525             }
526         }
527
528         $dql     = $rel->getRelationDql(count($list), 'collection');
529
530         $coll    = $query->query($dql, $list);
531
532         $this->populateRelated($name, $coll);
533     }
534
535     /**
536      * populateRelated
537      *
538      * @param string $name
539      * @param Doctrine_Collection $coll
540      * @return void
541      */
542     public function populateRelated($name, Doctrine_Collection $coll)
543     {
544         $rel     = $this->_table->getRelation($name);
545         $table   = $rel->getTable();
546         $foreign = $rel->getForeign();
547         $local   = $rel->getLocal();
548
549         if ($rel instanceof Doctrine_Relation_LocalKey) {
550             foreach ($this->data as $key => $record) {
551                 foreach ($coll as $k => $related) {
552                     if ($related[$foreign] == $record[$local]) {
553                         $this->data[$key]->setRelated($name, $related);
554                     }
555                 }
556             }
557         } elseif ($rel instanceof Doctrine_Relation_ForeignKey) {
558             foreach ($this->data as $key => $record) {
559                 if ( ! $record->exists()) {
560                     continue;
561                 }
562                 $sub = new Doctrine_Collection($table);
563
564                 foreach ($coll as $k => $related) {
565                     if ($related[$foreign] == $record[$local]) {
566                         $sub->add($related);
567                         $coll->remove($k);
568                     }
569                 }
570
571                 $this->data[$key]->setRelated($name, $sub);
572             }
573         } elseif ($rel instanceof Doctrine_Relation_Association) {
574             $identifier = $this->_table->getIdentifier();
575             $asf        = $rel->getAssociationFactory();
576             $name       = $table->getComponentName();
577
578             foreach ($this->data as $key => $record) {
579                 if ( ! $record->exists()) {
580                     continue;
581                 }
582                 $sub = new Doctrine_Collection($table);
583                 foreach ($coll as $k => $related) {
584                     if ($related->get($local) == $record[$identifier]) {
585                         $sub->add($related->get($name));
586                     }
587                 }
588                 $this->data[$key]->setRelated($name, $sub);
589
590             }
591         }
592     }
593
594     /**
595      * getNormalIterator
596      * returns normal iterator - an iterator that will not expand this collection
597      *
598      * @return Doctrine_Iterator_Normal
599      */
600     public function getNormalIterator()
601     {
602         return new Doctrine_Collection_Iterator_Normal($this);
603     }
604
605     /**
606      * takeSnapshot
607      * takes a snapshot from this collection
608      *
609      * snapshots are used for diff processing, for example
610      * when a fetched collection has three elements, then two of those
611      * are being removed the diff would contain one element
612      *
613      * Doctrine_Collection::save() attaches the diff with the help of last
614      * snapshot.
615      *
616      * @return Doctrine_Collection
617      */
618     public function takeSnapshot()
619     {
620         $this->_snapshot = $this->data;
621         
622         return $this;
623     }
624
625     /**
626      * getSnapshot
627      * returns the data of the last snapshot
628      *
629      * @return array    returns the data in last snapshot
630      */
631     public function getSnapshot()
632     {
633         return $this->_snapshot;
634     }
635
636     /**
637      * processDiff
638      * processes the difference of the last snapshot and the current data
639      *
640      * an example:
641      * Snapshot with the objects 1, 2 and 4
642      * Current data with objects 2, 3 and 5
643      *
644      * The process would remove object 4
645      *
646      * @return Doctrine_Collection
647      */
648     public function processDiff() 
649     {
650         foreach (array_udiff($this->_snapshot, $this->data, array($this, "compareRecords")) as $record) {
651             $record->delete();
652         }
653
654         return $this;
655     }
656
657     /**
658      * toArray
659      * Mimics the result of a $query->execute(array(), Doctrine::FETCH_ARRAY);
660      *
661      * @param boolean $deep
662      */
663     public function toArray($deep = false, $prefixKey = false)
664     {
665         $data = array();
666         foreach ($this as $key => $record) {
667             
668             $key = $prefixKey ? get_class($record) . '_' .$key:$key;
669             
670             $data[$key] = $record->toArray($deep, $prefixKey);
671         }
672         
673         return $data;
674     }
675
676     /**
677      * fromArray
678      *
679      * Populate a Doctrine_Collection from an array of data
680      *
681      * @param string $array 
682      * @return void
683      */
684     public function fromArray($array, $deep = true)
685     {
686         $data = array();
687         foreach ($array as $rowKey => $row) {
688             $this[$rowKey]->fromArray($row, $deep);
689         }
690     }
691
692     /**
693      * synchronizeWithArray
694      * synchronizes a Doctrine_Collection with data from an array
695      *
696      * it expects an array representation of a Doctrine_Collection similar to the return
697      * value of the toArray() method. It will create Dectrine_Records that don't exist
698      * on the collection, update the ones that do and remove the ones missing in the $array
699      *
700      * @param array $array representation of a Doctrine_Collection
701      */
702     public function synchronizeWithArray(array $array)
703     {
704         foreach ($this as $key => $record) {
705             if (isset($array[$key])) {
706                 $record->synchronizeWithArray($array[$key]);
707                 unset($array[$key]);
708             } else {
709                 // remove records that don't exist in the array
710                 $this->remove($key);
711             }
712         }
713         // create new records for each new row in the array
714         foreach ($array as $rowKey => $row) {
715             $this[$rowKey]->fromArray($row);
716         }
717     }
718
719     // Method was wrong named. Fixing by creating an alias, since we cannot change API (remove methods)
720     public function synchronizeFromArray(array $array) {
721         return $this->synchronizeWithArray($array);
722     }
723
724     /**
725      * exportTo
726      *
727      * Export a Doctrine_Collection to one of the supported Doctrine_Parser formats
728      *
729      * @param string $type 
730      * @param string $deep 
731      * @return void
732      */
733     public function exportTo($type, $deep = false)
734     {
735         if ($type == 'array') {
736             return $this->toArray($deep);
737         } else {
738             return Doctrine_Parser::dump($this->toArray($deep, true), $type);
739         }
740     }
741
742     /**
743      * importFrom
744      *
745      * Import data to a Doctrine_Collection from one of the supported Doctrine_Parser formats
746      *
747      * @param string $type 
748      * @param string $data 
749      * @return void
750      */
751     public function importFrom($type, $data)
752     {
753         if ($type == 'array') {
754             return $this->fromArray($data);
755         } else {
756             return $this->fromArray(Doctrine_Parser::load($data, $type));
757         }
758     }
759
760     /**
761      * getDeleteDiff
762      *
763      * @return void
764      */
765     public function getDeleteDiff()
766     {
767         return array_udiff($this->_snapshot, $this->data, array($this, "compareRecords"));
768     }
769
770     /**
771      * getInsertDiff
772      *
773      * @return void
774      */
775     public function getInsertDiff()
776     {
777         return array_udiff($this->data, $this->_snapshot, array($this, "compareRecords"));
778     }
779
780     /**
781      * compareRecords
782      * Compares two records. To be used on _snapshot diffs using array_udiff
783      */
784     protected function compareRecords($a, $b)
785     {
786         if ($a->getOid() == $b->getOid()) {
787             return 0;
788         }
789         
790         return ($a->getOid() > $b->getOid()) ? 1 : -1;
791     }
792
793     /**
794      * save
795      * saves all records of this collection and processes the 
796      * difference of the last snapshot and the current data
797      *
798      * @param Doctrine_Connection $conn     optional connection parameter
799      * @return Doctrine_Collection
800      */
801     public function save(Doctrine_Connection $conn = null)
802     {
803         if ($conn == null) {
804             $conn = $this->_table->getConnection();
805         }
806         
807         $conn->beginInternalTransaction();
808
809         $conn->transaction->addCollection($this);
810
811         $this->processDiff();
812
813         foreach ($this->getData() as $key => $record) {
814             $record->save($conn);
815         }
816
817         $conn->commit();
818
819         return $this;
820     }
821
822     /**
823      * delete
824      * single shot delete
825      * deletes all records from this collection
826      * and uses only one database query to perform this operation
827      *
828      * @return Doctrine_Collection
829      */
830     public function delete(Doctrine_Connection $conn = null)
831     {
832         if ($conn == null) {
833             $conn = $this->_table->getConnection();
834         }
835
836         $conn->beginInternalTransaction();
837         $conn->transaction->addCollection($this);
838
839         foreach ($this as $key => $record) {
840             $record->delete($conn);
841         }
842
843         $conn->commit();
844
845         $this->data = array();
846         
847         return $this;
848     }
849
850
851     public function free($deep = false)
852     {
853         foreach ($this->getData() as $key => $record) {
854             if ( ! ($record instanceof Doctrine_Null)) {
855                 $record->free($deep);
856             }
857         }
858
859         $this->data = array();
860
861         if ($this->reference) {
862             $this->reference->free($deep);
863             $this->reference = null;
864         }
865     }
866
867
868     /**
869      * getIterator
870      * @return object ArrayIterator
871      */
872     public function getIterator()
873     {
874         $data = $this->data;
875         return new ArrayIterator($data);
876     }
877
878     /**
879      * returns a string representation of this object
880      */
881     public function __toString()
882     {
883         return Doctrine_Lib::getCollectionAsString($this);
884     }
885     
886     /**
887      * returns the relation object
888      * @return object Doctrine_Relation
889      */
890     public function getRelation()
891     {
892         return $this->relation;
893     }
894 }