# Coverage for Doctrine_Collection

Back to coverage report

1 | <?php
2 | /*
3 | * $Id: Collection.php 3172 2007-11-15 22:45:09Z Jonathan.Wage $
4 | *
5 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
6 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
7 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
8 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
9 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
11 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
12 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
13 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
14 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
15 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16 | *
17 | * This software consists of voluntary contributions made by many individuals
18 | * and is licensed under the LGPL. For more information, see
19 | * <http://www.phpdoctrine.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: 3172 $
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 |
665 | /**
666 | * fromArray
667 | *
668 | * Populate a Doctrine_Collection from an array of data
669 | *
670 | * @param string $array
671 | * @return void
672 | */
673 |
674 | public function fromArray($array)
675 | {
676 | $data = array();
677 | foreach ($array as $rowKey => $row) {
678 | $this[$rowKey]->fromArray($row);
679 | }
680 | }
681 |
682 | /**
683 | * exportTo
684 | *
685 | * Export a Doctrine_Collection to one of the supported Doctrine_Parser formats
686 | *
687 | * @param string $type
688 | * @param string $deep
689 | * @return void
690 | */
691 | public function exportTo($type, $deep = false)
692 | {
693 | if ($type == 'array') {
694 | return $this->toArray($deep);
695 | } else {
696 | return Doctrine_Parser::dump($this->toArray($deep, true), $type);
697 | }
698 | }
699 |
700 | /**
701 | * importFrom
702 | *
703 | * Import data to a Doctrine_Collection from one of the supported Doctrine_Parser formats
704 | *
705 | * @param string $type
706 | * @param string $data
707 | * @return void
708 | */
709 | public function importFrom($type, $data)
710 | {
711 | if ($type == 'array') {
712 | return $this->fromArray($data);
713 | } else {
714 | return $this->fromArray(Doctrine_Parser::load($data, $type));
715 | }
716 | }
717 |
718 | /**
719 | * getDeleteDiff
720 | *
721 | * @return void
722 | */
723 | public function getDeleteDiff()
724 | {
725 | return array_udiff($this->_snapshot, $this->data, array($this, "compareRecords"));
726 | }
727 |
728 | /**
729 | * getInsertDiff
730 | *
731 | * @return void
732 | */
733 | public function getInsertDiff()
734 | {
735 | return array_udiff($this->data, $this->_snapshot, array($this, "compareRecords"));
736 | }
737 |
738 | /**
739 | * compareRecords
740 | * Compares two records. To be used on _snapshot diffs using array_udiff
741 | */
742 | protected function compareRecords($a, $b)
743 | {
744 | if ($a->getOid() == $b->getOid()) {
745 | return 0;
746 | }
747 |
748 | return ($a->getOid() > $b->getOid()) ? 1 : -1;
749 | }
750 |
751 | /**
752 | * save
753 | * saves all records of this collection and processes the
754 | * difference of the last snapshot and the current data
755 | *
756 | * @param Doctrine_Connection $conn optional connection parameter
757 | * @return Doctrine_Collection
758 | */
759 | public function save(Doctrine_Connection $conn = null)
760 | {
761 | if ($conn == null) {
762 | $conn = $this->_table->getConnection();
763 | }
764 |
765 | $conn->beginTransaction();
766 |
767 | $conn->transaction->addCollection($this);
768 |
769 | $this->processDiff();
770 |
771 | foreach ($this->getData() as $key => $record) {
772 | $record->save($conn);
773 | }
774 |
775 | $conn->commit();
776 |
777 | return $this;
778 | }
779 |
780 | /**
781 | * delete
782 | * single shot delete
783 | * deletes all records from this collection
784 | * and uses only one database query to perform this operation
785 | *
786 | * @return Doctrine_Collection
787 | */
788 | public function delete(Doctrine_Connection $conn = null)
789 | {
790 | if ($conn == null) {
791 | $conn = $this->_table->getConnection();
792 | }
793 |
794 | $conn->beginTransaction();
795 | $conn->transaction->addCollection($this);
796 |
797 | foreach ($this as $key => $record) {
798 | $record->delete($conn);
799 | }
800 |
801 | $conn->commit();
802 |
803 | $this->data = array();
804 |
805 | return $this;
806 | }
807 |
808 | /**
809 | * getIterator
810 | * @return object ArrayIterator
811 | */
812 | public function getIterator()
813 | {
814 | $data = $this->data;
815 | return new ArrayIterator($data);
816 | }
817 |
818 | /**
819 | * returns a string representation of this object
820 | */
821 | public function __toString()
822 | {
823 | return Doctrine_Lib::getCollectionAsString($this);
824 | }
825 |
826 | /**
827 | * returns the relation object
828 | * @return object Doctrine_Relation
829 | */
830 | public function getRelation()
831 | {
832 | return $this->relation;
833 | }
834 | }