Coverage for Doctrine_Import_Schema

Back to coverage report

1 <?php
2 /*
3  * $Id: Schema.php 1838 2007-06-26 00:58:21Z nicobn $
4  *
5  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
6  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
7  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
8  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
9  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
11  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
12  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
13  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
14  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
15  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16  *
17  * This software consists of voluntary contributions made by many individuals
18  * and is licensed under the LGPL. For more information, see
19  * <http://www.phpdoctrine.org>.
20  */
21
22 /**
23  * Doctrine_Import_Schema
24  *
25  * Class for importing Doctrine_Record classes from a yaml schema definition
26  *
27  * @package     Doctrine
28  * @subpackage  Import
29  * @link        www.phpdoctrine.org
30  * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
31  * @version     $Revision: 1838 $
32  * @author      Nicolas BĂ©rard-Nault <nicobn@gmail.com>
33  * @author      Jonathan H. Wage <jonwage@gmail.com>
34  */
35 class Doctrine_Import_Schema
36 {
37     /**
38      * _relations
39      *
40      * Array of all relationships parsed from all schema files
41      *
42      * @var array
43      */
44     protected $_relations = array();
45
46     /**
47      * _options
48      *
49      * Array of options used to configure the model generation from the parsed schema files
50      * This array is forwarded to Doctrine_Import_Builder
51      *
52      * @var array
53      */
54     protected $_options = array('packagesPrefix'        =>  'Package',
55                                 'packagesPath'          =>  '',
56                                 'packagesFolderName'    =>  'packages',
57                                 'suffix'                =>  '.php',
58                                 'generateBaseClasses'   =>  true,
59                                 'baseClassesPrefix'     =>  'Base',
60                                 'baseClassesDirectory'  =>  'generated',
61                                 'baseClassName'         =>  'Doctrine_Record');
62
63     /**
64      * _validation
65      *
66      * Array used to validate schema element.
67      * See: _validateSchemaElement
68      *
69      * @var array
70      */
71     protected $_validation = array('root'       =>  array('connection',
72                                                           'className',
73                                                           'tableName',
74                                                           'connection',
75                                                           'relations',
76                                                           'columns',
77                                                           'indexes',
78                                                           'attributes',
79                                                           'templates',
80                                                           'actAs',
81                                                           'options',
82                                                           'package',
83                                                           'inheritance',
84                                                           'detect_relations',
85                                                           'generate_accessors'),
86
87                                    'column'     =>  array('name',
88                                                           'format',
89                                                           'fixed',
90                                                           'primary',
91                                                           'autoincrement',
92                                                           'type',
93                                                           'length',
94                                                           'size',
95                                                           'default',
96                                                           'scale',
97                                                           'values'),
98
99                                    'relation'   =>  array('key',
100                                                           'class',
101                                                           'alias',
102                                                           'type',
103                                                           'refClass',
104                                                           'local',
105                                                           'foreign',
106                                                           'foreignAlias',
107                                                           'foreignType',
108                                                           'autoComplete',
109                                                           'onDelete',
110                                                           'onUpdate',
111                                                           'equal'),
112
113                                    'inheritance'=>  array('type',
114                                                           'extends',
115                                                           'keyField',
116                                                           'keyValue'));
117
118     /**
119      * _validators
120      *
121      * Array of available validators
122      *
123      * @see getValidators()
124      * @var array Array of available validators
125      */
126     protected $_validators = array();
127
128     /**
129      * getValidators
130      *
131      * Retrieve the array of available validators
132      *
133      * @return array
134      */
135     public function getValidators()
136     {
137         if (empty($this->_validators)) {
138             $this->_validators = Doctrine_Lib::getValidators();
139         }
140
141         return $this->_validators;
142     }
143
144     /**
145      * getOption
146      *
147      * @param string $name 
148      * @return void
149      */
150     public function getOption($name)
151     {
152         if (isset($this->_options[$name]))   {
153             return $this->_options[$name];
154         }
155     }
156
157     /**
158      * getOptions
159      *
160      * @return void
161      */
162     public function getOptions()
163     {
164         return $this->_options;
165     }
166
167     /**
168      * setOption
169      *
170      * @param string $name 
171      * @param string $value 
172      * @return void
173      */
174     public function setOption($name, $value)
175     {
176         if (isset($this->_options[$name])) {
177             $this->_options[$name] = $value;
178         }
179     }
180     
181     /**
182      * setOptions
183      *
184      * @param string $options 
185      * @return void
186      */
187     public function setOptions($options)
188     {
189         if ( ! empty($options)) {
190           $this->_options = $options;
191         }
192     }
193
194     /**
195      * buildSchema
196      *
197      * Loop throug directories of schema files and parse them all in to one complete array of schema information
198      *
199      * @param  string   $schema Array of schema files or single schema file. Array of directories with schema files or single directory
200      * @param  string   $format Format of the files we are parsing and building from
201      * @return array    $array
202      */
203     public function buildSchema($schema, $format)
204     {
205         $array = array();
206
207         foreach ((array) $schema AS $s) {
208             if (is_file($s)) {
209                 $array = array_merge($array, $this->parseSchema($s, $format));
210             } else if (is_dir($s)) {
211                 $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($s),
212                                                       RecursiveIteratorIterator::LEAVES_ONLY);
213
214                 foreach ($it as $file) {
215                     $e = explode('.', $file->getFileName());
216                     if (end($e) === $format) {
217                         $array = array_merge($array, $this->parseSchema($file->getPathName(), $format));
218                     }
219                 }
220             } else {
221               $array = array_merge($array, $this->parseSchema($s, $format));
222             }
223         }
224
225         $array = $this->_buildRelationships($array);
226         $array = $this->_processInheritance($array);
227
228         return $array;
229     }
230
231     /**
232      * importSchema
233      *
234      * A method to import a Schema and translate it into a Doctrine_Record object
235      *
236      * @param  string $schema       The file containing the XML schema
237      * @param  string $format       Format of the schema file
238      * @param  string $directory    The directory where the Doctrine_Record class will be written
239      * @param  array  $models       Optional array of models to import
240      *
241      * @return void
242      */
243     public function importSchema($schema, $format = 'yml', $directory = null, $models = array())
244     {
245         $builder = new Doctrine_Import_Builder();
246         $builder->setTargetPath($directory);
247         $builder->setOptions($this->getOptions());
248         
249         $array = $this->buildSchema($schema, $format);
250
251         foreach ($array as $name => $definition) {
252             if ( ! empty($models) && !in_array($definition['className'], $models)) {
253                 continue;
254             }
255             
256             $builder->buildRecord($definition);
257         }
258     }
259
260     /**
261      * parseSchema
262      *
263      * A method to parse a Schema and translate it into a property array.
264      * The function returns that property array.
265      *
266      * @param  string $schema   Path to the file containing the schema
267      * @param  string $type     Format type of the schema we are parsing
268      * @return array  $build    Built array of schema information
269      */
270     public function parseSchema($schema, $type)
271     {
272         $defaults = array('className'           =>  null,
273                           'tableName'           =>  null,
274                           'connection'          =>  null,
275                           'relations'           =>  array(),
276                           'indexes'             =>  array(),
277                           'attributes'          =>  array(),
278                           'templates'           =>  array(),
279                           'actAs'               =>  array(),
280                           'options'             =>  array(),
281                           'package'             =>  null,
282                           'inheritance'         =>  array(),
283                           'detect_relations'    =>  false,
284                           'generate_accessors'  =>  false);
285         
286         $array = Doctrine_Parser::load($schema, $type);
287
288         // Go through the schema and look for global values so we can assign them to each table/class
289         $globals = array();
290         $globalKeys = array('connection',
291                             'attributes',
292                             'templates',
293                             'actAs',
294                             'options',
295                             'package',
296                             'inheritance',
297                             'detect_relations',
298                             'generate_accessors');
299
300         // Loop over and build up all the global values and remove them from the array
301         foreach ($array as $key => $value) {
302             if (in_array($key, $globalKeys)) {
303                 unset($array[$key]);
304                 $globals[$key] = $value;
305             }
306         }
307
308         // Apply the globals to each table if it does not have a custom value set already
309         foreach ($array as $className => $table) {
310             foreach ($globals as $key => $value) {
311                 if (!isset($array[$className][$key])) {
312                     $array[$className][$key] = $value;
313                 }
314             }
315         }
316
317         $build = array();
318
319         foreach ($array as $className => $table) {
320             $this->_validateSchemaElement('root', array_keys($table));
321
322             $columns = array();
323
324             $className = isset($table['className']) ? (string) $table['className']:(string) $className;
325
326             if (isset($table['inheritance']['keyField']) || isset($table['inheritance']['keyValue'])) {
327                 $table['inheritance']['type'] = 'column_aggregation';
328             }
329
330             if (isset($table['tableName']) && $table['tableName']) {
331                 $tableName = $table['tableName'];
332             } else {
333                 if (isset($table['inheritance']['type']) && ($table['inheritance']['type'] == 'column_aggregation')) {
334                     $tableName = null;
335                 } else {
336                     $tableName = Doctrine::tableize($className);
337                 }
338             }
339
340             $connection = isset($table['connection']) ? $table['connection']:'current';
341
342             $columns = isset($table['columns']) ? $table['columns']:array();
343
344             if ( ! empty($columns)) {
345                 foreach ($columns as $columnName => $field) {
346
347                     // Support short syntax: my_column: integer(4)
348                     if ( ! is_array($field)) {
349                         $original = $field;
350                         $field = array();
351                         $field['type'] = $original;
352                     }
353
354                     $this->_validateSchemaElement('column', array_keys($field));
355
356                     $colDesc = array();
357                     $colDesc['name'] = $columnName;
358
359                     // Support short type(length) syntax: my_column: { type: integer(4) }
360                     $e = explode('(', $field['type']);
361                     if (isset($e[0]) && isset($e[1])) {
362                         $colDesc['type'] = $e[0];
363                         $colDesc['length'] = substr($e[1], 0, strlen($e[1]) - 1);
364                     } else {
365                         $colDesc['type'] = isset($field['type']) ? (string) $field['type']:null;
366                         $colDesc['length'] = isset($field['length']) ? (int) $field['length']:null;
367                         $colDesc['length'] = isset($field['size']) ? (int) $field['size']:$colDesc['length'];
368                     }
369
370                     $colDesc['fixed'] = isset($field['fixed']) ? (int) $field['fixed']:null;
371                     $colDesc['primary'] = isset($field['primary']) ? (bool) (isset($field['primary']) && $field['primary']):null;
372                     $colDesc['default'] = isset($field['default']) ? $field['default']:null;
373                     $colDesc['autoincrement'] = isset($field['autoincrement']) ? (bool) (isset($field['autoincrement']) && $field['autoincrement']):null;
374                     $colDesc['sequence'] = isset($field['sequence']) ? (string) $field['sequence']:null;
375                     $colDesc['values'] = isset($field['values']) ? (array) $field['values']:null;
376
377                     // Include all the specified and valid validators in the colDesc
378                     $validators = $this->getValidators();
379
380                     foreach ($validators as $validator) {
381                         if (isset($field[$validator])) {
382                             $colDesc[$validator] = $field[$validator];
383                         }
384                     }
385
386                     $columns[(string) $colDesc['name']] = $colDesc;
387                 }
388             }
389
390             // Apply the default values
391             foreach ($defaults as $key => $defaultValue) {
392                 if (isset($table[$key]) && ! isset($build[$className][$key])) {
393                     $build[$className][$key] = $table[$key];
394                 } else {
395                     $build[$className][$key] = isset($build[$className][$key]) ? $build[$className][$key]:$defaultValue;
396                 }
397             }
398             
399             $build[$className]['className'] = $className;
400             $build[$className]['tableName'] = $tableName;
401             $build[$className]['columns'] = $columns;
402             
403             // Make sure that anything else that is specified in the schema makes it to the final array
404             $build[$className] = Doctrine_Lib::arrayDeepMerge($table, $build[$className]);
405             
406             // We need to keep track of the className for the connection
407             $build[$className]['connectionClassName'] = $build[$className]['className'];
408         }
409
410         return $build;
411     }
412
413     /**
414      * _processInheritance
415      * 
416      * Perform some processing on inheritance.
417      * Sets the default type and sets some default values for certain types
418      *
419      * @param string $array 
420      * @return void
421      */
422     protected function _processInheritance($array)
423     {
424         // Apply default inheritance configuration
425         foreach ($array as $className => $definition) {
426             if ( ! empty($array[$className]['inheritance'])) {
427                 $this->_validateSchemaElement('inheritance', array_keys($definition['inheritance']));
428
429                 // Default inheritance to class table inheritance
430                 if ( ! isset($array[$className]['inheritance']['type'])) {
431                     $array[$className]['inheritance']['type'] = 'class_table';
432                 }
433
434                 // Some magic for setting up the keyField and keyValue column aggregation options
435                 // Adds keyField to the parent class automatically
436                 if ($array[$className]['inheritance']['type'] == 'column_aggregation') {
437                     // Set the keyField to 'type' by default
438                     if ( ! isset($array[$className]['inheritance']['keyField'])) {
439                         $array[$className]['inheritance']['keyField'] = 'type';                        
440                     }
441                     
442                     // Set the keyValue to the name of the child class if it does not exist
443                     if ( ! isset($array[$className]['inheritance']['keyValue'])) {
444                         $array[$className]['inheritance']['keyValue'] = $className;
445                     }
446                     
447                     // Add the keyType column to the parent if a definition does not already exist
448                     if ( ! isset($array[$array[$className]['inheritance']['extends']]['columns']['type'])) {
449                         $array[$definition['inheritance']['extends']]['columns']['type'] = array('name' => 'type', 'type' => 'string', 'length' => 255);
450                     }
451                 }
452             }
453         }
454
455         // Array of the array keys to move to the parent, and the value to default the child definition to
456         // after moving it. Will also populate the subclasses array for the inheritance parent
457         $moves = array('columns' => array());
458         
459         foreach ($array as $className => $definition) {
460             // Move any definitions on the schema to the parent
461             if (isset($definition['inheritance']['extends']) && isset($definition['inheritance']['type']) && ($definition['inheritance']['type'] == 'simple' || $definition['inheritance']['type'] == 'column_aggregation')) {
462                 $extends = $definition['inheritance']['extends'];
463
464                 foreach ($moves as $move => $resetValue) {
465                     $array[$extends][$move] = Doctrine_Lib::arrayDeepMerge($array[$extends][$move], $definition[$move]);
466                     $array[$definition['className']][$move] = $resetValue;
467                 }
468
469                 // Populate the parents subclasses
470                 if ($definition['inheritance']['type'] == 'column_aggregation') {
471                     $array[$extends]['inheritance']['subclasses'][$definition['className']] = array($definition['inheritance']['keyField'] => $definition['inheritance']['keyValue']);
472                 }
473             }
474         }
475
476         return $array;
477     }
478
479     /**
480      * buildRelationships
481      *
482      * Loop through an array of schema information and build all the necessary relationship information
483      * Will attempt to auto complete relationships and simplify the amount of information required 
484      * for defining a relationship
485      *
486      * @param  string $array 
487      * @return void
488      */
489     protected function _buildRelationships($array)
490     {
491         // Handle auto detecting relations by the names of columns
492         // User.contact_id will automatically create User hasOne Contact local => contact_id, foreign => id
493         foreach ($array as $className => $properties) {
494             if (isset($properties['columns']) && ! empty($properties['columns']) && isset($properties['detect_relations']) && $properties['detect_relations']) {
495                 foreach ($properties['columns'] as $column) {
496                     // Check if the column we are inflecting has a _id on the end of it before trying to inflect it and find
497                     // the class name for the column
498                     if (strpos($column['name'], '_id')) {
499                         $columnClassName = Doctrine_Inflector::classify(str_replace('_id', '', $column['name']));
500                         if (isset($array[$columnClassName]) && !isset($array[$className]['relations'][$columnClassName])) {
501                             $array[$className]['relations'][$columnClassName] = array();
502                         }
503                     }
504                 }
505             }
506         }
507
508         foreach ($array as $name => $properties) {
509             if ( ! isset($properties['relations'])) {
510                 continue;
511             }
512             
513             $className = $properties['className'];
514             $relations = $properties['relations'];
515             
516             foreach ($relations as $alias => $relation) {
517                 $class = isset($relation['class']) ? $relation['class']:$alias;
518                 
519                 $relation['class'] = $class;
520                 $relation['alias'] = isset($relation['alias']) ? $relation['alias'] : $alias;
521                 
522                 // Attempt to guess the local and foreign
523                 if (isset($relation['refClass'])) {
524                     $relation['local'] = isset($relation['local']) ? $relation['local']:Doctrine::tableize($name) . '_id';
525                     $relation['foreign'] = isset($relation['foreign']) ? $relation['foreign']:Doctrine::tableize($class) . '_id';
526                 } else {
527                     $relation['local'] = isset($relation['local']) ? $relation['local']:Doctrine::tableize($relation['class']) . '_id';
528                     $relation['foreign'] = isset($relation['foreign']) ? $relation['foreign']:'id';
529                 }
530                 
531                 if (isset($relation['refClass'])) {
532                     $relation['type'] = 'many';
533                 }
534                 
535                 if (isset($relation['type']) && $relation['type']) {
536                     $relation['type'] = $relation['type'] === 'one' ? Doctrine_Relation::ONE:Doctrine_Relation::MANY;
537                 } else {
538                     $relation['type'] = Doctrine_Relation::ONE;
539                 }
540
541                 if (isset($relation['foreignType']) && $relation['foreignType']) {
542                     $relation['foreignType'] = $relation['foreignType'] === 'one' ? Doctrine_Relation::ONE:Doctrine_Relation::MANY;
543                 }
544                 
545                 $relation['key'] = $this->_buildUniqueRelationKey($relation);
546                 
547                 $this->_validateSchemaElement('relation', array_keys($relation));
548                 
549                 $this->_relations[$className][$alias] = $relation;
550             }
551         }
552         
553         // Now we auto-complete opposite ends of relationships
554         $this->_autoCompleteOppositeRelations();
555         
556         // Make sure we do not have any duplicate relations
557         $this->_fixDuplicateRelations();
558
559         // Set the full array of relationships for each class to the final array
560         foreach ($this->_relations as $className => $relations) {
561             $array[$className]['relations'] = $relations;
562         }
563         
564         return $array;
565     }
566
567     /**
568      * fixRelationships
569      *
570      * Loop through all relationships building the opposite ends of each relationship
571      * and make sure no duplicate relations exist
572      *
573      * @return void
574      */
575     protected function _autoCompleteOppositeRelations()
576     {
577         foreach($this->_relations as $className => $relations) {
578             foreach ($relations AS $alias => $relation) {
579                 if ((isset($relation['equal']) && $relation['equal']) || (isset($relation['autoComplete']) && $relation['autoComplete'] === false)) {
580                     continue;
581                 }
582                 
583                 $newRelation = array();
584                 $newRelation['foreign'] = $relation['local'];
585                 $newRelation['local'] = $relation['foreign'];
586                 $newRelation['class'] = isset($relation['foreignClass']) ? $relation['foreignClass']:$className;
587                 $newRelation['alias'] = isset($relation['foreignAlias']) ? $relation['foreignAlias']:$className;
588                 
589                 // this is so that we know that this relation was autogenerated and
590                 // that we do not need to include it if it is explicitly declared in the schema by the users.
591                 $newRelation['autogenerated'] = true; 
592                 
593                 if (isset($relation['refClass'])) {
594                     $newRelation['refClass'] = $relation['refClass'];
595                     $newRelation['type'] = isset($relation['foreignType']) ? $relation['foreignType']:$relation['type'];
596                 } else {                
597                     if(isset($relation['foreignType'])) {
598                         $newRelation['type'] = $relation['foreignType'];
599                     } else {
600                         $newRelation['type'] = $relation['type'] === Doctrine_Relation::ONE ? Doctrine_Relation::MANY:Doctrine_Relation::ONE;
601                     }
602                 }
603
604                 // Make sure it doesn't already exist
605                 if ( ! isset($this->_relations[$relation['class']][$newRelation['alias']])) {
606                     $newRelation['key'] = $this->_buildUniqueRelationKey($newRelation);
607                     $this->_relations[$relation['class']][$newRelation['alias']] = $newRelation;
608                 }
609             }
610         }
611     }
612
613     /**
614      * _fixDuplicateRelations
615      *
616      * Ensure the relations for each class are unique and that no duplicated relations exist from the auto generated relations
617      * and the user explicitely defining the opposite end
618      *
619      * @return void
620      */
621     protected function _fixDuplicateRelations()
622     {
623         foreach($this->_relations as $className => $relations) {
624             // This is for checking for duplicates between alias-relations and a auto-generated relations to ensure the result set of unique relations
625             $existingRelations = array();
626             $uniqueRelations = array();
627             foreach ($relations as $relation) {
628                 if ( ! in_array($relation['key'], $existingRelations)) {
629                     $existingRelations[] = $relation['key'];
630                     $uniqueRelations = array_merge($uniqueRelations, array($relation['alias'] => $relation));
631                 } else {
632                     // check to see if this relationship is not autogenerated, if it's not, then the user must have explicitly declared it
633                     if ( ! isset($relation['autogenerated']) || $relation['autogenerated'] != true) {
634                         $uniqueRelations = array_merge($uniqueRelations, array($relation['alias'] => $relation));
635                     }
636                 }
637             }
638             
639             $this->_relations[$className] = $uniqueRelations;
640         }
641     }
642
643     /**
644      * _buildUniqueRelationKey
645      *
646      * Build a unique key to identify a relationship by
647      * Md5 hash of all the relationship parameters
648      *
649      * @param string $relation 
650      * @return void
651      */
652     protected function _buildUniqueRelationKey($relation)
653     {
654         return md5($relation['local'].$relation['foreign'].$relation['class'].(isset($relation['refClass']) ? $relation['refClass']:null));
655     }
656
657     /**
658      * _validateSchemaElement
659      *
660      * @param string $name 
661      * @param string $value 
662      * @return void
663      */
664     protected function _validateSchemaElement($name, $element)
665     {
666         $element = (array) $element;
667
668         $validation = $this->_validation[$name];
669
670         // Validators are a part of the column validation
671         // This should be fixed, made cleaner
672         if ($name == 'column') {
673             $validators = $this->getValidators();
674             $validation = array_merge($validation, $validators);
675         }
676
677         $validation = array_flip($validation);
678         foreach ($element as $key => $value) {
679             if ( ! isset($validation[$value])) {
680                 throw new Doctrine_Import_Exception('Invalid schema element named "' . $value . '"');
681             }
682         }
683     }
684 }