Coverage for Doctrine_Table

Back to coverage report

1 <?php
2 /*
3  *  $Id: Table.php 2991 2007-10-22 17:33:47Z zYne $
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: 2991 $
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 isIdentifier($identifier)
338     {
339         return ($identifier === $this->_identifier || 
340                 in_array($identifier, (array) $this->_identifier));
341     }
342
343     public function getMethodOwner($method)
344     {
345         return (isset($this->_invokedMethods[$method])) ?
346                       $this->_invokedMethods[$method] : false;
347     }
348     
349     public function setMethodOwner($method, $class)
350     {
351         $this->_invokedMethods[$method] = $class;
352     }
353
354     /**
355      * getTemplates
356      * returns all templates attached to this table
357      *
358      * @return array     an array containing all templates
359      */
360     public function getTemplates()
361     {
362         return $this->_templates;
363     }
364
365     /**
366      * export
367      * exports this table to database based on column and option definitions
368      *
369      * @throws Doctrine_Connection_Exception    if some error other than Doctrine::ERR_ALREADY_EXISTS
370      *                                          occurred during the create table operation
371      * @return boolean                          whether or not the export operation was successful
372      *                                          false if table already existed in the database
373      */
374     public function export()
375     {
376         $this->_conn->export->exportTable($this);
377     }
378
379     /**
380      * getExportableFormat
381      * returns exportable presentation of this object
382      *
383      * @return array
384      */
385     public function getExportableFormat($parseForeignKeys = true)
386     {
387         $columns = array();
388         $primary = array();
389
390         foreach ($this->getColumns() as $name => $column) {
391             $definition = $column;
392
393             switch ($definition['type']) {
394                 case 'enum':
395                     if (isset($definition['default'])) {
396                         $definition['default'] = $this->enumIndex($name, $definition['default']);
397                     }
398                     break;
399                 case 'boolean':
400                     if (isset($definition['default'])) {
401                         $definition['default'] = $this->getConnection()->convertBooleans($definition['default']);
402                     }
403                     break;
404             }
405             $columns[$name] = $definition;
406
407             if (isset($definition['primary']) && $definition['primary']) {
408                 $primary[] = $name;
409             }
410         }
411         $options['foreignKeys'] = array();
412
413         if ($parseForeignKeys) {
414             if ($this->getAttribute(Doctrine::ATTR_EXPORT) & Doctrine::EXPORT_CONSTRAINTS) {
415
416                 $constraints = array();
417
418                 $emptyIntegrity = array('onUpdate' => null,
419                                         'onDelete' => null);
420
421                 foreach ($this->getRelations() as $name => $relation) {
422                     $fk = $relation->toArray();
423                     $fk['foreignTable'] = $relation->getTable()->getTableName();
424
425                     if ($relation->getTable() === $this && in_array($relation->getLocal(), $primary)) {
426                         if ($relation->hasConstraint()) {
427                             throw new Doctrine_Table_Exception("Badly constructed integrity constraints.");
428                         }
429
430                         continue;
431                     }
432
433                     $integrity = array('onUpdate' => $fk['onUpdate'],
434                                        'onDelete' => $fk['onDelete']);
435
436                     if ($relation instanceof Doctrine_Relation_LocalKey) {
437                         $def = array('local'        => $relation->getLocal(),
438                                      'foreign'      => $relation->getForeign(),
439                                      'foreignTable' => $relation->getTable()->getTableName());
440
441                         if (($key = array_search($def, $options['foreignKeys'])) === false) {
442                             $options['foreignKeys'][] = $def;
443
444                             $constraints[] = $integrity;
445                         } else {
446                             if ($integrity !== $emptyIntegrity) {
447                                 $constraints[$key] = $integrity;
448                             }
449                         }
450                     }
451                 }
452
453                 foreach ($constraints as $k => $def) {
454                     $options['foreignKeys'][$k] = array_merge($options['foreignKeys'][$k], $def);
455                 }
456
457             }
458         }
459         $options['primary'] = $primary;
460
461         return array('tableName' => $this->getOption('tableName'),
462                      'columns'   => $columns,
463                      'options'   => array_merge($this->getOptions(), $options));
464     }
465
466     /**
467      * exportConstraints
468      * exports the constraints of this table into database based on option definitions
469      *
470      * @throws Doctrine_Connection_Exception    if something went wrong on db level
471      * @return void
472      */
473     public function exportConstraints()
474     {
475         try {
476             $this->_conn->beginTransaction();
477
478             foreach ($this->_options['index'] as $index => $definition) {
479                 $this->_conn->export->createIndex($this->_options['tableName'], $index, $definition);
480             }
481             $this->_conn->commit();
482         } catch(Doctrine_Connection_Exception $e) {
483             $this->_conn->rollback();
484
485             throw $e;
486         }
487     }
488
489     /**
490      * getRelationParser
491      * return the relation parser associated with this table
492      *
493      * @return Doctrine_Relation_Parser     relation parser object
494      */
495     public function getRelationParser()
496     {
497         return $this->_parser;
498     }
499
500     /**
501      * __get
502      * an alias for getOption
503      *
504      * @param string $option
505      */
506     public function __get($option)
507     {
508         if (isset($this->_options[$option])) {
509             return $this->_options[$option];
510         }
511         return null;
512     }
513
514     /**
515      * __isset
516      *
517      * @param string $option
518      */
519     public function __isset($option)
520     {
521         return isset($this->_options[$option]);
522     }
523
524     /**
525      * getOptions
526      * returns all options of this table and the associated values
527      *
528      * @return array    all options and their values
529      */
530     public function getOptions()
531     {
532         return $this->_options;
533     }
534
535     /**
536      * addForeignKey
537      *
538      * adds a foreignKey to this table
539      *
540      * @return void
541      */
542     public function addForeignKey(array $definition)
543     {
544         $this->_options['foreignKeys'][] = $definition;
545     }
546
547     /**
548      * addCheckConstraint
549      *
550      * adds a check constraint to this table
551      *
552      * @return void
553      */
554     public function addCheckConstraint($definition, $name)
555     {
556         if (is_string($name)) {
557             $this->_options['checks'][$name] = $definition;
558         } else {
559             $this->_options['checks'][] = $definition;
560         }
561
562         return $this;
563     }
564
565     /**
566      * addIndex
567      *
568      * adds an index to this table
569      *
570      * @return void
571      */
572     public function addIndex($index, array $definition)
573     {
574         $this->_options['indexes'][$index] = $definition;
575     }
576
577     /**
578      * getIndex
579      *
580      * @return array|boolean        array on success, FALSE on failure
581      */
582     public function getIndex($index)
583     {
584         if (isset($this->_options['indexes'][$index])) {
585             return $this->_options['indexes'][$index];
586         }
587
588         return false;
589     }
590     public function bind($args, $type)
591     {
592         $options = array();
593         $options['type'] = $type;
594
595         if ( ! isset($args[1])) {
596             $args[1] = array();
597         }
598
599         // the following is needed for backwards compatibility
600         if (is_string($args[1])) {
601             if ( ! isset($args[2])) {
602                 $args[2] = array();
603             } elseif (is_string($args[2])) {
604                 $args[2] = (array) $args[2];
605             }
606
607             $classes = array_merge($this->_options['parents'], array($this->getComponentName()));
608
609
610             $e = explode('.', $args[1]);
611             if (in_array($e[0], $classes)) {
612                 if ($options['type'] >= Doctrine_Relation::MANY) {
613                     $options['foreign'] = $e[1];
614                 } else {
615                     $options['local'] = $e[1];
616                 }
617             } else {
618                 $e2 = explode(' as ', $args[0]);
619                 if ($e[0] !== $e2[0] && ( ! isset($e2[1]) || $e[0] !== $e2[1])) {
620                     $options['refClass'] = $e[0];
621                 }
622
623                 $options['foreign'] = $e[1];
624             }
625
626             $options = array_merge($args[2], $options);
627
628             $this->_parser->bind($args[0], $options);
629         } else {
630             $options = array_merge($args[1], $options);
631             $this->_parser->bind($args[0], $options);
632         }
633     }
634
635     /**
636      * hasRelation
637      *
638      * @param string $alias      the relation to check if exists
639      * @return boolean           true if the relation exists otherwise false
640      */
641     public function hasRelation($alias)
642     {
643         return $this->_parser->hasRelation($alias);
644     }
645
646     /**
647      * getRelation
648      *
649      * @param string $alias      relation alias
650      */
651     public function getRelation($alias, $recursive = true)
652     {
653         return $this->_parser->getRelation($alias, $recursive);
654     }
655
656     /**
657      * getRelations
658      * returns an array containing all relation objects
659      *
660      * @return array        an array of Doctrine_Relation objects
661      */
662     public function getRelations()
663     {
664         return $this->_parser->getRelations();
665     }
666
667     /**
668      * createQuery
669      * creates a new Doctrine_Query object and adds the component name
670      * of this table as the query 'from' part
671      *
672      * @param string Optional alias name for component aliasing.
673      *
674      * @return Doctrine_Query
675      */
676     public function createQuery($alias = '')
677     {
678         if ( ! empty($alias)) {
679             $alias = ' ' . trim($alias);
680         }
681         return Doctrine_Query::create($this->_conn)->from($this->getComponentName() . $alias);
682     }
683
684     /**
685      * getRepository
686      *
687      * @return Doctrine_Table_Repository
688      */
689     public function getRepository()
690     {
691         return $this->_repository;
692     }
693
694     /**
695      * setOption
696      * sets an option and returns this object in order to
697      * allow flexible method chaining
698      *
699      * @see Doctrine_Table::$_options   for available options
700      * @param string $name              the name of the option to set
701      * @param mixed $value              the value of the option
702      * @return Doctrine_Table           this object
703      */
704     public function setOption($name, $value)
705     {
706         switch ($name) {
707         case 'name':
708         case 'tableName':
709             break;
710         case 'enumMap':
711         case 'inheritanceMap':
712         case 'index':
713         case 'treeOptions':
714             if ( ! is_array($value)) {
715                 throw new Doctrine_Table_Exception($name . ' should be an array.');
716             }
717             break;
718         }
719         $this->_options[$name] = $value;
720     }
721
722     /**
723      * getOption
724      * returns the value of given option
725      *
726      * @param string $name  the name of the option
727      * @return mixed        the value of given option
728      */
729     public function getOption($name)
730     {
731         if (isset($this->_options[$name])) {
732             return $this->_options[$name];
733         }
734         return null;
735     }
736
737     /**
738      * getColumnName
739      *
740      * returns a column name for column alias
741      * if the actual name for the alias cannot be found
742      * this method returns the given alias
743      *
744      * @param string $alias         column alias
745      * @return string               column name
746      */
747     public function getColumnName($alias)
748     {
749         $alias = strtolower($alias);
750         if (isset($this->_columnAliases[$alias])) {
751             return $this->_columnAliases[$alias];
752         }
753
754         return $alias;
755     }
756
757     /**
758      * setColumn
759      *
760      * @param string $name
761      * @param string $type
762      * @param integer $length
763      * @param mixed $options
764      * @throws Doctrine_Table_Exception     if trying use wrongly typed parameter
765      * @return void
766      */
767     public function setColumn($name, $type, $length = null, $options = array())
768     {
769         if (is_string($options)) {
770             $options = explode('|', $options);
771         }
772
773         foreach ($options as $k => $option) {
774             if (is_numeric($k)) {
775                 if ( ! empty($option)) {
776                     $options[$option] = true;
777                 }
778                 unset($options[$k]);
779             }
780         }
781
782         $name  = strtolower($name);
783         $parts = explode(' as ', $name);
784
785         if (count($parts) > 1) {
786             $this->_columnAliases[$parts[1]] = $parts[0];
787             $name = $parts[0];
788         }
789
790
791
792         if ($length == null) {
793             switch ($type) {
794                 case 'string':
795                 case 'clob':
796                 case 'float':
797                 case 'integer':
798                 case 'array':
799                 case 'object':
800                 case 'blob':
801                 case 'gzip':
802                     // use php int max
803                     $length = 2147483647;
804                 break;
805                 case 'boolean':
806                     $length = 1;
807                 case 'date':
808                     // YYYY-MM-DD ISO 8601
809                     $length = 10;
810                 case 'time':
811                     // HH:NN:SS+00:00 ISO 8601
812                     $length = 14;
813                 case 'timestamp':
814                     // YYYY-MM-DDTHH:MM:SS+00:00 ISO 8601
815                     $length = 25;
816                 break;
817             }
818         }
819
820         $this->_columns[$name] = $options;
821         $this->_columns[$name]['type'] = $type;
822         $this->_columns[$name]['length'] = $length;
823
824         if (isset($options['primary'])) {
825             $this->_identifier[] = $name;
826         }
827         if (isset($options['default'])) {
828             $this->hasDefaultValues = true;
829         }
830     }
831
832     /**
833      * hasDefaultValues
834      * returns true if this table has default values, otherwise false
835      *
836      * @return boolean
837      */
838     public function hasDefaultValues()
839     {
840         return $this->hasDefaultValues;
841     }
842
843     /**
844      * getDefaultValueOf
845      * returns the default value(if any) for given column
846      *
847      * @param string $column
848      * @return mixed
849      */
850     public function getDefaultValueOf($column)
851     {
852         $column = strtolower($column);
853         if ( ! isset($this->_columns[$column])) {
854             throw new Doctrine_Table_Exception("Couldn't get default value. Column ".$column." doesn't exist.");
855         }
856         if (isset($this->_columns[$column]['default'])) {
857             return $this->_columns[$column]['default'];
858         } else {
859             return null;
860         }
861     }
862
863     /**
864      * @return mixed
865      */
866     public function getIdentifier()
867     {
868         return $this->_identifier;
869     }
870
871     /**
872      * @return integer
873      */
874     public function getIdentifierType()
875     {
876         return $this->_identifierType;
877     }
878
879     /**
880      * hasColumn
881      * @return boolean
882      */
883     public function hasColumn($name)
884     {
885         return isset($this->_columns[$name]);
886     }
887
888     /**
889      * sets the connection for this class
890      *
891      * @params Doctrine_Connection      a connection object 
892      * @return Doctrine_Table           this object
893      */
894     public function setConnection(Doctrine_Connection $conn)
895     {
896         $this->_conn = $conn;
897
898         $this->setParent($this->_conn);
899         
900         return $this;
901     }
902
903     /**
904      * returns the connection associated with this table (if any)
905      *
906      * @return Doctrine_Connection|null     the connection object
907      */
908     public function getConnection()
909     {
910         return $this->_conn;
911     }
912
913     /**
914      * creates a new record
915      *
916      * @param $array             an array where keys are field names and
917      *                           values representing field values
918      * @return Doctrine_Record   the created record object
919      */
920     public function create(array $array = array()) 
921     {
922         $this->_data = $array;
923         $record      = new $this->_options['name']($this, true);
924         $this->_data = array();
925
926         return $record;
927     }
928
929     /**
930      * finds a record by its identifier
931      *
932      * @param $id                       database row id
933      * @param int $hydrationMode        Doctrine::HYDRATE_ARRAY or Doctrine::HYDRATE_RECORD
934      * @return mixed                    Array or Doctrine_Record or false if no result
935      */
936     public function find($id, $hydrationMode = null)
937     {
938         if (is_null($id)) {
939             return false;
940         }
941
942         $id = is_array($id) ? array_values($id) : array($id);
943
944         return $this->createQuery()
945             ->where(implode(' = ? AND ', (array) $this->_identifier) . ' = ?')
946             ->fetchOne($id, $hydrationMode);
947     }
948
949     /**
950      * findAll
951      * returns a collection of records
952      *
953      * @param int $hydrationMode        Doctrine::FETCH_ARRAY or Doctrine::FETCH_RECORD
954      * @return Doctrine_Collection
955      */
956     public function findAll($hydrationMode = null)
957     {
958         return $this->createQuery()->execute(array(), $hydrationMode);
959     }
960
961     /**
962      * findByDql
963      * finds records with given DQL where clause
964      * returns a collection of records
965      *
966      * @param string $dql               DQL after WHERE clause
967      * @param array $params             query parameters
968      * @param int $hydrationMode        Doctrine::FETCH_ARRAY or Doctrine::FETCH_RECORD
969      * @return Doctrine_Collection
970      */
971     public function findBySql($dql, array $params = array(), $hydrationMode = null)
972     {
973         return $this->createQuery()->where($dql)->execute($params, $hydrationMode);
974     }
975
976     public function findByDql($dql, array $params = array(), $hydrationMode = null)
977     {
978         return $this->findBySql($dql, $params, $hydrationMode);
979     }
980
981     /**
982      * execute
983      * fetches data using the provided queryKey and 
984      * the associated query in the query registry
985      *
986      * if no query for given queryKey is being found a 
987      * Doctrine_Query_Registry exception is being thrown
988      *
989      * @param string $queryKey      the query key
990      * @param array $params         prepared statement params (if any)
991      * @return mixed                the fetched data
992      */
993     public function execute($queryKey, $params = array(), $hydrationMode = Doctrine::HYDRATE_RECORD)
994     {
995         return Doctrine_Manager::getInstance()
996                             ->getQueryRegistry()
997                             ->get($queryKey, $this->getComponentName())
998                             ->execute($params, $hydrationMode);
999     }
1000
1001     /**
1002      * executeOne
1003      * fetches data using the provided queryKey and 
1004      * the associated query in the query registry
1005      *
1006      * if no query for given queryKey is being found a 
1007      * Doctrine_Query_Registry exception is being thrown
1008      *
1009      * @param string $queryKey      the query key
1010      * @param array $params         prepared statement params (if any)
1011      * @return mixed                the fetched data
1012      */
1013     public function executeOne($queryKey, $params = array(), $hydrationMode = Doctrine::HYDRATE_RECORD)
1014     {
1015         return Doctrine_Manager::getInstance()
1016                             ->getQueryRegistry()
1017                             ->get($queryKey, $this->getComponentName())
1018                             ->fetchOne($params, $hydrationMode);
1019     }
1020
1021     /**
1022      * clear
1023      * clears the first level cache (identityMap)
1024      *
1025      * @return void
1026      */
1027     public function clear()
1028     {
1029         $this->_identityMap = array();
1030     }
1031
1032     /**
1033      * addRecord
1034      * adds a record to identity map
1035      *
1036      * @param Doctrine_Record $record       record to be added
1037      * @return boolean
1038      */
1039     public function addRecord(Doctrine_Record $record)
1040     {
1041         $id = implode(' ', $record->identifier());
1042
1043         if (isset($this->_identityMap[$id])) {
1044             return false;
1045         }
1046
1047         $this->_identityMap[$id] = $record;
1048
1049         return true;
1050     }
1051
1052     /**
1053      * getRecord
1054      * first checks if record exists in identityMap, if not
1055      * returns a new record
1056      *
1057      * @return Doctrine_Record
1058      */
1059     public function getRecord()
1060     {
1061         if ( ! empty($this->_data)) {
1062             $this->_data = array_change_key_case($this->_data, CASE_LOWER);
1063
1064             $key = $this->getIdentifier();
1065
1066             if ( ! is_array($key)) {
1067                 $key = array($key);
1068             }
1069
1070             $found = false;
1071             foreach ($key as $k) {
1072                 if ( ! isset($this->_data[$k])) {
1073                     // primary key column not found return new record
1074                     $found = true;
1075                     break;
1076                 }
1077                 $id[] = $this->_data[$k];
1078             }
1079
1080             if ($found) {
1081                 $recordName = $this->getClassnameToReturn();
1082                 $record = new $recordName($this, true);
1083                 $this->_data = array();
1084
1085                 return $record;
1086             }
1087
1088
1089             $id = implode(' ', $id);
1090
1091             if (isset($this->_identityMap[$id])) {
1092                 $record = $this->_identityMap[$id];
1093                 $record->hydrate($this->_data);
1094             } else {
1095                 $recordName = $this->getClassnameToReturn();
1096                 $record = new $recordName($this);
1097                 $this->_identityMap[$id] = $record;
1098             }
1099             $this->_data = array();
1100         } else {
1101             $recordName = $this->getClassnameToReturn();
1102             $record = new $recordName($this, true);
1103         }
1104
1105
1106         return $record;
1107     }
1108
1109     /**
1110      * Get the classname to return. Most often this is just the options['name']
1111      *
1112      * Check the subclasses option and the inheritanceMap for each subclass to see
1113      * if all the maps in a subclass is met. If this is the case return that
1114      * subclass name. If no subclasses match or if there are no subclasses defined
1115      * return the name of the class for this tables record.
1116      *
1117      * @todo this function could use reflection to check the first time it runs
1118      * if the subclassing option is not set.
1119      *
1120      * @return string The name of the class to create
1121      *
1122      */
1123     public function getClassnameToReturn()
1124     {
1125         if ( ! isset($this->_options['subclasses'])) {
1126             return $this->_options['name'];
1127         }
1128         foreach ($this->_options['subclasses'] as $subclass) {
1129             $table = $this->_conn->getTable($subclass);
1130             $inheritanceMap = $table->getOption('inheritanceMap');
1131             $nomatch = false;
1132             foreach ($inheritanceMap as $key => $value) {
1133                 if ( ! isset($this->_data[$key]) || $this->_data[$key] != $value) {
1134                     $nomatch = true;
1135                     break;
1136                 }
1137             }
1138             if ( ! $nomatch) {
1139                 return $table->getComponentName();
1140             }
1141         }
1142         return $this->_options['name'];
1143     }
1144
1145     /**
1146      * @param $id                       database row id
1147      * @throws Doctrine_Find_Exception
1148      */
1149     final public function getProxy($id = null)
1150     {
1151         if ($id !== null) {
1152             $query = 'SELECT ' . implode(', ', (array) $this->_identifier)
1153                 . ' FROM ' . $this->getTableName()
1154                 . ' WHERE ' . implode(' = ? && ', (array) $this->_identifier) . ' = ?';
1155             $query = $this->applyInheritance($query);
1156
1157             $params = array_merge(array($id), array_values($this->_options['inheritanceMap']));
1158
1159             $this->_data = $this->_conn->execute($query, $params)->fetch(PDO::FETCH_ASSOC);
1160
1161             if ($this->_data === false)
1162                 return false;
1163         }
1164         return $this->getRecord();
1165     }
1166
1167     /**
1168      * applyInheritance
1169      * @param $where                    query where part to be modified
1170      * @return string                   query where part with column aggregation inheritance added
1171      */
1172     final public function applyInheritance($where)
1173     {
1174         if ( ! empty($this->_options['inheritanceMap'])) {
1175             $a = array();
1176             foreach ($this->_options['inheritanceMap'] as $field => $value) {
1177                 $a[] = $field . ' = ?';
1178             }
1179             $i = implode(' AND ', $a);
1180             $where .= ' AND ' . $i;
1181         }
1182         return $where;
1183     }
1184
1185     /**
1186      * count
1187      *
1188      * @return integer
1189      */
1190     public function count()
1191     {
1192         $a = $this->_conn->execute('SELECT COUNT(1) FROM ' . $this->_options['tableName'])->fetch(Doctrine::FETCH_NUM);
1193         return current($a);
1194     }
1195
1196     /**
1197      * @return Doctrine_Query                           a Doctrine_Query object
1198      */
1199     public function getQueryObject()
1200     {
1201         $graph = new Doctrine_Query($this->getConnection());
1202         $graph->load($this->getComponentName());
1203         return $graph;
1204     }
1205
1206     /**
1207      * @param string $field
1208      * @return array
1209      */
1210     public function getEnumValues($field)
1211     {
1212         if (isset($this->_columns[$field]['values'])) {
1213             return $this->_columns[$field]['values'];
1214         } else {
1215             return array();
1216         }
1217     }
1218
1219     /**
1220      * enumValue
1221      *
1222      * @param string $field
1223      * @param integer $index
1224      * @return mixed
1225      */
1226     public function enumValue($field, $index)
1227     {
1228         if ($index instanceof Doctrine_Null) {
1229             return $index;
1230         }
1231
1232         if ( ! $this->_conn->getAttribute(Doctrine::ATTR_USE_NATIVE_ENUM)
1233             && isset($this->_columns[$field]['values'][$index])
1234         ) {
1235             return $this->_columns[$field]['values'][$index];
1236         }
1237
1238         return $index;
1239     }
1240
1241     /**
1242      * enumIndex
1243      *
1244      * @param string $field
1245      * @param mixed $value
1246      * @return mixed
1247      */
1248     public function enumIndex($field, $value)
1249     {
1250         $values = $this->getEnumValues($field);
1251
1252         $index = array_search($value, $values);
1253         if ($index === false || !$this->_conn->getAttribute(Doctrine::ATTR_USE_NATIVE_ENUM)) {
1254             return $index;
1255         }
1256         return $value;
1257     }
1258     /* getColumnCount
1259      *
1260      * @return integer      the number of columns in this table
1261      */
1262     public function getColumnCount()
1263     {
1264         return $this->columnCount;
1265     }
1266
1267     /**
1268      * returns all columns and their definitions
1269      *
1270      * @return array
1271      */
1272     public function getColumns()
1273     {
1274         return $this->_columns;
1275     }
1276
1277     /**
1278      * removeColumn
1279      * removes given column
1280      *
1281      * @return boolean
1282      */
1283     public function removeColumn($column)
1284     {
1285         if (isset($this->_columns[$column])) {
1286             unset($this->_columns[$column]);
1287
1288             return true;
1289         }
1290         
1291         return false;
1292     }
1293
1294     /**
1295      * returns an array containing all the column names
1296      *
1297      * @return array
1298      */
1299     public function getColumnNames()
1300     {
1301         return array_keys($this->_columns);
1302     }
1303
1304     /**
1305      * getDefinitionOf
1306      *
1307      * @return mixed        array on success, false on failure
1308      */
1309     public function getDefinitionOf($column)
1310     {
1311         if (isset($this->_columns[$column])) {
1312             return $this->_columns[$column];
1313         }
1314         return false;
1315     }
1316
1317     /**
1318      * getTypeOf
1319      *
1320      * @return mixed        string on success, false on failure
1321      */
1322     public function getTypeOf($column)
1323     {
1324         if (isset($this->_columns[$column])) {
1325             return $this->_columns[$column]['type'];
1326         }
1327         return false;
1328     }
1329
1330     /**
1331      * setData
1332      * doctrine uses this function internally
1333      * users are strongly discouraged to use this function
1334      *
1335      * @param array $data               internal data
1336      * @return void
1337      */
1338     public function setData(array $data)
1339     {
1340         $this->_data = $data;
1341     }
1342
1343     /**
1344      * returns internal data, used by Doctrine_Record instances
1345      * when retrieving data from database
1346      *
1347      * @return array
1348      */
1349     public function getData()
1350     {
1351         return $this->_data;
1352     }
1353
1354     /**
1355      * prepareValue
1356      * this method performs special data preparation depending on
1357      * the type of the given column
1358      *
1359      * 1. It unserializes array and object typed columns
1360      * 2. Uncompresses gzip typed columns
1361      * 3. Gets the appropriate enum values for enum typed columns
1362      * 4. Initializes special null object pointer for null values (for fast column existence checking purposes)
1363      *
1364      * example:
1365      * <code type='php'>
1366      * $field = 'name';
1367      * $value = null;
1368      * $table->prepareValue($field, $value); // Doctrine_Null
1369      * </code>
1370      *
1371      * @throws Doctrine_Table_Exception     if unserialization of array/object typed column fails or
1372      * @throws Doctrine_Table_Exception     if uncompression of gzip typed column fails         *
1373      * @param string $field     the name of the field
1374      * @param string $value     field value
1375      * @return mixed            prepared value
1376      */
1377     public function prepareValue($field, $value)
1378     {
1379         if ($value === self::$_null) {
1380             return self::$_null;
1381         } elseif ($value === null) {
1382             return null;
1383         } else {
1384             $type = $this->getTypeOf($field);
1385
1386             switch ($type) {
1387                 case 'array':
1388                 case 'object':
1389                     if (is_string($value)) {
1390                         $value = unserialize($value);
1391
1392                         if ($value === false) {
1393                             throw new Doctrine_Table_Exception('Unserialization of ' . $field . ' failed.');
1394                         }
1395                         return $value;
1396                     }
1397                 break;
1398                 case 'gzip':
1399                     $value = gzuncompress($value);
1400
1401                     if ($value === false) {
1402                         throw new Doctrine_Table_Exception('Uncompressing of ' . $field . ' failed.');
1403                     }
1404                     return $value;
1405                 break;
1406                 case 'enum':
1407                     return $this->enumValue($field, $value);
1408                 break;
1409                 case 'boolean':
1410                     return (boolean) $value;
1411                 break;
1412                 case 'integer':
1413                     // don't do any casting here PHP INT_MAX is smaller than what the databases support
1414                 break;
1415             }
1416         }
1417         return $value;
1418     }
1419
1420     /**
1421      * getTree
1422      *
1423      * getter for associated tree
1424      *
1425      * @return mixed  if tree return instance of Doctrine_Tree, otherwise returns false
1426      */
1427     public function getTree()
1428     {
1429         if (isset($this->_options['treeImpl'])) {
1430             if ( ! $this->_tree) {
1431                 $options = isset($this->_options['treeOptions']) ? $this->_options['treeOptions'] : array();
1432                 $this->_tree = Doctrine_Tree::factory($this,
1433                     $this->_options['treeImpl'],
1434                     $options
1435                 );
1436             }
1437             return $this->_tree;
1438         }
1439         return false;
1440     }
1441
1442     /**
1443      * getComponentName
1444      *
1445      * @return void
1446      */
1447     public function getComponentName()
1448     {
1449         return $this->_options['name'];
1450     }
1451
1452     /**
1453      * getTableName
1454      *
1455      * @return void
1456      */
1457     public function getTableName()
1458     {
1459         return $this->_options['tableName'];
1460     }
1461
1462     /**
1463      * setTableName
1464      *
1465      * @param string $tableName 
1466      * @return void
1467      */
1468     public function setTableName($tableName)
1469     {
1470         $this->_options['tableName'] = $tableName;
1471     }
1472
1473     /**
1474      * isTree
1475      *
1476      * determine if table acts as tree
1477      *
1478      * @return mixed  if tree return true, otherwise returns false
1479      */
1480     public function isTree()
1481     {
1482         return ( ! is_null($this->_options['treeImpl'])) ? true : false;
1483     }
1484
1485     /**
1486      * getTemplate
1487      *
1488      * @param string $template 
1489      * @return void
1490      */
1491     public function getTemplate($template)
1492     {
1493         if ( ! isset($this->_templates[$template])) {
1494             throw new Doctrine_Table_Exception('Template ' . $template . ' not loaded');
1495         }
1496
1497         return $this->_templates[$template];
1498     }
1499     
1500     public function hasTemplate($template)
1501     {
1502         return isset($this->_templates[$template]);
1503     }
1504
1505     public function addTemplate($template, Doctrine_Template $impl)
1506     {
1507         $this->_templates[$template] = $impl;
1508
1509         return $this;
1510     }
1511
1512     /**
1513      * bindQueryParts
1514      * binds query parts to given component
1515      *
1516      * @param array $queryParts         an array of pre-bound query parts
1517      * @return Doctrine_Record          this object
1518      */
1519     public function bindQueryParts(array $queryParts)
1520     {
1521      $this->_options['queryParts'] = $queryParts;
1522
1523         return $this;
1524     }
1525
1526     /**
1527      * bindQueryPart
1528      * binds given value to given query part
1529      *
1530      * @param string $queryPart
1531      * @param mixed $value
1532      * @return Doctrine_Record          this object
1533      */
1534     public function bindQueryPart($queryPart, $value)
1535     {
1536      $this->_options['queryParts'][$queryPart] = $value;
1537
1538         return $this;
1539     }
1540     
1541     /**
1542      * getBoundQueryPart
1543      *
1544      * @param string $queryPart 
1545      * @return string $queryPart
1546      */
1547     public function getBoundQueryPart($queryPart)
1548     {
1549         if ( ! isset($this->_options['queryParts'][$queryPart])) {
1550             return null;
1551         }
1552
1553         return $this->_options['queryParts'][$queryPart];
1554     }
1555     
1556     /**
1557      * unshiftFilter
1558      *
1559      * @param  object Doctrine_Record_Filter $filter
1560      * @return object $this
1561      */
1562     public function unshiftFilter(Doctrine_Record_Filter $filter)
1563     {
1564         $filter->setTable($this);
1565
1566         $filter->init();
1567
1568         array_unshift($this->_filters, $filter);
1569
1570         return $this;
1571     }
1572     
1573     /**
1574      * getFilters
1575      *
1576      * @return array $filters
1577      */
1578     public function getFilters()
1579     {
1580         return $this->_filters;
1581     }
1582
1583     /**
1584      * returns a string representation of this object
1585      *
1586      * @return string
1587      */
1588     public function __toString()
1589     {
1590         return Doctrine_Lib::getTableAsString($this);
1591     }
1592     
1593     /**
1594      * findBy
1595      *
1596      * @param string $column 
1597      * @param string $value 
1598      * @param string $hydrationMode 
1599      * @return void
1600      */
1601     protected function findBy($column, $value, $hydrationMode = null)
1602     {
1603         return $this->createQuery()->where($column . ' = ?')->execute(array($value), $hydrationMode);
1604     }
1605     
1606     /**
1607      * findOneBy
1608      *
1609      * @param string $column 
1610      * @param string $value 
1611      * @param string $hydrationMode 
1612      * @return void
1613      */
1614     protected function findOneBy($column, $value, $hydrationMode = null)
1615     {
1616         $results = $this->createQuery()->where($column . ' = ?')->limit(1)->execute(array($value), $hydrationMode);
1617         
1618         return $hydrationMode === Doctrine::FETCH_ARRAY ? $results[0]:$results->getFirst();
1619     }
1620     
1621     /**
1622      * __call
1623      *
1624      * Adds support for magic finders.
1625      * findByColumnName, findByRelationAlias
1626      * findById, findByContactId, etc.
1627      *
1628      * @return void
1629      */
1630     public function __call($method, $arguments)
1631     {
1632         if (substr($method, 0, 6) == 'findBy') {
1633             $by = substr($method, 6, strlen($method));
1634             $method = 'findBy';
1635         } else if (substr($method, 0, 9) == 'findOneBy') {
1636             $by = substr($method, 9, strlen($method));
1637             $method = 'findOneBy';
1638         }
1639         
1640         if (isset($by)) {
1641             if (!isset($arguments[0])) {
1642                 throw new Doctrine_Table_Exception('You must specify the value to findBy');
1643             }
1644             
1645             $column = Doctrine::tableize($by);
1646             $hydrationMode = isset($arguments[1]) ? $arguments[1]:null;
1647             
1648             if ($this->hasColumn($column)) {
1649                 return $this->$method($column, $arguments[0], $hydrationMode);
1650             } else if ($this->hasRelation($by)) {
1651                 $relation = $this->getRelation($by);
1652                 
1653                 if ($relation['type'] === Doctrine_Relation::MANY) {
1654                     throw new Doctrine_Table_Exception('Cannot findBy many relationship.');
1655                 }
1656                 
1657                 return $this->$method($relation['local'], $arguments[0], $hydrationMode);
1658             } else {
1659                 throw new Doctrine_Table_Exception('Cannot find by: ' . $by . '. Invalid column or relationship alias.');
1660             }
1661         }
1662     }
1663 }