Coverage for Doctrine_Record

Back to coverage report

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