Coverage for Doctrine_Collection

Back to coverage report

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