Coverage for Doctrine_Import_Builder

Back to coverage report

1 <?php
2 /*
3  *  $Id: Builder.php 2997 2007-10-23 03:50:45Z 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_Import_Builder
24  *
25  * Import builder is responsible of building Doctrine_Record classes
26  * based on a database schema.
27  *
28  * @package     Doctrine
29  * @subpackage  Import
30  * @link        www.phpdoctrine.com
31  * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
32  * @since       1.0
33  * @version     $Revision: 2997 $
34  * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
35  * @author      Jukka Hassinen <Jukka.Hassinen@BrainAlliance.com>
36  * @author      Nicolas BĂ©rard-Nault <nicobn@php.net>
37  * @author      Jonathan H. Wage <jwage@mac.com>
38  */
39 class Doctrine_Import_Builder
40 {
41     /**
42      * Path
43      * 
44      * the path where imported files are being generated
45      *
46      * @var string $path
47      */
48     private $path = '';
49     
50     private $packagesPrefix = 'Package';
51     
52     private $packagesPath = '';
53     
54     private $pathAfterPackage = DIRECTORY_SEPARATOR;
55     
56     /**
57      * suffix
58      * 
59      * File suffix to use when writing class definitions
60      *
61      * @var string $suffix
62      */
63     private $suffix = '.class.php';
64
65     /**
66      * generateBaseClasses
67      * 
68      * Bool true/false for whether or not to generate base classes
69      *
70      * @var string $suffix
71      */
72     private $generateBaseClasses = false;
73
74     /**
75      * baseClassesDirectory
76      * 
77      * Directory to put the generate base classes in
78      *
79      * @var string $suffix
80      */
81     private $baseClassesDirectory = 'generated';
82
83     /**
84      * tpl
85      *
86      * Class template used for writing classes
87      *
88      * @var $tpl
89      */
90     private static $tpl;
91
92     /**
93      * __construct
94      *
95      * @return void
96      */
97     public function __construct()
98     {
99         $this->loadTemplate();
100     }
101
102     /**
103      * setTargetPath
104      *
105      * @param string path   the path where imported files are being generated
106      * @return
107      */
108     public function setTargetPath($path)
109     {
110         if ( ! file_exists($path)) {
111             mkdir($path, 0777);
112         }
113
114         $this->path = $path;
115     }
116
117     /**
118      * generateBaseClasses
119      *
120      * Specify whether or not to generate classes which extend from generated base classes
121      *
122      * @param string $bool
123      * @return void
124      * @author Jonathan H. Wage
125      */
126     public function generateBaseClasses($bool = null)
127     {
128       if ($bool !== null) {
129         $this->generateBaseClasses = $bool;
130       }
131       
132       return $this->generateBaseClasses;
133     }
134
135     /**
136      * getTargetPath
137      *
138      * @return string       the path where imported files are being generated
139      */
140     public function getTargetPath()
141     {
142         return $this->path;
143     }
144
145     /**
146      * loadTemplate
147      * 
148      * Loads the class template used for generating classes
149      *
150      * @return void
151      */
152     public function loadTemplate() 
153     {
154         if (isset(self::$tpl)) {
155             return;
156         }
157
158         self::$tpl =<<<END
159 /**
160  * This class has been auto-generated by the Doctrine ORM Framework
161  */
162 %sclass %s extends %s
163 {
164 %s
165 %s
166 %s
167 }
168 END;
169     }
170
171     /*
172      * Build the accessors
173      *
174      * @param  string $table
175      * @param  array  $columns
176      */
177     public function buildAccessors(array $options, array $columns)
178     {
179         $ret = '';
180         foreach ($columns as $name => $column) {
181             // getters
182             $ret .= "\n\tpublic function get".Doctrine::classify($name)."(\$load = true)\n";
183             $ret .= "\t{\n";
184             $ret .= "\t\treturn \$this->get('{$name}', \$load);\n";
185             $ret .= "\t}\n";
186
187             // setters
188             $ret .= "\n\tpublic function set".Doctrine::classify($name)."(\${$name}, \$load = true)\n";
189             $ret .= "\t{\n";
190             $ret .= "\t\treturn \$this->set('{$name}', \${$name}, \$load);\n";
191             $ret .= "\t}\n";
192         }
193
194         return $ret;
195     }
196
197     /*
198      * Build the table definition of a Doctrine_Record object
199      *
200      * @param  string $table
201      * @param  array  $tableColumns
202      */
203     public function buildTableDefinition(array $options, array $columns, array $relations, array $indexes, array $attributes, array $templates, array $actAs)
204     {
205         $ret = array();
206         
207         $i = 0;
208         
209         if (isset($options['inheritance']['extends']) && !isset($options['override_parent'])) {
210             $ret[$i] = "\t\tparent::setTableDefinition();";
211             $i++;
212         }
213         
214         if (isset($options['tableName']) && !empty($options['tableName'])) {
215             $ret[$i] = "\t\t".'$this->setTableName(\''. $options['tableName'].'\');';
216             
217             $i++;
218         }
219         
220         foreach ($columns as $name => $column) {
221             $ret[$i] = "\t\t".'$this->hasColumn(\'' . $name . '\', \'' . $column['type'] . '\'';
222             
223             if ($column['length']) {
224                 $ret[$i] .= ', ' . $column['length'];
225             } else {
226                 $ret[$i] .= ', null';
227             }
228
229             $a = array();
230
231             if (isset($column['default'])) {
232                 $a[] = '\'default\' => ' . var_export($column['default'], true);
233             }
234             if (isset($column['notnull']) && $column['notnull']) {
235                 $a[] = '\'notnull\' => true';
236             }
237             if (isset($column['primary']) && $column['primary']) {
238                 $a[] = '\'primary\' => true';
239             }
240             if ((isset($column['autoinc']) && $column['autoinc']) || isset($column['autoincrement']) && $column['autoincrement']) {
241                 $a[] = '\'autoincrement\' => true';
242             }
243             if (isset($column['unique']) && $column['unique']) {
244                 $a[] = '\'unique\' => true';
245             }
246             if (isset($column['unsigned']) && $column['unsigned']) {
247                 $a[] = '\'unsigned\' => true';
248             }
249             if ($column['type'] == 'enum' && isset($column['values']) ) {
250                 $a[] = '\'values\' => array(\'' . implode('\',\'', $column['values']) . '\')';
251             }
252
253             if ( ! empty($a)) {
254                 $ret[$i] .= ', ' . 'array(';
255                 $length = strlen($ret[$i]);
256                 $ret[$i] .= implode(',' . PHP_EOL . str_repeat(' ', $length), $a) . ')';
257             }
258             
259             $ret[$i] .= ');';
260
261             if ($i < (count($columns) - 1)) {
262                 $ret[$i] .= PHP_EOL;
263             }
264             $i++;
265         }
266         
267         $ret[$i] = $this->buildIndexes($indexes);
268         $i++;
269         
270         $ret[$i] = $this->buildAttributes($attributes);
271         $i++;
272         
273         $ret[$i] = $this->buildTemplates($templates);
274         $i++;
275         
276         $ret[$i] = $this->buildActAs($actAs);
277         
278         if ( ! empty($ret)) {
279           return "\n\tpublic function setTableDefinition()"."\n\t{\n".implode("\n", $ret)."\n\t}";
280         }
281     }
282
283     /**
284      * buildTemplates
285      *
286      * @param string $array 
287      * @return void
288      */
289     public function buildTemplates(array $templates)
290     {
291         $build = '';
292         foreach ($templates as $name => $options) {
293             
294             if (is_array($options) && !empty($options)) {
295                 $optionsPhp = $this->arrayToPhpArrayCode($options);
296             
297                 $build .= "\t\t\$this->loadTemplate('" . $name . "', " . $optionsPhp . ");\n";
298             } else {
299                 if (isset($templates[0])) {
300                     $build .= "\t\t\$this->loadTemplate('" . $options . "');\n";
301                 } else {
302                     $build .= "\t\t\$this->loadTemplate('" . $name . "');\n";
303                 }
304             }
305         }
306         
307         return $build;
308     }
309
310     /**
311      * buildActAs
312      *
313      * @param string $array 
314      * @return void
315      */
316     public function buildActAs(array $actAs)
317     {
318         $build = '';
319         foreach ($actAs as $name => $options) {
320             if (is_array($options) && !empty($options)) {
321                 $optionsPhp = $this->arrayToPhp($options);
322                 
323                 $build .= "\t\t\$this->actAs('" . $name . "', " . $optionsPhp . ");\n";
324             } else {
325                 if (isset($actAs[0])) {
326                     $build .= "\t\t\$this->actAs('" . $options . "');\n";
327                 } else {
328                     $build .= "\t\t\$this->actAs('" . $name . "');\n";
329                 }
330             }
331         }
332         
333         return $build;
334     }
335
336     /**
337      * arrayToPhp
338      *
339      * @param string $array 
340      * @return void
341      */
342     protected function arrayToPhp(array $array)
343     {
344         ob_start();
345         var_export($array);
346         $php = ob_get_contents();
347         ob_end_clean();
348         
349         return $php;
350     }
351
352     /**
353      * buildAttributes
354      *
355      * @param string $array 
356      * @return void
357      */
358     public function buildAttributes(array $attributes)
359     {
360         $build = "\n";
361         foreach ($attributes as $key => $value) {
362             if ( ! is_array($value)) {
363                 $value = array($value);
364             }
365             
366             $values = '';
367             foreach ($value as $attr) {
368                 $values .= "Doctrine::" . strtoupper($key) . "_" . strtoupper($attr) . ' ^ ';
369             }
370             
371             // Trim last ^
372             $values = substr($values, 0, strlen($values) - 3);
373             
374             $build .= "\t\t\$this->setAttribute(Doctrine::ATTR_" . strtoupper($key) . ", " . $values . ");\n";
375         }
376         
377         return $build;
378     }
379
380     /**
381      * buildIndexes
382      *
383      * @param string $array 
384      * @return void
385      */
386     public function buildIndexes(array $indexes)
387     {
388       $build = '';
389
390       foreach ($indexes as $indexName => $definitions) {
391           $build = "\n\t\t".'$this->index(\'' . $indexName . '\', array(';
392
393           foreach ($definitions as $name => $value) {
394
395             // parse fields
396             if ($name === 'fields' || $name === 'columns') {
397               $build .= '\'fields\' => array(';
398
399               foreach ($value as $fieldName => $fieldValue) {
400                 $build .= '\'' . $fieldName . '\' => array( ';
401
402                 // parse options { sorting, length, primary }
403                 if (isset($fieldValue) && $fieldValue) {
404                   foreach ($fieldValue as $optionName => $optionValue) {
405
406                     $build .= '\'' . $optionName . '\' => ';
407
408                     // check primary option, mark either as true or false
409                     if ($optionName === 'primary') {
410                      $build .= (($optionValue == 'true') ? 'true' : 'false') . ', ';
411                      continue;
412                     }
413
414                     // convert sorting option to uppercase, for instance, asc -> ASC
415                     if ($optionName === 'sorting') {
416                      $build .= '\'' . strtoupper($optionValue) . '\', ';
417                      continue;
418                     }
419
420                     // check the rest of the options
421                     $build .= '\'' . $optionValue . '\', ';
422                   }
423                 }
424
425                 $build .= '), ';
426               }
427             }
428
429             // parse index type option, 4 choices { unique, fulltext, gist, gin }
430             if ($name === 'type') {
431              $build .= '), \'type\' => \'' . $value . '\'';
432             }
433
434             // add extra ) if type definition is not declared
435             if ( ! isset($definitions['type'])) {
436              $build .= ')';
437             }
438           }
439
440           $build .= '));';
441       }
442
443       return $build;
444     }
445
446     /**
447      * buildSetUp
448      *
449      * @param  array $options 
450      * @param  array $columns 
451      * @param  array $relations 
452      * @return string
453      */
454     public function buildSetUp(array $options, array $columns, array $relations)
455     {
456         $ret = array();
457         $i = 0;
458         
459         if ( !  (isset($options['override_parent']) && $options['override_parent'] === true)) {
460             $ret[$i] = "\t\tparent::setUp();";
461             $i++;
462         }
463         
464         foreach ($relations as $name => $relation) {
465             $class = isset($relation['class']) ? $relation['class']:$name;
466             $alias = (isset($relation['alias']) && $relation['alias'] !== $relation['class']) ? ' as ' . $relation['alias'] : '';
467
468             if ( ! isset($relation['type'])) {
469                 $relation['type'] = Doctrine_Relation::ONE;
470             }
471
472             if ($relation['type'] === Doctrine_Relation::ONE || 
473                 $relation['type'] === Doctrine_Relation::ONE_COMPOSITE) {
474                 $ret[$i] = "\t\t".'$this->hasOne(\'' . $class . $alias . '\'';
475             } else {
476                 $ret[$i] = "\t\t".'$this->hasMany(\'' . $class . $alias . '\'';
477             }
478             
479             $a = array();
480
481             if (isset($relation['refClass'])) {
482                 $a[] = '\'refClass\' => ' . var_export($relation['refClass'], true);
483             }
484             
485             if (isset($relation['deferred']) && $relation['deferred']) {
486                 $a[] = '\'default\' => ' . var_export($relation['deferred'], true);
487             }
488             
489             if (isset($relation['local']) && $relation['local']) {
490                 $a[] = '\'local\' => ' . var_export($relation['local'], true);
491             }
492             
493             if (isset($relation['foreign']) && $relation['foreign']) {
494                 $a[] = '\'foreign\' => ' . var_export($relation['foreign'], true);
495             }
496             
497             if (isset($relation['onDelete']) && $relation['onDelete']) {
498                 $a[] = '\'onDelete\' => ' . var_export($relation['onDelete'], true);
499             }
500             
501             if (isset($relation['onUpdate']) && $relation['onUpdate']) {
502                 $a[] = '\'onUpdate\' => ' . var_export($relation['onUpdate'], true);
503             }
504             
505             if (isset($relation['equal']) && $relation['equal']) { 
506                 $a[] = '\'equal\' => ' . var_export($relation['equal'], true); 
507             }
508             
509             if ( ! empty($a)) {
510                 $ret[$i] .= ', ' . 'array(';
511                 $length = strlen($ret[$i]);
512                 $ret[$i] .= implode(',' . PHP_EOL . str_repeat(' ', $length), $a) . ')';
513             }
514             
515             $ret[$i] .= ');'."\n";
516             $i++;
517         }
518         
519         if (isset($options['inheritance']['keyField']) && isset($options['inheritance']['keyValue'])) {
520             $i++;
521             $ret[$i] = "\t\t".'$this->setInheritanceMap(array(\''.$options['inheritance']['keyField'].'\' => '.$options['inheritance']['keyValue'].'));';
522         }
523         
524         if ( ! empty($ret)) {
525           return "\n\tpublic function setUp()\n\t{\n".implode("\n", $ret)."\n\t}";
526         }
527     }
528
529     /**
530      * buildDefinition
531      *
532      * @param array $options 
533      * @param array $columns 
534      * @param array $relations 
535      * @param array $indexes 
536      * @param array $attributes 
537      * @param array $templates 
538      * @param array $actAs 
539      * @return string
540      */
541     public function buildDefinition(array $options, array $columns, array $relations = array(), array $indexes = array(), $attributes = array(), array $templates = array(), array $actAs = array())
542     {
543         if ( ! isset($options['className'])) {
544             throw new Doctrine_Import_Builder_Exception('Missing class name.');
545         }
546
547         $abstract = isset($options['abstract']) && $options['abstract'] === true ? 'abstract ':null;
548         $className = $options['className'];
549         $extends = isset($options['inheritance']['extends']) ? $options['inheritance']['extends']:'Doctrine_Record';
550
551         if ( ! (isset($options['no_definition']) && $options['no_definition'] === true)) {
552             $definition = $this->buildTableDefinition($options, $columns, $relations, $indexes, $attributes, $templates, $actAs);
553             $setUp = $this->buildSetUp($options, $columns, $relations);
554         } else {
555             $definition = null;
556             $setUp = null;
557         }
558         
559         $accessors = (isset($options['generate_accessors']) && $options['generate_accessors'] === true) ? $this->buildAccessors($options, $columns):null;
560         
561         $content = sprintf(self::$tpl, $abstract,
562                                        $className,
563                                        $extends,
564                                        $definition,
565                                        $setUp,
566                                        $accessors);
567         
568         return $content;
569     }
570
571     /**
572      * buildRecord
573      *
574      * @param array $options 
575      * @param array $columns 
576      * @param array $relations 
577      * @param array $indexes 
578      * @param array $attributes 
579      * @param array $templates 
580      * @param array $actAs 
581      * @return void=
582      */
583     public function buildRecord(array $options, array $columns, array $relations = array(), array $indexes = array(), array $attributes = array(), array $templates = array(), array $actAs = array())
584     {
585         if ( !isset($options['className'])) {
586             throw new Doctrine_Import_Builder_Exception('Missing class name.');
587         }
588
589         if ( !isset($options['fileName'])) {
590             if (empty($this->path)) {
591                 throw new Doctrine_Import_Builder_Exception('No build target directory set.');
592             }
593             
594
595             if (is_writable($this->path) === false) {
596                 throw new Doctrine_Import_Builder_Exception('Build target directory ' . $this->path . ' is not writable.');
597             }
598
599             $options['fileName']  = $this->path . DIRECTORY_SEPARATOR . $options['className'] . $this->suffix;
600         }
601         
602         if ($this->generateBaseClasses()) {
603           
604           // We only want to generate this one if it doesn't already exist
605           if ( ! file_exists($options['fileName'])) {
606             $optionsBak = $options;
607             
608             unset($options['tableName']);
609             $options['inheritance']['extends'] = 'Base' . $options['className'];
610             $options['requires'] = array($this->baseClassesDirectory . DIRECTORY_SEPARATOR  . $options['inheritance']['extends'] . $this->suffix);
611             $options['no_definition'] = true;
612             
613             $this->writeDefinition($options);
614             
615             $options = $optionsBak;
616           }
617           
618           $generatedPath = $this->path . DIRECTORY_SEPARATOR . $this->baseClassesDirectory;
619           
620           if ( ! file_exists($generatedPath)) {
621             mkdir($generatedPath);
622           }
623           
624           $options['className'] = 'Base' . $options['className'];
625           $options['abstract'] = true;
626           $options['fileName']  = $generatedPath . DIRECTORY_SEPARATOR . $options['className'] . $this->suffix;
627           $options['override_parent'] = true;
628           
629           $this->writeDefinition($options, $columns, $relations, $indexes, $attributes, $templates, $actAs);
630         } else {
631           $this->writeDefinition($options, $columns, $relations, $indexes, $attributes, $templates, $actAs);
632         }
633     }
634
635     /**
636      * writeDefinition
637      *
638      * @param array $options 
639      * @param array $columns 
640      * @param array $relations 
641      * @param array $indexes 
642      * @param array $attributes 
643      * @param array $templates 
644      * @param array $actAs 
645      * @return void
646      */
647     public function writeDefinition(array $options, array $columns = array(), array $relations = array(), array $indexes = array(), array $attributes = array(), array $templates = array(), array $actAs = array())
648     {
649         $content = $this->buildDefinition($options, $columns, $relations, $indexes, $attributes, $templates, $actAs);
650         $code = "<?php\n";
651         
652         if (isset($options['requires'])) {
653             if ( ! is_array($options['requires'])) {
654                 $options['requires'] = array($options['requires']);
655             }
656
657             foreach ($options['requires'] as $require) {
658                 $code .= "require_once('".$require."');\n";
659             }
660         }
661         
662         if (isset($options['connection']) && $options['connection']) {
663             $code .= "// Connection Component Binding\n";
664             $code .= "Doctrine_Manager::getInstance()->bindComponent('" . $options['connectionClassName'] . "', '" . $options['connection'] . "');\n";
665         }
666         
667         $code .= PHP_EOL . $content;
668
669         $bytes = file_put_contents($options['fileName'], $code);
670
671         if ($bytes === false) {
672             throw new Doctrine_Import_Builder_Exception("Couldn't write file " . $options['fileName']);
673         }
674     }
675 }