Coverage for Doctrine_Record

Back to coverage report

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