Coverage for Doctrine_Record

Back to coverage report

1 <?php
2 /*
3  *  $Id: Record.php 2794 2007-10-09 20:51:42Z 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.com>.
20  */
21 Doctrine::autoload('Doctrine_Record_Abstract');
22 /**
23  * Doctrine_Record
24  * All record classes should inherit this super class
25  *
26  * @package     Doctrine
27  * @subpackage  Record
28  * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
29  * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
30  * @link        www.phpdoctrine.com
31  * @since       1.0
32  * @version     $Revision: 2794 $
33  */
34 abstract class Doctrine_Record extends Doctrine_Record_Abstract implements Countable, IteratorAggregate, Serializable
35 {
36     /**
37      * STATE CONSTANTS
38      */
39
40     /**
41      * DIRTY STATE
42      * a Doctrine_Record is in dirty state when its properties are changed
43      */
44     const STATE_DIRTY       = 1;
45     /**
46      * TDIRTY STATE
47      * a Doctrine_Record is in transient dirty state when it is created 
48      * and some of its fields are modified but it is NOT yet persisted into database
49      */
50     const STATE_TDIRTY      = 2;
51     /**
52      * CLEAN STATE
53      * a Doctrine_Record is in clean state when all of its properties are loaded from the database
54      * and none of its properties are changed
55      */
56     const STATE_CLEAN       = 3;
57     /**
58      * PROXY STATE
59      * a Doctrine_Record is in proxy state when its properties are not fully loaded
60      */
61     const STATE_PROXY       = 4;
62     /**
63      * NEW TCLEAN
64      * a Doctrine_Record is in transient clean state when it is created and none of its fields are modified
65      */
66     const STATE_TCLEAN      = 5;
67     /**
68      * LOCKED STATE
69      * a Doctrine_Record is temporarily locked during deletes and saves
70      *
71      * This state is used internally to ensure that circular deletes
72      * and saves will not cause infinite loops
73      */
74     const STATE_LOCKED     = 6;
75
76     /**
77      * @var Doctrine_Node_<TreeImpl>        node object
78      */
79     protected $_node;
80     /**
81      * @var integer $_id                    the primary keys of this object
82      */
83     protected $_id           = array();
84     /**
85      * @var array $_data                    the record data
86      */
87     protected $_data         = array();
88     /**
89      * @var array $_values                  the values array, aggregate values and such are mapped into this array
90      */
91     protected $_values       = array();
92     /**
93      * @var integer $_state                 the state of this record
94      * @see STATE_* constants
95      */
96     protected $_state;
97     /**
98      * @var array $_modified                an array containing properties that have been modified
99      */
100     protected $_modified     = array();
101     /**
102      * @var Doctrine_Validator_ErrorStack   error stack object
103      */
104     protected $_errorStack;
105     /**
106      * @var array $_references              an array containing all the references
107      */
108     protected $_references     = array();
109     /**
110      * @var integer $index                  this index is used for creating object identifiers
111      */
112     private static $_index = 1;
113     /**
114      * @var integer $oid                    object identifier, each Record object has a unique object identifier
115      */
116     private $_oid;
117
118     /**
119      * constructor
120      * @param Doctrine_Table|null $table       a Doctrine_Table object or null,
121      *                                         if null the table object is retrieved from current connection
122      *
123      * @param boolean $isNewEntry              whether or not this record is transient
124      *
125      * @throws Doctrine_Connection_Exception   if object is created using the new operator and there are no
126      *                                         open connections
127      * @throws Doctrine_Record_Exception       if the cleanData operation fails somehow
128      */
129     public function __construct($table = null, $isNewEntry = false)
130     {
131         if (isset($table) && $table instanceof Doctrine_Table) {
132             $this->_table = $table;
133             $exists = ( ! $isNewEntry);
134         } else {
135             $class  = get_class($this);
136             // get the table of this class
137             $this->_table = Doctrine_Manager::getInstance()
138                             ->getTable($class);
139             $exists = false;
140         }
141
142         // Check if the current connection has the records table in its registry
143         // If not this record is only used for creating table definition and setting up
144         // relations.
145
146         if ($this->_table->getConnection()->hasTable($this->_table->getComponentName())) {
147             $this->_oid = self::$_index;
148
149             self::$_index++;
150
151             $keys = (array) $this->_table->getIdentifier();
152
153             // get the data array
154             $this->_data = $this->_table->getData();
155
156             // get the column count
157             $count = count($this->_data);
158
159             $this->_values = $this->cleanData($this->_data);
160
161             $this->prepareIdentifiers($exists);
162
163             if ( ! $exists) {
164                 if ($count > 0) {
165                     $this->_state = Doctrine_Record::STATE_TDIRTY;
166                 } else {
167                     $this->_state = Doctrine_Record::STATE_TCLEAN;
168                 }
169
170                 // set the default values for this record
171                 $this->assignDefaultValues();
172             } else {
173                 $this->_state      = Doctrine_Record::STATE_CLEAN;
174
175                 if ($count < $this->_table->getColumnCount()) {
176                     $this->_state  = Doctrine_Record::STATE_PROXY;
177                 }
178             }
179
180             $this->_errorStack = new Doctrine_Validator_ErrorStack(get_class($this));
181
182             $repository = $this->_table->getRepository();
183             $repository->add($this);
184             
185             $this->construct();
186         }
187         
188     }
189     /**
190      * _index
191      *
192      * @return integer
193      */
194     public static function _index()
195     {
196         return self::$_index;
197     }
198     /**
199      * setUp
200      * this method is used for setting up relations and attributes
201      * it should be implemented by child classes
202      *
203      * @return void
204      */
205     public function setUp()
206     { }
207     /**
208      * construct
209      * Empty template method to provide concrete Record classes with the possibility
210      * to hook into the constructor procedure
211      *
212      * @return void
213      */
214     public function construct()
215     { }
216     /**
217      * getOid
218      * returns the object identifier
219      *
220      * @return integer
221      */
222     public function getOid()
223     {
224         return $this->_oid;
225     }
226     /**
227      * isValid
228      *
229      * @return boolean                          whether or not this record passes all column validations
230      */
231     public function isValid()
232     {
233         if ( ! $this->_table->getAttribute(Doctrine::ATTR_VALIDATE)) {
234             return true;
235         }
236         // Clear the stack from any previous errors.
237         $this->_errorStack->clear();
238
239         // Run validation process
240         $validator = new Doctrine_Validator();
241         $validator->validateRecord($this);
242         $this->validate();
243         if ($this->_state == self::STATE_TDIRTY || $this->_state == self::STATE_TCLEAN) {
244             $this->validateOnInsert();
245         } else {
246             $this->validateOnUpdate();
247         }
248
249         return $this->_errorStack->count() == 0 ? true : false;
250     }
251     /**
252      * Empty template method to provide concrete Record classes with the possibility
253      * to hook into the validation procedure, doing any custom / specialized
254      * validations that are neccessary.
255      */
256     protected function validate()
257     { }
258     /**
259      * Empty template method to provide concrete Record classes with the possibility
260      * to hook into the validation procedure only when the record is going to be
261      * updated.
262      */
263     protected function validateOnUpdate()
264     { }
265     /**
266      * Empty template method to provide concrete Record classes with the possibility
267      * to hook into the validation procedure only when the record is going to be
268      * inserted into the data store the first time.
269      */
270     protected function validateOnInsert()
271     { }
272     /**
273      * Empty template method to provide concrete Record classes with the possibility
274      * to hook into the serializing procedure.
275      */
276     public function preSerialize($event)
277     { }
278     /**
279      * Empty template method to provide concrete Record classes with the possibility
280      * to hook into the serializing procedure.
281      */
282     public function postSerialize($event)
283     { }
284     /**
285      * Empty template method to provide concrete Record classes with the possibility
286      * to hook into the serializing procedure.
287      */
288     public function preUnserialize($event)
289     { }
290     /**
291      * Empty template method to provide concrete Record classes with the possibility
292      * to hook into the serializing procedure.
293      */
294     public function postUnserialize($event)
295     { }
296     /**
297      * Empty template method to provide concrete Record classes with the possibility
298      * to hook into the saving procedure.
299      */
300     public function preSave($event)
301     { }
302     /**
303      * Empty template method to provide concrete Record classes with the possibility
304      * to hook into the saving procedure.
305      */
306     public function postSave($event)
307     { }
308     /**
309      * Empty template method to provide concrete Record classes with the possibility
310      * to hook into the deletion procedure.
311      */
312     public function preDelete($event)
313     { }
314     /**
315      * Empty template method to provide concrete Record classes with the possibility
316      * to hook into the deletion procedure.
317      */
318     public function postDelete($event)
319     { }
320     /**
321      * Empty template method to provide concrete Record classes with the possibility
322      * to hook into the saving procedure only when the record is going to be
323      * updated.
324      */
325     public function preUpdate($event)
326     { }
327     /**
328      * Empty template method to provide concrete Record classes with the possibility
329      * to hook into the saving procedure only when the record is going to be
330      * updated.
331      */
332     public function postUpdate($event)
333     { }
334     /**
335      * Empty template method to provide concrete Record classes with the possibility
336      * to hook into the saving procedure only when the record is going to be
337      * inserted into the data store the first time.
338      */
339     public function preInsert($event)
340     { }
341     /**
342      * Empty template method to provide concrete Record classes with the possibility
343      * to hook into the saving procedure only when the record is going to be
344      * inserted into the data store the first time.
345      */
346     public function postInsert($event)
347     { }
348     /**
349      * getErrorStack
350      *
351      * @return Doctrine_Validator_ErrorStack    returns the errorStack associated with this record
352      */
353     public function getErrorStack()
354     {
355         return $this->_errorStack;
356     }
357     /**
358      * errorStack
359      * assigns / returns record errorStack
360      *
361      * @param Doctrine_Validator_ErrorStack          errorStack to be assigned for this record
362      * @return void|Doctrine_Validator_ErrorStack    returns the errorStack associated with this record
363      */
364     public function errorStack($stack = null)
365     {
366         if ($stack !== null) {
367             if ( ! ($stack instanceof Doctrine_Validator_ErrorStack)) {
368                throw new Doctrine_Record_Exception('Argument should be an instance of Doctrine_Validator_ErrorStack.');
369             }
370             $this->_errorStack = $stack;
371         } else {
372             return $this->_errorStack;
373         }
374     }
375     /**
376      * setDefaultValues
377      * sets the default values for records internal data
378      *
379      * @param boolean $overwrite                whether or not to overwrite the already set values
380      * @return boolean
381      */
382     public function assignDefaultValues($overwrite = false)
383     {
384         if ( ! $this->_table->hasDefaultValues()) {
385             return false;
386         }
387         foreach ($this->_data as $column => $value) {
388             $default = $this->_table->getDefaultValueOf($column);
389
390             if ($default === null) {
391                 $default = self::$_null;
392             }
393
394             if ($value === self::$_null || $overwrite) {
395                 $this->_data[$column] = $default;
396                 $this->_modified[]    = $column;
397                 $this->_state = Doctrine_Record::STATE_TDIRTY;
398             }
399         }
400     }
401     /**
402      * cleanData
403      *
404      * @param array $data       data array to be cleaned
405      * @return integer
406      */
407     public function cleanData(&$data)
408     {
409         $tmp = $data;
410         $data = array();
411
412         foreach ($this->getTable()->getColumnNames() as $name) {
413             if ( ! isset($tmp[$name])) {
414                 $data[$name] = self::$_null;
415             } else {
416                 $data[$name] = $tmp[$name];
417             }
418             unset($tmp[$name]);
419         }
420
421         return $tmp;
422     }
423     /**
424      * hydrate
425      * hydrates this object from given array
426      *
427      * @param array $data
428      * @return boolean
429      */
430     public function hydrate(array $data)
431     {
432         $this->_values = $this->cleanData($data);
433         $this->_data   = array_merge($this->_data, $data);
434
435         $this->prepareIdentifiers(true);
436     }
437     /**
438      * prepareIdentifiers
439      * prepares identifiers for later use
440      *
441      * @param boolean $exists               whether or not this record exists in persistent data store
442      * @return void
443      */
444     private function prepareIdentifiers($exists = true)
445     {
446         switch ($this->_table->getIdentifierType()) {
447             case Doctrine::IDENTIFIER_AUTOINC:
448             case Doctrine::IDENTIFIER_SEQUENCE:
449             case Doctrine::IDENTIFIER_NATURAL:
450                 $name = $this->_table->getIdentifier();
451
452                 if ($exists) {
453                     if (isset($this->_data[$name]) && $this->_data[$name] !== self::$_null) {
454                         $this->_id[$name] = $this->_data[$name];
455                     }
456                 }
457                 break;
458             case Doctrine::IDENTIFIER_COMPOSITE:
459                 $names = $this->_table->getIdentifier();
460
461                 foreach ($names as $name) {
462                     if ($this->_data[$name] === self::$_null) {
463                         $this->_id[$name] = null;
464                     } else {
465                         $this->_id[$name] = $this->_data[$name];
466                     }
467                 }
468                 break;
469         }
470     }
471     /**
472      * serialize
473      * this method is automatically called when this Doctrine_Record is serialized
474      *
475      * @return array
476      */
477     public function serialize()
478     {
479         $event = new Doctrine_Event($this, Doctrine_Event::RECORD_SERIALIZE);
480
481         $this->preSerialize($event);
482
483         $vars = get_object_vars($this);
484
485         unset($vars['_references']);
486         unset($vars['_table']);
487         unset($vars['_errorStack']);
488         unset($vars['_filter']);
489         unset($vars['_node']);
490
491         $name = $this->_table->getIdentifier();
492         $this->_data = array_merge($this->_data, $this->_id);
493
494         foreach ($this->_data as $k => $v) {
495             if ($v instanceof Doctrine_Record && $this->_table->getTypeOf($k) != 'object') {
496                 unset($vars['_data'][$k]);
497             } elseif ($v === self::$_null) {
498                 unset($vars['_data'][$k]);
499             } else {
500                 switch ($this->_table->getTypeOf($k)) {
501                     case 'array':
502                     case 'object':
503                         $vars['_data'][$k] = serialize($vars['_data'][$k]);
504                         break;
505                     case 'gzip':
506                         $vars['_data'][$k] = gzcompress($vars['_data'][$k]);
507                         break;
508                     case 'enum':
509                         $vars['_data'][$k] = $this->_table->enumIndex($k, $vars['_data'][$k]);
510                         break;
511                 }
512             }
513         }
514
515         $str = serialize($vars);
516         
517         $this->postSerialize($event);
518
519         return $str;
520     }
521     /**
522      * unseralize
523      * this method is automatically called everytime a Doctrine_Record object is unserialized
524      *
525      * @param string $serialized                Doctrine_Record as serialized string
526      * @throws Doctrine_Record_Exception        if the cleanData operation fails somehow
527      * @return void
528      */
529     public function unserialize($serialized)
530     {
531         $event = new Doctrine_Event($this, Doctrine_Event::RECORD_UNSERIALIZE);
532
533         $this->preUnserialize($event);
534
535         $manager    = Doctrine_Manager::getInstance();
536         $connection = $manager->getConnectionForComponent(get_class($this));
537
538         $this->_oid = self::$_index;
539         self::$_index++;
540
541         $this->_table = $connection->getTable(get_class($this));
542
543         $array = unserialize($serialized);
544
545         foreach($array as $k => $v) {
546             $this->$k = $v;
547         }
548
549         foreach ($this->_data as $k => $v) {
550
551             switch ($this->_table->getTypeOf($k)) {
552                 case 'array':
553                 case 'object':
554                     $this->_data[$k] = unserialize($this->_data[$k]);
555                     break;
556                 case 'gzip':
557                    $this->_data[$k] = gzuncompress($this->_data[$k]);
558                     break;
559                 case 'enum':
560                     $this->_data[$k] = $this->_table->enumValue($k, $this->_data[$k]);
561                     break;
562                 
563             }
564         }
565         
566         $this->_table->getRepository()->add($this);
567
568         $this->cleanData($this->_data);
569
570         $this->prepareIdentifiers($this->exists());
571         
572         $this->postUnserialize($event);
573     }
574     /**
575      * state
576      * returns / assigns the state of this record
577      *
578      * @param integer|string $state                 if set, this method tries to set the record state to $state
579      * @see Doctrine_Record::STATE_* constants
580      *
581      * @throws Doctrine_Record_State_Exception      if trying to set an unknown state
582      * @return null|integer
583      */
584     public function state($state = null)
585     {
586         if ($state == null) {
587             return $this->_state;
588         }
589         $err = false;
590         if (is_integer($state)) {
591             if ($state >= 1 && $state <= 6) {
592                 $this->_state = $state;
593             } else {
594                 $err = true;
595             }
596         } elseif (is_string($state)) {
597             $upper = strtoupper($state);
598             
599             $const = 'Doctrine_Record::STATE_' . $upper;
600             if (defined($const)) {
601                 $this->_state = constant($const);  
602             } else {
603                 $err = true;
604             }
605         }
606
607         if ($this->_state === Doctrine_Record::STATE_TCLEAN ||
608             $this->_state === Doctrine_Record::STATE_CLEAN) {
609
610             $this->_modified = array();
611         }
612
613         if ($err) {
614             throw new Doctrine_Record_State_Exception('Unknown record state ' . $state);
615         }
616     }
617     /**
618      * refresh
619      * refresh internal data from the database
620      *
621      * @throws Doctrine_Record_Exception        When the refresh operation fails (when the database row
622      *                                          this record represents does not exist anymore)
623      * @return boolean
624      */
625     public function refresh()
626     {
627         $id = $this->identifier();
628         if ( ! is_array($id)) {
629             $id = array($id);
630         }
631         if (empty($id)) {
632             return false;
633         }
634         $id = array_values($id);
635
636         // Use FETCH_ARRAY to avoid clearing object relations
637         $record = $this->getTable()->find($id, Doctrine::FETCH_ARRAY);
638
639         if ($record === false) {
640             throw new Doctrine_Record_Exception('Failed to refresh. Record does not exist.');
641         }
642
643         $this->hydrate($record);
644
645         $this->_modified = array();
646
647         $this->prepareIdentifiers();
648
649         $this->_state    = Doctrine_Record::STATE_CLEAN;
650
651         return $this;
652     }
653     
654     /**
655      * refresh
656      * refres data of related objects from the database
657      *
658      * @param string $name              name of a related component.
659      *                                  if set, this method only refreshes the specified related component
660      *
661      * @return Doctrine_Record          this object
662      */
663     public function refreshRelated($name = null)
664     {
665         if (is_null($name)) {
666             foreach ($this->_table->getRelations() as $rel) {
667                 $this->_references[$rel->getAlias()] = $rel->fetchRelatedFor($this);
668             }
669         } else {
670             $rel = $this->_table->getRelation($name);
671             $this->_references[$name] = $rel->fetchRelatedFor($this);
672         }
673     }
674
675     /**
676      * clearRelated
677      * unsets all the relationships this object has
678      *
679      * (references to related objects still remain on Table objects)
680      */
681     public function clearRelated()
682     {
683         $this->_references = array();
684     }
685     
686     /**
687      * getTable
688      * returns the table object for this record
689      *
690      * @return object Doctrine_Table        a Doctrine_Table object
691      */
692     public function getTable()
693     {
694         return $this->_table;
695     }
696     /**
697      * getData
698      * return all the internal data
699      *
700      * @return array                        an array containing all the properties
701      */
702     public function getData()
703     {
704         return $this->_data;
705     }
706     /**
707      * rawGet
708      * returns the value of a property, if the property is not yet loaded
709      * this method does NOT load it
710      *
711      * @param $name                         name of the property
712      * @throws Doctrine_Record_Exception    if trying to get an unknown property
713      * @return mixed
714      */
715
716     public function rawGet($name)
717     {
718         if ( ! isset($this->_data[$name])) {
719             throw new Doctrine_Record_Exception('Unknown property '. $name);
720         }
721         if ($this->_data[$name] === self::$_null)
722             return null;
723
724         return $this->_data[$name];
725     }
726
727     /**
728      * load
729      * loads all the unitialized properties from the database
730      *
731      * @return boolean
732      */
733     public function load()
734     {
735         // only load the data from database if the Doctrine_Record is in proxy state
736         if ($this->_state == Doctrine_Record::STATE_PROXY) {
737             $this->refresh();
738
739             $this->_state = Doctrine_Record::STATE_CLEAN;
740
741             return true;
742         }
743         return false;
744     }
745     /**
746      * get
747      * returns a value of a property or a related component
748      *
749      * @param mixed $name                       name of the property or related component
750      * @param boolean $load                     whether or not to invoke the loading procedure
751      * @throws Doctrine_Record_Exception        if trying to get a value of unknown property / related component
752      * @return mixed
753      */
754     public function get($name, $load = true)
755     {
756         $value = self::$_null;
757         $lower = strtolower($name);
758
759         $lower = $this->_table->getColumnName($lower);
760
761         if (isset($this->_data[$lower])) {
762             // check if the property is null (= it is the Doctrine_Null object located in self::$_null)
763             if ($this->_data[$lower] === self::$_null && $load) {
764                 $this->load();
765             }
766
767             if ($this->_data[$lower] === self::$_null) {
768                 $value = null;
769             } else {
770                 $value = $this->_data[$lower];
771             }
772             return $value;
773         }
774
775         if (isset($this->_values[$lower])) {
776             return $this->_values[$lower];
777         }
778
779         try {
780
781             if ( ! isset($this->_references[$name]) && $load) {
782
783                 $rel = $this->_table->getRelation($name);
784
785                 $this->_references[$name] = $rel->fetchRelatedFor($this);
786             }
787             return $this->_references[$name];
788
789         } catch(Doctrine_Table_Exception $e) { 
790
791             foreach ($this->_table->getFilters() as $filter) {
792                 if (($value = $filter->filterGet($this, $name, $value)) !== null) {
793                     return $value;
794                 }
795             }
796         }
797     }
798     /**
799      * mapValue
800      * This simple method is used for mapping values to $values property.
801      * Usually this method is used internally by Doctrine for the mapping of
802      * aggregate values.
803      *
804      * @param string $name                  the name of the mapped value
805      * @param mixed $value                  mixed value to be mapped
806      * @return void
807      */
808     public function mapValue($name, $value)
809     {
810         $name = strtolower($name);
811         $this->_values[$name] = $value;
812     }
813     /**
814      * set
815      * method for altering properties and Doctrine_Record references
816      * if the load parameter is set to false this method will not try to load uninitialized record data
817      *
818      * @param mixed $name                   name of the property or reference
819      * @param mixed $value                  value of the property or reference
820      * @param boolean $load                 whether or not to refresh / load the uninitialized record data
821      *
822      * @throws Doctrine_Record_Exception    if trying to set a value for unknown property / related component
823      * @throws Doctrine_Record_Exception    if trying to set a value of wrong type for related component
824      *
825      * @return Doctrine_Record
826      */
827     public function set($name, $value, $load = true)
828     {
829         $lower = strtolower($name);
830
831         $lower = $this->_table->getColumnName($lower);
832
833         if (isset($this->_data[$lower])) {
834             if ($value instanceof Doctrine_Record) {
835                 $type = $this->_table->getTypeOf($name);
836
837                 $id = $value->getIncremented();
838
839                 if ($id !== null && $type !== 'object') {
840                     $value = $id;
841                 }
842             }
843
844             if ($load) {
845                 $old = $this->get($lower, $load);
846             } else {
847                 $old = $this->_data[$lower];
848             }
849
850             if ($old !== $value) {
851                 if ($value === null) {
852                     $value = self::$_null;
853                 }
854
855                 $this->_data[$lower] = $value;
856                 $this->_modified[]   = $lower;
857                 switch ($this->_state) {
858                     case Doctrine_Record::STATE_CLEAN:
859                         $this->_state = Doctrine_Record::STATE_DIRTY;
860                         break;
861                     case Doctrine_Record::STATE_TCLEAN:
862                         $this->_state = Doctrine_Record::STATE_TDIRTY;
863                         break;
864                 }
865             }
866         } else {
867             try {
868                 $this->coreSetRelated($name, $value);
869             } catch(Doctrine_Table_Exception $e) {
870                 foreach ($this->_table->getFilters() as $filter) {
871                     if (($value = $filter->filterSet($this, $name, $value)) !== null) {
872                         return $value;
873                     }
874                 }
875             }
876         }
877     }
878
879     public function coreSetRelated($name, $value)
880     {
881         $rel = $this->_table->getRelation($name);
882
883         // one-to-many or one-to-one relation
884         if ($rel instanceof Doctrine_Relation_ForeignKey ||
885             $rel instanceof Doctrine_Relation_LocalKey) {
886             if ( ! $rel->isOneToOne()) {
887                 // one-to-many relation found
888                 if ( ! ($value instanceof Doctrine_Collection)) {
889                     throw new Doctrine_Record_Exception("Couldn't call Doctrine::set(), second argument should be an instance of Doctrine_Collection when setting one-to-many references.");
890                 }
891                 if (isset($this->_references[$name])) {
892                     $this->_references[$name]->setData($value->getData());
893                     return $this;
894                 }
895             } else {
896                 if ($value !== self::$_null) {
897                     // one-to-one relation found
898                     if ( ! ($value instanceof Doctrine_Record)) {
899                         throw new Doctrine_Record_Exception("Couldn't call Doctrine::set(), second argument should be an instance of Doctrine_Record or Doctrine_Null when setting one-to-one references.");
900                     }
901                     if ($rel instanceof Doctrine_Relation_LocalKey) {
902                         $foreign = $rel->getForeign();
903                         if ( ! empty($foreign) && $foreign != $value->getTable()->getIdentifier())
904                           $this->set($rel->getLocal(), $value->rawGet($foreign), false);
905                         else
906                           $this->set($rel->getLocal(), $value, false);                          
907                     } else {
908                         $value->set($rel->getForeign(), $this, false);
909                     }                            
910                 }
911             }
912
913         } elseif ($rel instanceof Doctrine_Relation_Association) {
914             // join table relation found
915             if ( ! ($value instanceof Doctrine_Collection)) {
916                 throw new Doctrine_Record_Exception("Couldn't call Doctrine::set(), second argument should be an instance of Doctrine_Collection when setting many-to-many references.");
917             }
918         }
919
920         $this->_references[$name] = $value;
921     }
922     /**
923      * contains
924      *
925      * @param string $name
926      * @return boolean
927      */
928     public function contains($name)
929     {
930         $lower = strtolower($name);
931
932         if (isset($this->_data[$lower])) {
933             return true;
934         }
935         if (isset($this->_id[$lower])) {
936             return true;
937         }
938         if (isset($this->_values[$lower])) {
939             return true;                                      
940         }
941         if (isset($this->_references[$name]) && 
942             $this->_references[$name] !== self::$_null) {
943
944             return true;
945         }
946         return false;
947     }
948     /**
949      * @param string $name
950      * @return void
951      */
952     public function __unset($name)
953     {
954         if (isset($this->_data[$name])) {
955             $this->_data[$name] = array();
956         }
957         // todo: what to do with references ?
958     }
959     /**
960      * applies the changes made to this object into database
961      * this method is smart enough to know if any changes are made
962      * and whether to use INSERT or UPDATE statement
963      *
964      * this method also saves the related components
965      *
966      * @param Doctrine_Connection $conn                 optional connection parameter
967      * @return void
968      */
969     public function save(Doctrine_Connection $conn = null)
970     {
971         if ($conn === null) {
972             $conn = $this->_table->getConnection();
973         }
974         $conn->unitOfWork->saveGraph($this);
975     }
976     /**
977      * Tries to save the object and all its related components.
978      * In contrast to Doctrine_Record::save(), this method does not
979      * throw an exception when validation fails but returns TRUE on
980      * success or FALSE on failure.
981      * 
982      * @param Doctrine_Connection $conn                 optional connection parameter
983      * @return TRUE if the record was saved sucessfully without errors, FALSE otherwise.
984      */
985     public function trySave(Doctrine_Connection $conn = null) {
986         try {
987             $this->save($conn);
988             return true;
989         } catch (Doctrine_Validator_Exception $ignored) {
990             return false;
991         }
992     }
993     /**
994      * replace
995      * Execute a SQL REPLACE query. A REPLACE query is identical to a INSERT
996      * query, except that if there is already a row in the table with the same
997      * key field values, the REPLACE query just updates its values instead of
998      * inserting a new row.
999      *
1000      * The REPLACE type of query does not make part of the SQL standards. Since
1001      * practically only MySQL and SQLIte implement it natively, this type of
1002      * query isemulated through this method for other DBMS using standard types
1003      * of queries inside a transaction to assure the atomicity of the operation.
1004      *
1005      * @param Doctrine_Connection $conn             optional connection parameter
1006      * @throws Doctrine_Connection_Exception        if some of the key values was null
1007      * @throws Doctrine_Connection_Exception        if there were no key fields
1008      * @throws PDOException                         if something fails at PDO level
1009      * @return integer                              number of rows affected
1010      */
1011     public function replace(Doctrine_Connection $conn = null)
1012     {
1013         if ($conn === null) {
1014             $conn = $this->_table->getConnection();
1015         }
1016
1017         return $conn->replace($this->_table->getTableName(), $this->getPrepared(), $this->id);
1018     }
1019     /**
1020      * returns an array of modified fields and associated values
1021      * @return array
1022      */
1023     public function getModified()
1024     {
1025         $a = array();
1026
1027         foreach ($this->_modified as $k => $v) {
1028             $a[$v] = $this->_data[$v];
1029         }
1030         return $a;
1031     }
1032     /**
1033      * getPrepared
1034      *
1035      * returns an array of modified fields and values with data preparation
1036      * adds column aggregation inheritance and converts Records into primary key values
1037      *
1038      * @param array $array
1039      * @return array
1040      */
1041     public function getPrepared(array $array = array()) 
1042     {
1043         $a = array();
1044
1045         if (empty($array)) {
1046             $array = $this->_modified;
1047         }
1048
1049         foreach ($array as $k => $v) {
1050             $type = $this->_table->getTypeOf($v);
1051
1052             if ($this->_data[$v] === self::$_null) {
1053                 $a[$v] = null;
1054                 continue;
1055             }
1056
1057             switch ($type) {
1058                 case 'array':
1059                 case 'object':
1060                     $a[$v] = serialize($this->_data[$v]);
1061                     break;
1062                 case 'gzip':
1063                     $a[$v] = gzcompress($this->_data[$v],5);
1064                     break;
1065                 case 'boolean':
1066                     $a[$v] = $this->getTable()->getConnection()->convertBooleans($this->_data[$v]);
1067                 break;
1068                 case 'enum':
1069                     $a[$v] = $this->_table->enumIndex($v, $this->_data[$v]);
1070                     break;
1071                 default:
1072                     if ($this->_data[$v] instanceof Doctrine_Record) {
1073                         $this->_data[$v] = $this->_data[$v]->getIncremented();
1074                     }
1075                     /** TODO:
1076                     if ($this->_data[$v] === null) {
1077                         throw new Doctrine_Record_Exception('Unexpected null value.');
1078                     }
1079                     */
1080
1081                     $a[$v] = $this->_data[$v];
1082             }
1083         }
1084         $map = $this->_table->inheritanceMap;
1085         foreach ($map as $k => $v) {
1086             $old = $this->get($k, false);
1087
1088             if ((string) $old !== (string) $v || $old === null) {
1089                 $a[$k] = $v;
1090                 $this->_data[$k] = $v;
1091             }
1092         }
1093
1094         return $a;
1095     }
1096     /**
1097      * count
1098      * this class implements countable interface
1099      *
1100      * @return integer          the number of columns in this record
1101      */
1102     public function count()
1103     {
1104         return count($this->_data);
1105     }
1106     /**
1107      * alias for count()
1108      *
1109      * @return integer          the number of columns in this record
1110      */
1111     public function columnCount()
1112     {
1113         return $this->count();
1114     }
1115     /**
1116      * toArray
1117      * returns the record as an array
1118      *
1119      * @param boolean $deep - Return also the relations
1120      * @return array
1121      */
1122     public function toArray($deep = false, $prefixKey = false)
1123     {
1124         $a = array();
1125
1126         foreach ($this as $column => $value) {
1127             if ($value === self::$_null) {
1128                 $value = null;
1129             }
1130             $a[$column] = $value;
1131         }
1132         if ($this->_table->getIdentifierType() ==  Doctrine::IDENTIFIER_AUTOINC) {
1133             $i      = $this->_table->getIdentifier();
1134             $a[$i]  = $this->getIncremented();
1135         }
1136         if ($deep) {
1137             foreach ($this->_references as $key => $relation) {
1138                 if (!$relation instanceof Doctrine_Null) {
1139                     $a[$key] = $relation->toArray($deep, $prefixKey);
1140                 }
1141             }
1142         }
1143         return array_merge($a, $this->_values);
1144     }
1145     public function fromArray($array)
1146     {
1147         if (is_array($array)) {
1148             foreach ($array as $key => $value) {
1149                 if ($this->getTable()->hasRelation($key) && $value) {
1150                     $this->$key->fromArray($value);
1151                 } else if($this->getTable()->hasColumn($key) && $value) {
1152                     $this->$key = $value;
1153                 }
1154             }
1155         }
1156     }
1157     public function exportTo($type, $deep = false)
1158     {
1159         if ($type == 'array') {
1160             return $this->toArray($deep);
1161         } else {
1162             return Doctrine_Parser::dump($this->toArray($deep, true), $type);
1163         }
1164     }
1165     public function importFrom($type, $data)
1166     {
1167         if ($type == 'array') {
1168             return $this->fromArray($data);
1169         } else {
1170             return $this->fromArray(Doctrine_Parser::load($data, $type));
1171         }
1172     }
1173     /**
1174      * exists
1175      * returns true if this record is persistent, otherwise false
1176      *
1177      * @return boolean
1178      */
1179     public function exists()
1180     {
1181         return ($this->_state !== Doctrine_Record::STATE_TCLEAN &&
1182                 $this->_state !== Doctrine_Record::STATE_TDIRTY);
1183     }
1184     /**
1185      * isModified
1186      * returns true if this record was modified, otherwise false
1187      *
1188      * @return boolean
1189      */
1190     public function isModified()
1191     {
1192         return ($this->_state === Doctrine_Record::STATE_DIRTY ||
1193                 $this->_state === Doctrine_Record::STATE_TDIRTY);
1194     }
1195     /**
1196      * method for checking existence of properties and Doctrine_Record references
1197      * @param mixed $name               name of the property or reference
1198      * @return boolean
1199      */
1200     public function hasRelation($name)
1201     {
1202         if (isset($this->_data[$name]) || isset($this->_id[$name])) {
1203             return true;
1204         }
1205         return $this->_table->hasRelation($name);
1206     }
1207     /**
1208      * getIterator
1209      * @return Doctrine_Record_Iterator     a Doctrine_Record_Iterator that iterates through the data
1210      */
1211     public function getIterator()
1212     {
1213         return new Doctrine_Record_Iterator($this);
1214     }
1215     /**
1216      * deletes this data access object and all the related composites
1217      * this operation is isolated by a transaction
1218      *
1219      * this event can be listened by the onPreDelete and onDelete listeners
1220      *
1221      * @return boolean      true on success, false on failure
1222      */
1223     public function delete(Doctrine_Connection $conn = null)
1224     {
1225         if ($conn == null) {
1226             $conn = $this->_table->getConnection();
1227         }
1228         return $conn->unitOfWork->delete($this);
1229     }
1230     /**
1231      * copy
1232      * returns a copy of this object
1233      *
1234      * @return Doctrine_Record
1235      */
1236     public function copy()
1237     {
1238         $data = $this->_data;
1239
1240         if ($this->_table->getIdentifierType() === Doctrine::IDENTIFIER_AUTOINC) {
1241             $id = $this->_table->getIdentifier();
1242
1243             unset($data[$id]);
1244         }
1245
1246         $ret = $this->_table->create($data);
1247         $modified = array();
1248
1249         foreach ($data as $key => $val) {
1250             if ( ! ($val instanceof Doctrine_Null)) {
1251                 $ret->_modified[] = $key;
1252             }
1253         }
1254         
1255
1256         return $ret;
1257     }
1258     /**
1259      * copyDeep
1260      * returns a copy of this object and all its related objects
1261      *
1262      * @return Doctrine_Record
1263      */
1264     public function copyDeep() {
1265         $copy = $this->copy();
1266
1267         foreach ($this->_references as $key => $value) {
1268             if ($value instanceof Doctrine_Collection) {
1269                 foreach ($value as $record) {
1270                     $copy->{$key}[] = $record->copyDeep();
1271                 }
1272             } else {
1273                 $copy->set($key, $value->copyDeep());
1274             }
1275         }
1276         return $copy;
1277     }
1278     
1279     /**
1280      * assignIdentifier
1281      *
1282      * @param integer $id
1283      * @return void
1284      */
1285     public function assignIdentifier($id = false)
1286     {
1287         if ($id === false) {
1288             $this->_id       = array();
1289             $this->_data     = $this->cleanData($this->_data);
1290             $this->_state    = Doctrine_Record::STATE_TCLEAN;
1291             $this->_modified = array();
1292         } elseif ($id === true) {
1293             $this->prepareIdentifiers(true);
1294             $this->_state    = Doctrine_Record::STATE_CLEAN;
1295             $this->_modified = array();
1296         } else {
1297             $name             = $this->_table->getIdentifier();   
1298             $this->_id[$name] = $id;
1299             $this->_data[$name] = $id;
1300             $this->_state     = Doctrine_Record::STATE_CLEAN;
1301             $this->_modified  = array();
1302         }
1303     }
1304     /**
1305      * returns the primary keys of this object
1306      *
1307      * @return array
1308      */
1309     public function identifier()
1310     {
1311         return $this->_id;
1312     }
1313     /**
1314      * returns the value of autoincremented primary key of this object (if any)
1315      *
1316      * @return integer
1317      */
1318     final public function getIncremented()
1319     {
1320         $id = current($this->_id);
1321         if ($id === false) {
1322             return null;
1323         }
1324
1325         return $id;
1326     }
1327     /**
1328      * getLast
1329      * this method is used internally be Doctrine_Query
1330      * it is needed to provide compatibility between
1331      * records and collections
1332      *
1333      * @return Doctrine_Record
1334      */
1335     public function getLast()
1336     {
1337         return $this;
1338     }
1339     /**
1340      * hasRefence
1341      * @param string $name
1342      * @return boolean
1343      */
1344     public function hasReference($name)
1345     {
1346         return isset($this->_references[$name]);
1347     }
1348     /**
1349      * reference
1350      *
1351      * @param string $name
1352      */
1353     public function reference($name)
1354     {
1355         if (isset($this->_references[$name])) {
1356             return $this->_references[$name];
1357         }
1358     }
1359     /**
1360      * obtainReference
1361      *
1362      * @param string $name
1363      * @throws Doctrine_Record_Exception        if trying to get an unknown related component
1364      */
1365     public function obtainReference($name)
1366     {
1367         if (isset($this->_references[$name])) {
1368             return $this->_references[$name];
1369         }
1370         throw new Doctrine_Record_Exception("Unknown reference $name");
1371     }
1372     /**
1373      * getReferences
1374      * @return array    all references
1375      */
1376     public function getReferences()
1377     {
1378         return $this->_references;
1379     }
1380     /**
1381      * setRelated
1382      *
1383      * @param string $alias
1384      * @param Doctrine_Access $coll
1385      */
1386     final public function setRelated($alias, Doctrine_Access $coll)
1387     {
1388         $this->_references[$alias] = $coll;
1389     }
1390     /**
1391      * loadReference
1392      * loads a related component
1393      *
1394      * @throws Doctrine_Table_Exception             if trying to load an unknown related component
1395      * @param string $name
1396      * @return void
1397      */
1398     public function loadReference($name)
1399     {
1400         $rel = $this->_table->getRelation($name);
1401         $this->_references[$name] = $rel->fetchRelatedFor($this);
1402     }
1403
1404     /**
1405      * merge
1406      * merges this record with an array of values
1407      *
1408      * @param array $values
1409      * @return void
1410      */
1411     public function merge(array $values)
1412     {
1413         foreach ($this->_table->getColumnNames() as $value) {
1414             try {
1415                 if (isset($values[$value])) {
1416                     $this->set($value, $values[$value]);
1417                 }
1418             } catch(Exception $e) {
1419                 // silence all exceptions
1420             }
1421         }
1422     }
1423     
1424     /**
1425      * call
1426      *
1427      * @param string|array $callback    valid callback
1428      * @param string $column            column name
1429      * @param mixed arg1 ... argN       optional callback arguments
1430      * @return Doctrine_Record
1431      */
1432     public function call($callback, $column)
1433     {
1434         $args = func_get_args();
1435         array_shift($args);
1436
1437         if (isset($args[0])) {
1438             $column = $args[0];
1439             $args[0] = $this->get($column);
1440
1441             $newvalue = call_user_func_array($callback, $args);
1442
1443             $this->_data[$column] = $newvalue;
1444         }
1445         return $this;
1446     }
1447     /**
1448      * getter for node assciated with this record
1449      *
1450      * @return mixed if tree returns Doctrine_Node otherwise returns false
1451      */    
1452     public function getNode() 
1453     {
1454         if ( ! $this->_table->isTree()) {
1455             return false;
1456         }
1457
1458         if ( ! isset($this->_node)) {
1459             $this->_node = Doctrine_Node::factory($this,
1460                                               $this->getTable()->getOption('treeImpl'),
1461                                               $this->getTable()->getOption('treeOptions')
1462                                               );
1463         }
1464         
1465         return $this->_node;
1466     }
1467
1468     public function unshiftFilter(Doctrine_Record_Filter $filter)
1469     {
1470         return $this->_table->unshiftFilter($filter);
1471     }
1472     /**
1473      * revert
1474      * reverts this record to given version, this method only works if versioning plugin
1475      * is enabled
1476      *
1477      * @throws Doctrine_Record_Exception    if given version does not exist
1478      * @param integer $version      an integer > 1
1479      * @return Doctrine_Record      this object
1480      */
1481     public function revert($version)
1482     {
1483         $data = $this->_table
1484                 ->getTemplate('Doctrine_Template_Versionable')
1485                 ->getAuditLog()
1486                 ->getVersion($this, $version);
1487
1488         if ( ! isset($data[0])) {
1489             throw new Doctrine_Record_Exception('Version ' . $version . ' does not exist!');
1490         }
1491
1492         $this->_data = $data[0];
1493
1494         return $this;
1495     }
1496     /**
1497      * unlink
1498      * removes links from this record to given records
1499      * if no ids are given, it removes all links
1500      *
1501      * @param string $alias     related component alias
1502      * @param array $ids        the identifiers of the related records
1503      * @return Doctrine_Record  this object
1504      */
1505     public function unlink($alias, $ids = array())
1506     {
1507         $ids = (array) $ids;
1508         
1509         $q = new Doctrine_Query();
1510
1511         $rel = $this->getTable()->getRelation($alias);
1512
1513         if ($rel instanceof Doctrine_Relation_Association) {
1514             $q->delete()
1515               ->from($rel->getAssociationTable()->getComponentName())
1516               ->where($rel->getLocal() . ' = ?', array_values($this->identifier()));
1517
1518             if (count($ids) > 0) {
1519                 $q->whereIn($rel->getForeign(), $ids);
1520             }
1521
1522             $q->execute();
1523
1524
1525         } elseif ($rel instanceof Doctrine_Relation_ForeignKey) {
1526             $q->update($rel->getTable()->getComponentName())
1527               ->set($rel->getForeign(), '?', array(null))
1528               ->addWhere($rel->getForeign() . ' = ?', array_values($this->identifier()));
1529
1530             if (count($ids) > 0) {
1531                 $q->whereIn($rel->getTable()->getIdentifier(), $ids);
1532             }
1533
1534             $q->execute();
1535         }
1536         if (isset($this->_references[$alias])) {
1537             foreach ($this->_references[$alias] as $k => $record) {
1538                 if (in_array(current($record->identifier()), $ids)) {
1539                     $this->_references[$alias]->remove($k);
1540                 }
1541             }
1542             $this->_references[$alias]->takeSnapshot();
1543         }
1544         return $this;
1545     }
1546     /**
1547      * __call
1548      * this method is a magic method that is being used for method overloading
1549      *
1550      * the function of this method is to try to find given method from the templates
1551      * this record is using and if it finds given method it will execute it
1552      *
1553      * So, in sense, this method replicates the usage of mixins (as seen in some programming languages)
1554      *
1555      * @param string $method        name of the method
1556      * @param array $args           method arguments
1557      * @return mixed                the return value of the given method
1558      */
1559     public function __call($method, $args) 
1560     {
1561         if (($template = $this->_table->getMethodOwner($method)) !== false) {
1562             $template->setInvoker($this);
1563             return call_user_func_array(array($template, $method), $args);
1564         }
1565         
1566         foreach ($this->_table->getTemplates() as $template) {
1567             if (method_exists($template, $method)) {
1568                 $template->setInvoker($this);
1569                 $this->_table->setMethodOwner($method, $template);
1570                 
1571                 return call_user_func_array(array($template, $method), $args);
1572             }
1573         }
1574         
1575         throw new Doctrine_Record_Exception('Unknown method ' . $method);
1576     }
1577     /**
1578      * used to delete node from tree - MUST BE USE TO DELETE RECORD IF TABLE ACTS AS TREE
1579      *
1580      */    
1581     public function deleteNode() {
1582         $this->getNode()->delete();
1583     }
1584     public function toString()
1585     {
1586         return Doctrine::dump(get_object_vars($this));
1587     }
1588     /**
1589      * returns a string representation of this object
1590      */
1591     public function __toString()
1592     {
1593         return (string) $this->_oid;
1594     }
1595 }