Coverage for Doctrine_Record

Back to coverage report

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