Coverage for Doctrine_Table

Back to coverage report

1 <?php
2 /*
3  *  $Id: Table.php 2964 2007-10-21 07:24:18Z Jonathan.Wage $
4  *
5  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
6  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
7  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
8  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
9  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
11  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
12  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
13  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
14  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
15  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16  *
17  * This software consists of voluntary contributions made by many individuals
18  * and is licensed under the LGPL. For more information, see
19  * <http://www.phpdoctrine.com>.
20  */
21
22 /**
23  * Doctrine_Table   represents a database table
24  *                  each Doctrine_Table holds the information of foreignKeys and associations
25  *
26  *
27  * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
28  * @package     Doctrine
29  * @subpackage  Table
30  * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
31  * @version     $Revision: 2964 $
32  * @link        www.phpdoctrine.com
33  * @since       1.0
34  */
35 class Doctrine_Table extends Doctrine_Configurable implements Countable
36 {
37     /**
38      * @var array $data                                 temporary data which is then loaded into Doctrine_Record::$data
39      */
40     protected $_data             = array();
41
42     /**
43      * @var mixed $identifier
44      */
45     protected $_identifier;
46
47     /**
48      * @see Doctrine_Identifier constants
49      * @var integer $identifierType                     the type of identifier this table uses
50      */
51     protected $_identifierType;
52
53     /**
54      * @var Doctrine_Connection $conn                   Doctrine_Connection object that created this table
55      */
56     protected $_conn;
57
58     /**
59      * @var array $identityMap                          first level cache
60      */
61     protected $_identityMap        = array();
62
63     /**
64      * @var Doctrine_Table_Repository $repository       record repository
65      */
66     protected $_repository;
67
68     /**
69      * @var array $columns                  an array of column definitions,
70      *                                      keys as column names and values as column definitions
71      *
72      *                                      the definition array has atleast the following values:
73      *
74      *                                      -- type         the column type, eg. 'integer'
75      *                                      -- length       the column length, eg. 11
76      *
77      *                                      additional keys:
78      *                                      -- notnull      whether or not the column is marked as notnull
79      *                                      -- values       enum values
80      *                                      -- notblank     notblank validator + notnull constraint
81      *                                      ... many more
82      */
83     protected $_columns          = array();
84
85     /**
86      * @var array $columnAliases            an array of column aliases
87      *                                      keys as column aliases and values as column names
88      */
89     protected $_columnAliases    = array();
90
91     /**
92      * @var integer $columnCount            cached column count, Doctrine_Record uses this column count in when
93      *                                      determining its state
94      */
95     protected $columnCount;
96
97     /**
98      * @var boolean $hasDefaultValues       whether or not this table has default values
99      */
100     protected $hasDefaultValues;
101
102     /**
103      * @var array $options                  an array containing all options
104      *
105      *      -- name                         name of the component, for example component name of the GroupTable is 'Group'
106      *
107      *      -- parents                      the parent classes of this component
108      *
109      *      -- declaringClass               name of the table definition declaring class (when using inheritance the class
110      *                                      that defines the table structure can be any class in the inheritance hierarchy,
111      *                                      hence we need reflection to check out which class actually calls setTableDefinition)
112      *
113      *      -- tableName                    database table name, in most cases this is the same as component name but in some cases
114      *                                      where one-table-multi-class inheritance is used this will be the name of the inherited table
115      *
116      *      -- sequenceName                 Some databases need sequences instead of auto incrementation primary keys,
117      *                                      you can set specific sequence for your table by calling setOption('sequenceName', $seqName)
118      *                                      where $seqName is the name of the desired sequence
119      *
120      *      -- enumMap                      enum value arrays
121      *
122      *      -- inheritanceMap               inheritanceMap is used for inheritance mapping, keys representing columns and values
123      *                                      the column values that should correspond to child classes
124      *
125      *      -- type                         table type (mysql example: INNODB)
126      *
127      *      -- charset                      character set
128      *
129      *      -- foreignKeys                  the foreign keys of this table
130      *
131      *      -- checks                       the check constraints of this table, eg. 'price > dicounted_price'
132      *
133      *      -- collation                    collation attribute
134      *
135      *      -- indexes                      the index definitions of this table
136      *
137      *      -- treeImpl                     the tree implementation of this table (if any)
138      *
139      *      -- treeOptions                  the tree options
140      *
141      *      -- queryParts                   the bound query parts
142      *
143      *      -- versioning
144      */
145     protected $_options      = array('name'           => null,
146                                      'tableName'      => null,
147                                      'sequenceName'   => null,
148                                      'inheritanceMap' => array(),
149                                      'enumMap'        => array(),
150                                      'type'           => null,
151                                      'charset'        => null,
152                                      'collation'      => null,
153                                      'treeImpl'       => null,
154                                      'treeOptions'    => null,
155                                      'indexes'        => array(),
156                                      'parents'        => array(),
157                                      'queryParts'     => array(),
158                                      'versioning'     => null,
159                                      );
160
161     /**
162      * @var Doctrine_Tree $tree                 tree object associated with this table
163      */
164     protected $_tree;
165
166     /**
167      * @var Doctrine_Relation_Parser $_parser   relation parser object
168      */
169     protected $_parser;
170
171     /**
172      * @var array $_templates                   an array containing all templates attached to this table
173      */
174     protected $_templates   = array();
175
176     /**
177      * @var array $_filters                     an array containing all record filters attached to this table
178      */
179     protected $_filters     = array();
180
181     /**
182      * @var array $_invokedMethods              method invoker cache
183      */
184     protected $_invokedMethods = array();
185
186
187
188     /**
189      * the constructor
190      *
191      * @throws Doctrine_Connection_Exception    if there are no opened connections
192      * @param string $name                      the name of the component
193      * @param Doctrine_Connection $conn         the connection associated with this table
194      */
195     public function __construct($name, Doctrine_Connection $conn, $initDefinition = false)
196     {
197         $this->_conn = $conn;
198
199         $this->setParent($this->_conn);
200
201         $this->_options['name'] = $name;
202         $this->_parser = new Doctrine_Relation_Parser($this);
203
204         if ($initDefinition) {
205             $record = $this->initDefinition($name);
206     
207             $this->initIdentifier();
208     
209             $record->setUp();
210     
211             // if tree, set up tree
212             if ($this->isTree()) {
213                 $this->getTree()->setUp();
214             }
215         }
216         $this->_filters[]  = new Doctrine_Record_Filter_Standard();
217         $this->_repository = new Doctrine_Table_Repository($this);
218     }
219     public function initDefinition($name)
220     {
221         if ( ! class_exists($name) || empty($name)) {
222             throw new Doctrine_Exception("Couldn't find class " . $name);
223         }
224         $record = new $name($this);
225
226         $names = array();
227
228         $class = $name;
229
230         // get parent classes
231
232         do {
233             if ($class === 'Doctrine_Record') {
234                 break;
235             }
236
237             $name = $class;
238             $names[] = $name;
239         } while ($class = get_parent_class($class));
240
241         if ($class === false) {
242             throw new Doctrine_Table_Exception('Unknown component.');
243         }
244
245         // reverse names
246         $names = array_reverse($names);
247         // save parents
248         array_pop($names);
249         $this->_options['parents'] = $names;
250
251         // create database table
252         if (method_exists($record, 'setTableDefinition')) {
253             $record->setTableDefinition();
254             // get the declaring class of setTableDefinition method
255             $method = new ReflectionMethod($this->_options['name'], 'setTableDefinition');
256             $class  = $method->getDeclaringClass();
257         } else {
258             $class = new ReflectionClass($class);
259         }
260         $this->_options['declaringClass'] = $class;
261
262         // set the table definition for the given tree implementation
263         if ($this->isTree()) {
264             $this->getTree()->setTableDefinition();
265         }
266
267         $this->columnCount = count($this->_columns);
268         
269         if ( ! isset($this->_options['tableName'])) {
270             $this->_options['tableName'] = Doctrine::tableize($class->getName());
271         }
272         
273         return $record;
274     }
275     public function initIdentifier()
276     {
277         switch (count($this->_identifier)) {
278             case 0:
279                 $this->_columns = array_merge(array('id' =>
280                                               array('type'          => 'integer',
281                                                     'length'        => 20,
282                                                     'autoincrement' => true,
283                                                     'primary'       => true)), $this->_columns);
284                 $this->_identifier = 'id';
285                 $this->_identifierType = Doctrine::IDENTIFIER_AUTOINC;
286                 $this->columnCount++;
287                 break;
288             case 1:
289                 foreach ($this->_identifier as $pk) {
290                     $e = $this->_columns[$pk];
291
292                     $found = false;
293
294                     foreach ($e as $option => $value) {
295                         if ($found) {
296                             break;
297                         }
298
299                         $e2 = explode(':', $option);
300
301                         switch (strtolower($e2[0])) {
302                             case 'autoincrement':
303                             case 'autoinc':
304                                 $this->_identifierType = Doctrine::IDENTIFIER_AUTOINC;
305                                 $found = true;
306                                 break;
307                             case 'seq':
308                             case 'sequence':
309                                 $this->_identifierType = Doctrine::IDENTIFIER_SEQUENCE;
310                                 $found = true;
311
312                                 if ($value) {
313                                     $this->_options['sequenceName'] = $value;
314                                 } else {
315                                     if (($sequence = $this->getAttribute(Doctrine::ATTR_DEFAULT_SEQUENCE)) !== null) {
316                                         $this->_options['sequenceName'] = $sequence;
317                                     } else {
318                                         $this->_options['sequenceName'] = $this->_conn->getSequenceName($this->_options['tableName']);
319                                     }
320                                 }
321                                 break;
322                         }
323                     }
324                     if ( ! isset($this->_identifierType)) {
325                         $this->_identifierType = Doctrine::IDENTIFIER_NATURAL;
326                     }
327                 }
328
329                 $this->_identifier = $pk;
330
331                 break;
332             default:
333                 $this->_identifierType = Doctrine::IDENTIFIER_COMPOSITE;
334         }
335     }
336
337     public function getMethodOwner($method) 
338     {
339         return (isset($this->_invokedMethods[$method])) ?
340                       $this->_invokedMethods[$method] : false;
341     }
342     
343     public function setMethodOwner($method, $class)
344     {
345         $this->_invokedMethods[$method] = $class;
346     }
347
348     /**
349      * getTemplates
350      * returns all templates attached to this table
351      *
352      * @return array     an array containing all templates
353      */
354     public function getTemplates()
355     {
356         return $this->_templates;
357     }
358
359     /**
360      * export
361      * exports this table to database based on column and option definitions
362      *
363      * @throws Doctrine_Connection_Exception    if some error other than Doctrine::ERR_ALREADY_EXISTS
364      *                                          occurred during the create table operation
365      * @return boolean                          whether or not the export operation was successful
366      *                                          false if table already existed in the database
367      */
368     public function export()
369     {
370         $this->_conn->export->exportTable($this);
371     }
372
373     /**
374      * getExportableFormat
375      * returns exportable presentation of this object
376      *
377      * @return array
378      */
379     public function getExportableFormat($parseForeignKeys = true)
380     {
381         $columns = array();
382         $primary = array();
383
384         foreach ($this->getColumns() as $name => $column) {
385             $definition = $column;
386
387             switch ($definition['type']) {
388                 case 'enum':
389                     if (isset($definition['default'])) {
390                         $definition['default'] = $this->enumIndex($name, $definition['default']);
391                     }
392                     break;
393                 case 'boolean':
394                     if (isset($definition['default'])) {
395                         $definition['default'] = $this->getConnection()->convertBooleans($definition['default']);
396                     }
397                     break;
398             }
399             $columns[$name] = $definition;
400
401             if (isset($definition['primary']) && $definition['primary']) {
402                 $primary[] = $name;
403             }
404         }
405         $options['foreignKeys'] = array();
406
407         if ($parseForeignKeys) {
408             if ($this->getAttribute(Doctrine::ATTR_EXPORT) & Doctrine::EXPORT_CONSTRAINTS) {
409
410                 $constraints = array();
411
412                 $emptyIntegrity = array('onUpdate' => null,
413                                         'onDelete' => null);
414
415                 foreach ($this->getRelations() as $name => $relation) {
416                     $fk = $relation->toArray();
417                     $fk['foreignTable'] = $relation->getTable()->getTableName();
418
419                     if ($relation->getTable() === $this && in_array($relation->getLocal(), $primary)) {
420                         if ($relation->hasConstraint()) {
421                             throw new Doctrine_Table_Exception("Badly constructed integrity constraints.");
422                         }
423
424                         continue;
425                     }
426
427                     $integrity = array('onUpdate' => $fk['onUpdate'],
428                                        'onDelete' => $fk['onDelete']);
429
430                     if ($relation instanceof Doctrine_Relation_LocalKey) {
431                         $def = array('local'        => $relation->getLocal(),
432                                      'foreign'      => $relation->getForeign(),
433                                      'foreignTable' => $relation->getTable()->getTableName());
434
435                         if (($key = array_search($def, $options['foreignKeys'])) === false) {
436                             $options['foreignKeys'][] = $def;
437
438                             $constraints[] = $integrity;
439                         } else {
440                             if ($integrity !== $emptyIntegrity) {
441                                 $constraints[$key] = $integrity;
442                             }
443                         }
444                     }
445                 }
446
447                 foreach ($constraints as $k => $def) {
448                     $options['foreignKeys'][$k] = array_merge($options['foreignKeys'][$k], $def);
449                 }
450
451             }
452         }
453         $options['primary'] = $primary;
454
455         return array('tableName' => $this->getOption('tableName'),
456                      'columns'   => $columns,
457                      'options'   => array_merge($this->getOptions(), $options));
458     }
459
460     /**
461      * exportConstraints
462      * exports the constraints of this table into database based on option definitions
463      *
464      * @throws Doctrine_Connection_Exception    if something went wrong on db level
465      * @return void
466      */
467     public function exportConstraints()
468     {
469         try {
470             $this->_conn->beginTransaction();
471
472             foreach ($this->_options['index'] as $index => $definition) {
473                 $this->_conn->export->createIndex($this->_options['tableName'], $index, $definition);
474             }
475             $this->_conn->commit();
476         } catch(Doctrine_Connection_Exception $e) {
477             $this->_conn->rollback();
478
479             throw $e;
480         }
481     }
482
483     /**
484      * getRelationParser
485      * return the relation parser associated with this table
486      *
487      * @return Doctrine_Relation_Parser     relation parser object
488      */
489     public function getRelationParser()
490     {
491         return $this->_parser;
492     }
493
494     /**
495      * __get
496      * an alias for getOption
497      *
498      * @param string $option
499      */
500     public function __get($option)
501     {
502         if (isset($this->_options[$option])) {
503             return $this->_options[$option];
504         }
505         return null;
506     }
507
508     /**
509      * __isset
510      *
511      * @param string $option
512      */
513     public function __isset($option)
514     {
515         return isset($this->_options[$option]);
516     }
517
518     /**
519      * getOptions
520      * returns all options of this table and the associated values
521      *
522      * @return array    all options and their values
523      */
524     public function getOptions()
525     {
526         return $this->_options;
527     }
528
529     /**
530      * addForeignKey
531      *
532      * adds a foreignKey to this table
533      *
534      * @return void
535      */
536     public function addForeignKey(array $definition)
537     {
538         $this->_options['foreignKeys'][] = $definition;
539     }
540
541     /**
542      * addCheckConstraint
543      *
544      * adds a check constraint to this table
545      *
546      * @return void
547      */
548     public function addCheckConstraint($definition, $name)
549     {
550         if (is_string($name)) {
551             $this->_options['checks'][$name] = $definition;
552         } else {
553             $this->_options['checks'][] = $definition;
554         }
555
556         return $this;
557     }
558
559     /**
560      * addIndex
561      *
562      * adds an index to this table
563      *
564      * @return void
565      */
566     public function addIndex($index, array $definition)
567     {
568         $this->_options['indexes'][$index] = $definition;
569     }
570
571     /**
572      * getIndex
573      *
574      * @return array|boolean        array on success, FALSE on failure
575      */
576     public function getIndex($index)
577     {
578         if (isset($this->_options['indexes'][$index])) {
579             return $this->_options['indexes'][$index];
580         }
581
582         return false;
583     }
584     public function bind($args, $type)
585     {
586         $options = array();
587         $options['type'] = $type;
588
589         if ( ! isset($args[1])) {
590             $args[1] = array();
591         }
592
593         // the following is needed for backwards compatibility
594         if (is_string($args[1])) {
595             if ( ! isset($args[2])) {
596                 $args[2] = array();
597             } elseif (is_string($args[2])) {
598                 $args[2] = (array) $args[2];
599             }
600
601             $classes = array_merge($this->_options['parents'], array($this->getComponentName()));
602
603
604             $e = explode('.', $args[1]);
605             if (in_array($e[0], $classes)) {
606                 if ($options['type'] >= Doctrine_Relation::MANY) {
607                     $options['foreign'] = $e[1];
608                 } else {
609                     $options['local'] = $e[1];
610                 }
611             } else {
612                 $e2 = explode(' as ', $args[0]);
613                 if ($e[0] !== $e2[0] && ( ! isset($e2[1]) || $e[0] !== $e2[1])) {
614                     $options['refClass'] = $e[0];
615                 }
616
617                 $options['foreign'] = $e[1];
618             }
619
620             $options = array_merge($args[2], $options);
621
622             $this->_parser->bind($args[0], $options);
623         } else {
624             $options = array_merge($args[1], $options);
625             $this->_parser->bind($args[0], $options);
626         }
627     }
628
629     /**
630      * hasRelation
631      *
632      * @param string $alias      the relation to check if exists
633      * @return boolean           true if the relation exists otherwise false
634      */
635     public function hasRelation($alias)
636     {
637         return $this->_parser->hasRelation($alias);
638     }
639
640     /**
641      * getRelation
642      *
643      * @param string $alias      relation alias
644      */
645     public function getRelation($alias, $recursive = true)
646     {
647         return $this->_parser->getRelation($alias, $recursive);
648     }
649
650     /**
651      * getRelations
652      * returns an array containing all relation objects
653      *
654      * @return array        an array of Doctrine_Relation objects
655      */
656     public function getRelations()
657     {
658         return $this->_parser->getRelations();
659     }
660
661     /**
662      * createQuery
663      * creates a new Doctrine_Query object and adds the component name
664      * of this table as the query 'from' part
665      *
666      * @param string Optional alias name for component aliasing.
667      *
668      * @return Doctrine_Query
669      */
670     public function createQuery($alias = '')
671     {
672         if ( ! empty($alias)) {
673             $alias = ' ' . trim($alias);
674         }
675         return Doctrine_Query::create($this->_conn)->from($this->getComponentName() . $alias);
676     }
677
678     /**
679      * getRepository
680      *
681      * @return Doctrine_Table_Repository
682      */
683     public function getRepository()
684     {
685         return $this->_repository;
686     }
687
688     /**
689      * setOption
690      * sets an option and returns this object in order to
691      * allow flexible method chaining
692      *
693      * @see Doctrine_Table::$_options   for available options
694      * @param string $name              the name of the option to set
695      * @param mixed $value              the value of the option
696      * @return Doctrine_Table           this object
697      */
698     public function setOption($name, $value)
699     {
700         switch ($name) {
701         case 'name':
702         case 'tableName':
703             break;
704         case 'enumMap':
705         case 'inheritanceMap':
706         case 'index':
707         case 'treeOptions':
708             if ( ! is_array($value)) {
709                 throw new Doctrine_Table_Exception($name . ' should be an array.');
710             }
711             break;
712         }
713         $this->_options[$name] = $value;
714     }
715
716     /**
717      * getOption
718      * returns the value of given option
719      *
720      * @param string $name  the name of the option
721      * @return mixed        the value of given option
722      */
723     public function getOption($name)
724     {
725         if (isset($this->_options[$name])) {
726             return $this->_options[$name];
727         }
728         return null;
729     }
730
731     /**
732      * getColumnName
733      *
734      * returns a column name for column alias
735      * if the actual name for the alias cannot be found
736      * this method returns the given alias
737      *
738      * @param string $alias         column alias
739      * @return string               column name
740      */
741     public function getColumnName($alias)
742     {
743         $alias = strtolower($alias);
744         if (isset($this->_columnAliases[$alias])) {
745             return $this->_columnAliases[$alias];
746         }
747
748         return $alias;
749     }
750
751     /**
752      * setColumn
753      *
754      * @param string $name
755      * @param string $type
756      * @param integer $length
757      * @param mixed $options
758      * @throws Doctrine_Table_Exception     if trying use wrongly typed parameter
759      * @return void
760      */
761     public function setColumn($name, $type, $length = null, $options = array())
762     {
763         if (is_string($options)) {
764             $options = explode('|', $options);
765         }
766
767         foreach ($options as $k => $option) {
768             if (is_numeric($k)) {
769                 if ( ! empty($option)) {
770                     $options[$option] = true;
771                 }
772                 unset($options[$k]);
773             }
774         }
775
776         $name  = strtolower($name);
777         $parts = explode(' as ', $name);
778
779         if (count($parts) > 1) {
780             $this->_columnAliases[$parts[1]] = $parts[0];
781             $name = $parts[0];
782         }
783
784
785
786         if ($length == null) {
787             switch ($type) {
788                 case 'string':
789                 case 'clob':
790                 case 'float':
791                 case 'integer':
792                 case 'array':
793                 case 'object':
794                 case 'blob':
795                 case 'gzip':
796                     // use php int max
797                     $length = 2147483647;
798                 break;
799                 case 'boolean':
800                     $length = 1;
801                 case 'date':
802                     // YYYY-MM-DD ISO 8601
803                     $length = 10;
804                 case 'time':
805                     // HH:NN:SS+00:00 ISO 8601
806                     $length = 14;
807                 case 'timestamp':
808                     // YYYY-MM-DDTHH:MM:SS+00:00 ISO 8601
809                     $length = 25;
810                 break;
811             }
812         }
813
814         $this->_columns[$name] = $options;
815         $this->_columns[$name]['type'] = $type;
816         $this->_columns[$name]['length'] = $length;
817
818         if (isset($options['primary'])) {
819             $this->_identifier[] = $name;
820         }
821         if (isset($options['default'])) {
822             $this->hasDefaultValues = true;
823         }
824     }
825
826     /**
827      * hasDefaultValues
828      * returns true if this table has default values, otherwise false
829      *
830      * @return boolean
831      */
832     public function hasDefaultValues()
833     {
834         return $this->hasDefaultValues;
835     }
836
837     /**
838      * getDefaultValueOf
839      * returns the default value(if any) for given column
840      *
841      * @param string $column
842      * @return mixed
843      */
844     public function getDefaultValueOf($column)
845     {
846         $column = strtolower($column);
847         if ( ! isset($this->_columns[$column])) {
848             throw new Doctrine_Table_Exception("Couldn't get default value. Column ".$column." doesn't exist.");
849         }
850         if (isset($this->_columns[$column]['default'])) {
851             return $this->_columns[$column]['default'];
852         } else {
853             return null;
854         }
855     }
856
857     /**
858      * @return mixed
859      */
860     public function getIdentifier()
861     {
862         return $this->_identifier;
863     }
864
865     /**
866      * @return integer
867      */
868     public function getIdentifierType()
869     {
870         return $this->_identifierType;
871     }
872
873     /**
874      * hasColumn
875      * @return boolean
876      */
877     public function hasColumn($name)
878     {
879         return isset($this->_columns[$name]);
880     }
881
882     /**
883      * sets the connection for this class
884      *
885      * @params Doctrine_Connection      a connection object 
886      * @return Doctrine_Table           this object
887      */
888     public function setConnection(Doctrine_Connection $conn)
889     {
890         $this->_conn = $conn;
891
892         $this->setParent($this->_conn);
893         
894         return $this;
895     }
896
897     /**
898      * returns the connection associated with this table (if any)
899      *
900      * @return Doctrine_Connection|null     the connection object
901      */
902     public function getConnection()
903     {
904         return $this->_conn;
905     }
906
907     /**
908      * creates a new record
909      *
910      * @param $array             an array where keys are field names and
911      *                           values representing field values
912      * @return Doctrine_Record   the created record object
913      */
914     public function create(array $array = array()) 
915     {
916         $this->_data = $array;
917         $record      = new $this->_options['name']($this, true);
918         $this->_data = array();
919
920         return $record;
921     }
922
923     /**
924      * finds a record by its identifier
925      *
926      * @param $id                       database row id
927      * @param int $hydrationMode        Doctrine::HYDRATE_ARRAY or Doctrine::HYDRATE_RECORD
928      * @return mixed                    Array or Doctrine_Record or false if no result
929      */
930     public function find($id, $hydrationMode = null)
931     {
932         if (is_null($id)) {
933             return false;
934         }
935
936         $id = is_array($id) ? array_values($id) : array($id);
937
938         return $this->createQuery()
939             ->where(implode(' = ? AND ', (array) $this->_identifier) . ' = ?')
940             ->fetchOne($id, $hydrationMode);
941     }
942
943     /**
944      * findAll
945      * returns a collection of records
946      *
947      * @param int $hydrationMode        Doctrine::FETCH_ARRAY or Doctrine::FETCH_RECORD
948      * @return Doctrine_Collection
949      */
950     public function findAll($hydrationMode = null)
951     {
952         return $this->createQuery()->execute(array(), $hydrationMode);
953     }
954
955     /**
956      * findByDql
957      * finds records with given DQL where clause
958      * returns a collection of records
959      *
960      * @param string $dql               DQL after WHERE clause
961      * @param array $params             query parameters
962      * @param int $hydrationMode        Doctrine::FETCH_ARRAY or Doctrine::FETCH_RECORD
963      * @return Doctrine_Collection
964      */
965     public function findBySql($dql, array $params = array(), $hydrationMode = null)
966     {
967         return $this->createQuery()->where($dql)->execute($params, $hydrationMode);
968     }
969
970     public function findByDql($dql, array $params = array(), $hydrationMode = null)
971     {
972         return $this->findBySql($dql, $params, $hydrationMode);
973     }
974
975     /**
976      * execute
977      * fetches data using the provided queryKey and 
978      * the associated query in the query registry
979      *
980      * if no query for given queryKey is being found a 
981      * Doctrine_Query_Registry exception is being thrown
982      *
983      * @param string $queryKey      the query key
984      * @param array $params         prepared statement params (if any)
985      * @return mixed                the fetched data
986      */
987     public function execute($queryKey, $params = array(), $hydrationMode = Doctrine::HYDRATE_RECORD)
988     {
989         return Doctrine_Manager::getInstance()
990                             ->getQueryRegistry()
991                             ->get($queryKey, $this->getComponentName())
992                             ->execute($params, $hydrationMode);
993     }
994
995     /**
996      * executeOne
997      * fetches data using the provided queryKey and 
998      * the associated query in the query registry
999      *
1000      * if no query for given queryKey is being found a 
1001      * Doctrine_Query_Registry exception is being thrown
1002      *
1003      * @param string $queryKey      the query key
1004      * @param array $params         prepared statement params (if any)
1005      * @return mixed                the fetched data
1006      */
1007     public function executeOne($queryKey, $params = array(), $hydrationMode = Doctrine::HYDRATE_RECORD)
1008     {
1009         return Doctrine_Manager::getInstance()
1010                             ->getQueryRegistry()
1011                             ->get($queryKey, $this->getComponentName())
1012                             ->fetchOne($params, $hydrationMode);
1013     }
1014
1015     /**
1016      * clear
1017      * clears the first level cache (identityMap)
1018      *
1019      * @return void
1020      */
1021     public function clear()
1022     {
1023         $this->_identityMap = array();
1024     }
1025
1026     /**
1027      * addRecord
1028      * adds a record to identity map
1029      *
1030      * @param Doctrine_Record $record       record to be added
1031      * @return boolean
1032      */
1033     public function addRecord(Doctrine_Record $record)
1034     {
1035         $id = implode(' ', $record->identifier());
1036
1037         if (isset($this->_identityMap[$id])) {
1038             return false;
1039         }
1040
1041         $this->_identityMap[$id] = $record;
1042
1043         return true;
1044     }
1045
1046     /**
1047      * getRecord
1048      * first checks if record exists in identityMap, if not
1049      * returns a new record
1050      *
1051      * @return Doctrine_Record
1052      */
1053     public function getRecord()
1054     {
1055         if ( ! empty($this->_data)) {
1056             $this->_data = array_change_key_case($this->_data, CASE_LOWER);
1057
1058             $key = $this->getIdentifier();
1059
1060             if ( ! is_array($key)) {
1061                 $key = array($key);
1062             }
1063
1064             $found = false;
1065             foreach ($key as $k) {
1066                 if ( ! isset($this->_data[$k])) {
1067                     // primary key column not found return new record
1068                     $found = true;
1069                     break;
1070                 }
1071                 $id[] = $this->_data[$k];
1072             }
1073
1074             if ($found) {
1075                 $recordName = $this->getClassnameToReturn();
1076                 $record = new $recordName($this, true);
1077                 $this->_data = array();
1078
1079                 return $record;
1080             }
1081
1082
1083             $id = implode(' ', $id);
1084
1085             if (isset($this->_identityMap[$id])) {
1086                 $record = $this->_identityMap[$id];
1087                 $record->hydrate($this->_data);
1088             } else {
1089                 $recordName = $this->getClassnameToReturn();
1090                 $record = new $recordName($this);
1091                 $this->_identityMap[$id] = $record;
1092             }
1093             $this->_data = array();
1094         } else {
1095             $recordName = $this->getClassnameToReturn();
1096             $record = new $recordName($this, true);
1097         }
1098
1099
1100         return $record;
1101     }
1102
1103     /**
1104      * Get the classname to return. Most often this is just the options['name']
1105      *
1106      * Check the subclasses option and the inheritanceMap for each subclass to see
1107      * if all the maps in a subclass is met. If this is the case return that
1108      * subclass name. If no subclasses match or if there are no subclasses defined
1109      * return the name of the class for this tables record.
1110      *
1111      * @todo this function could use reflection to check the first time it runs
1112      * if the subclassing option is not set.
1113      *
1114      * @return string The name of the class to create
1115      *
1116      */
1117     public function getClassnameToReturn()
1118     {
1119         if ( ! isset($this->_options['subclasses'])) {
1120             return $this->_options['name'];
1121         }
1122         foreach ($this->_options['subclasses'] as $subclass) {
1123             $table = $this->_conn->getTable($subclass);
1124             $inheritanceMap = $table->getOption('inheritanceMap');
1125             $nomatch = false;
1126             foreach ($inheritanceMap as $key => $value) {
1127                 if ( ! isset($this->_data[$key]) || $this->_data[$key] != $value) {
1128                     $nomatch = true;
1129                     break;
1130                 }
1131             }
1132             if ( ! $nomatch) {
1133                 return $table->getComponentName();
1134             }
1135         }
1136         return $this->_options['name'];
1137     }
1138
1139     /**
1140      * @param $id                       database row id
1141      * @throws Doctrine_Find_Exception
1142      */
1143     final public function getProxy($id = null)
1144     {
1145         if ($id !== null) {
1146             $query = 'SELECT ' . implode(', ', (array) $this->_identifier)
1147                 . ' FROM ' . $this->getTableName()
1148                 . ' WHERE ' . implode(' = ? && ', (array) $this->_identifier) . ' = ?';
1149             $query = $this->applyInheritance($query);
1150
1151             $params = array_merge(array($id), array_values($this->_options['inheritanceMap']));
1152
1153             $this->_data = $this->_conn->execute($query, $params)->fetch(PDO::FETCH_ASSOC);
1154
1155             if ($this->_data === false)
1156                 return false;
1157         }
1158         return $this->getRecord();
1159     }
1160
1161     /**
1162      * applyInheritance
1163      * @param $where                    query where part to be modified
1164      * @return string                   query where part with column aggregation inheritance added
1165      */
1166     final public function applyInheritance($where)
1167     {
1168         if ( ! empty($this->_options['inheritanceMap'])) {
1169             $a = array();
1170             foreach ($this->_options['inheritanceMap'] as $field => $value) {
1171                 $a[] = $field . ' = ?';
1172             }
1173             $i = implode(' AND ', $a);
1174             $where .= ' AND ' . $i;
1175         }
1176         return $where;
1177     }
1178
1179     /**
1180      * count
1181      *
1182      * @return integer
1183      */
1184     public function count()
1185     {
1186         $a = $this->_conn->execute('SELECT COUNT(1) FROM ' . $this->_options['tableName'])->fetch(Doctrine::FETCH_NUM);
1187         return current($a);
1188     }
1189
1190     /**
1191      * @return Doctrine_Query                           a Doctrine_Query object
1192      */
1193     public function getQueryObject()
1194     {
1195         $graph = new Doctrine_Query($this->getConnection());
1196         $graph->load($this->getComponentName());
1197         return $graph;
1198     }
1199
1200     /**
1201      * @param string $field
1202      * @return array
1203      */
1204     public function getEnumValues($field)
1205     {
1206         if (isset($this->_columns[$field]['values'])) {
1207             return $this->_columns[$field]['values'];
1208         } else {
1209             return array();
1210         }
1211     }
1212
1213     /**
1214      * enumValue
1215      *
1216      * @param string $field
1217      * @param integer $index
1218      * @return mixed
1219      */
1220     public function enumValue($field, $index)
1221     {
1222         if ($index instanceof Doctrine_Null) {
1223             return $index;
1224         }
1225
1226         if ( ! $this->_conn->getAttribute(Doctrine::ATTR_USE_NATIVE_ENUM)
1227             && isset($this->_columns[$field]['values'][$index])
1228         ) {
1229             return $this->_columns[$field]['values'][$index];
1230         }
1231
1232         return $index;
1233     }
1234
1235     /**
1236      * enumIndex
1237      *
1238      * @param string $field
1239      * @param mixed $value
1240      * @return mixed
1241      */
1242     public function enumIndex($field, $value)
1243     {
1244         $values = $this->getEnumValues($field);
1245
1246         $index = array_search($value, $values);
1247         if ($index === false || !$this->_conn->getAttribute(Doctrine::ATTR_USE_NATIVE_ENUM)) {
1248             return $index;
1249         }
1250         return $value;
1251     }
1252     /* getColumnCount
1253      *
1254      * @return integer      the number of columns in this table
1255      */
1256     public function getColumnCount()
1257     {
1258         return $this->columnCount;
1259     }
1260
1261     /**
1262      * returns all columns and their definitions
1263      *
1264      * @return array
1265      */
1266     public function getColumns()
1267     {
1268         return $this->_columns;
1269     }
1270
1271     /**
1272      * removeColumn
1273      * removes given column
1274      *
1275      * @return boolean
1276      */
1277     public function removeColumn($column)
1278     {
1279         if (isset($this->_columns[$column])) {
1280             unset($this->_columns[$column]);
1281
1282             return true;
1283         }
1284         
1285         return false;
1286     }
1287
1288     /**
1289      * returns an array containing all the column names
1290      *
1291      * @return array
1292      */
1293     public function getColumnNames()
1294     {
1295         return array_keys($this->_columns);
1296     }
1297
1298     /**
1299      * getDefinitionOf
1300      *
1301      * @return mixed        array on success, false on failure
1302      */
1303     public function getDefinitionOf($column)
1304     {
1305         if (isset($this->_columns[$column])) {
1306             return $this->_columns[$column];
1307         }
1308         return false;
1309     }
1310
1311     /**
1312      * getTypeOf
1313      *
1314      * @return mixed        string on success, false on failure
1315      */
1316     public function getTypeOf($column)
1317     {
1318         if (isset($this->_columns[$column])) {
1319             return $this->_columns[$column]['type'];
1320         }
1321         return false;
1322     }
1323
1324     /**
1325      * setData
1326      * doctrine uses this function internally
1327      * users are strongly discouraged to use this function
1328      *
1329      * @param array $data               internal data
1330      * @return void
1331      */
1332     public function setData(array $data)
1333     {
1334         $this->_data = $data;
1335     }
1336
1337     /**
1338      * returns internal data, used by Doctrine_Record instances
1339      * when retrieving data from database
1340      *
1341      * @return array
1342      */
1343     public function getData()
1344     {
1345         return $this->_data;
1346     }
1347
1348     /**
1349      * prepareValue
1350      * this method performs special data preparation depending on
1351      * the type of the given column
1352      *
1353      * 1. It unserializes array and object typed columns
1354      * 2. Uncompresses gzip typed columns
1355      * 3. Gets the appropriate enum values for enum typed columns
1356      * 4. Initializes special null object pointer for null values (for fast column existence checking purposes)
1357      *
1358      * example:
1359      * <code type='php'>
1360      * $field = 'name';
1361      * $value = null;
1362      * $table->prepareValue($field, $value); // Doctrine_Null
1363      * </code>
1364      *
1365      * @throws Doctrine_Table_Exception     if unserialization of array/object typed column fails or
1366      * @throws Doctrine_Table_Exception     if uncompression of gzip typed column fails         *
1367      * @param string $field     the name of the field
1368      * @param string $value     field value
1369      * @return mixed            prepared value
1370      */
1371     public function prepareValue($field, $value)
1372     {
1373         if ($value === self::$_null) {
1374             return self::$_null;
1375         } else if ($value === null) {
1376             return null;
1377         } else {
1378             $type = $this->getTypeOf($field);
1379
1380             switch ($type) {
1381                 case 'array':
1382                 case 'object':
1383                     if (is_string($value)) {
1384                         $value = unserialize($value);
1385
1386                         if ($value === false) {
1387                             throw new Doctrine_Table_Exception('Unserialization of ' . $field . ' failed.');
1388                         }
1389                         return $value;
1390                     }
1391                 break;
1392                 case 'gzip':
1393                     $value = gzuncompress($value);
1394
1395                     if ($value === false) {
1396                         throw new Doctrine_Table_Exception('Uncompressing of ' . $field . ' failed.');
1397                     }
1398                     return $value;
1399                 break;
1400                 case 'enum':
1401                     return $this->enumValue($field, $value);
1402                 break;
1403                 case 'boolean':
1404                     return (boolean) $value;
1405                 break;
1406                 case 'integer':
1407                     // don't do any casting here PHP INT_MAX is smaller than what the databases support
1408                 break;
1409             }
1410         }
1411         return $value;
1412     }
1413
1414     /**
1415      * getTree
1416      *
1417      * getter for associated tree
1418      *
1419      * @return mixed  if tree return instance of Doctrine_Tree, otherwise returns false
1420      */
1421     public function getTree()
1422     {
1423         if (isset($this->_options['treeImpl'])) {
1424             if ( ! $this->_tree) {
1425                 $options = isset($this->_options['treeOptions']) ? $this->_options['treeOptions'] : array();
1426                 $this->_tree = Doctrine_Tree::factory($this,
1427                     $this->_options['treeImpl'],
1428                     $options
1429                 );
1430             }
1431             return $this->_tree;
1432         }
1433         return false;
1434     }
1435
1436     /**
1437      * getComponentName
1438      *
1439      * @return void
1440      */
1441     public function getComponentName()
1442     {
1443         return $this->_options['name'];
1444     }
1445
1446     /**
1447      * getTableName
1448      *
1449      * @return void
1450      */
1451     public function getTableName()
1452     {
1453         return $this->_options['tableName'];
1454     }
1455
1456     /**
1457      * setTableName
1458      *
1459      * @param string $tableName 
1460      * @return void
1461      */
1462     public function setTableName($tableName)
1463     {
1464         $this->_options['tableName'] = $tableName;
1465     }
1466
1467     /**
1468      * isTree
1469      *
1470      * determine if table acts as tree
1471      *
1472      * @return mixed  if tree return true, otherwise returns false
1473      */
1474     public function isTree()
1475     {
1476         return ( ! is_null($this->_options['treeImpl'])) ? true : false;
1477     }
1478
1479     /**
1480      * getTemplate
1481      *
1482      * @param string $template 
1483      * @return void
1484      */
1485     public function getTemplate($template)
1486     {
1487         if ( ! isset($this->_templates[$template])) {
1488             throw new Doctrine_Table_Exception('Template ' . $template . ' not loaded');
1489         }
1490
1491         return $this->_templates[$template];
1492     }
1493     
1494     public function hasTemplate($template)
1495     {
1496         return isset($this->_templates[$template]);
1497     }
1498
1499     public function addTemplate($template, Doctrine_Template $impl)
1500     {
1501         $this->_templates[$template] = $impl;
1502
1503         return $this;
1504     }
1505
1506     /**
1507      * bindQueryParts
1508      * binds query parts to given component
1509      *
1510      * @param array $queryParts         an array of pre-bound query parts
1511      * @return Doctrine_Record          this object
1512      */
1513     public function bindQueryParts(array $queryParts)
1514     {
1515      $this->_options['queryParts'] = $queryParts;
1516
1517         return $this;
1518     }
1519
1520     /**
1521      * bindQueryPart
1522      * binds given value to given query part
1523      *
1524      * @param string $queryPart
1525      * @param mixed $value
1526      * @return Doctrine_Record          this object
1527      */
1528     public function bindQueryPart($queryPart, $value)
1529     {
1530      $this->_options['queryParts'][$queryPart] = $value;
1531
1532         return $this;
1533     }
1534     
1535     /**
1536      * getBoundQueryPart
1537      *
1538      * @param string $queryPart 
1539      * @return string $queryPart
1540      */
1541     public function getBoundQueryPart($queryPart)
1542     {
1543         if ( ! isset($this->_options['queryParts'][$queryPart])) {
1544             return null;
1545         }
1546
1547         return $this->_options['queryParts'][$queryPart];
1548     }
1549     
1550     /**
1551      * unshiftFilter
1552      *
1553      * @param  object Doctrine_Record_Filter $filter
1554      * @return object $this
1555      */
1556     public function unshiftFilter(Doctrine_Record_Filter $filter)
1557     {
1558         $filter->setTable($this);
1559
1560         $filter->init();
1561
1562         array_unshift($this->_filters, $filter);
1563
1564         return $this;
1565     }
1566     
1567     /**
1568      * getFilters
1569      *
1570      * @return array $filters
1571      */
1572     public function getFilters()
1573     {
1574         return $this->_filters;
1575     }
1576
1577     /**
1578      * returns a string representation of this object
1579      *
1580      * @return string
1581      */
1582     public function __toString()
1583     {
1584         return Doctrine_Lib::getTableAsString($this);
1585     }
1586     
1587     /**
1588      * findBy
1589      *
1590      * @param string $column 
1591      * @param string $value 
1592      * @param string $hydrationMode 
1593      * @return void
1594      */
1595     protected function findBy($column, $value, $hydrationMode = null)
1596     {
1597         return $this->createQuery()->where($column . ' = ?')->execute(array($value), $hydrationMode);
1598     }
1599     
1600     /**
1601      * findOneBy
1602      *
1603      * @param string $column 
1604      * @param string $value 
1605      * @param string $hydrationMode 
1606      * @return void
1607      */
1608     protected function findOneBy($column, $value, $hydrationMode = null)
1609     {
1610         $results = $this->createQuery()->where($column . ' = ?')->limit(1)->execute(array($value), $hydrationMode);
1611         
1612         return $hydrationMode === Doctrine::FETCH_ARRAY ? $results[0]:$results->getFirst();
1613     }
1614     
1615     /**
1616      * __call
1617      *
1618      * Adds support for magic finders.
1619      * findByColumnName, findByRelationAlias
1620      * findById, findByContactId, etc.
1621      *
1622      * @return void
1623      */
1624     public function __call($method, $arguments)
1625     {
1626         if (substr($method, 0, 6) == 'findBy') {
1627             $by = substr($method, 6, strlen($method));
1628             $method = 'findBy';
1629         } else if (substr($method, 0, 9) == 'findOneBy') {
1630             $by = substr($method, 9, strlen($method));
1631             $method = 'findOneBy';
1632         }
1633         
1634         if (isset($by)) {
1635             if (!isset($arguments[0])) {
1636                 throw new Doctrine_Table_Exception('You must specify the value to findBy');
1637             }
1638             
1639             $column = Doctrine::tableize($by);
1640             $hydrationMode = isset($arguments[1]) ? $arguments[1]:null;
1641             
1642             if ($this->hasColumn($column)) {
1643                 return $this->$method($column, $arguments[0], $hydrationMode);
1644             } else if ($this->hasRelation($by)) {
1645                 $relation = $this->getRelation($by);
1646                 
1647                 if ($relation['type'] === Doctrine_Relation::MANY) {
1648                     throw new Doctrine_Table_Exception('Cannot findBy many relationship.');
1649                 }
1650                 
1651                 return $this->$method($relation['local'], $arguments[0], $hydrationMode);
1652             } else {
1653                 throw new Doctrine_Table_Exception('Cannot find by: ' . $by . '. Invalid column or relationship alias.');
1654             }
1655         }
1656     }
1657 }