Development

/branches/1.0/lib/addon/propel/sfPropelDatabaseSchema.class.php

You must first sign up to be able to contribute.

root/branches/1.0/lib/addon/propel/sfPropelDatabaseSchema.class.php

Revision 15765, 21.6 kB (checked in by fabien, 6 years ago)

[1.0, 1.1, 1.2, 1.3] fixed propel:schema-to-yml composite foreign-Keys wrongly converted (closes #5483)

Line 
1 <?php
2
3 /*
4  * This file is part of the symfony package.
5  * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com>
6  *
7  * For the full copyright and license information, please view the LICENSE
8  * file that was distributed with this source code.
9  */
10
11 /**
12  *
13  * @package    symfony
14  * @subpackage addon
15  * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
16  * @author     Fran├žois Zaninotto <francois.zaninotto@symfony-project.com>
17  * @version    SVN: $Id$
18  */
19 class sfPropelDatabaseSchema
20 {
21   protected $connection_name = '';
22   protected $database        = array();
23
24   public function asArray()
25   {
26     return array($this->connection_name => $this->database);
27   }
28
29   public function loadYAML($file)
30   {
31     $schema = sfYaml::load($file);
32
33     if (count($schema) > 1)
34     {
35       throw new sfException('A schema.yml must only contain 1 database entry.');
36     }
37
38     $tmp = array_keys($schema);
39     $this->connection_name = array_shift($tmp);
40     if ($this->connection_name)
41     {
42       $this->database = $schema[$this->connection_name];
43
44       $this->fixYAMLDatabase();
45       $this->fixYAMLI18n();
46       $this->fixYAMLColumns();
47     }
48   }
49
50   public function asXML()
51   {
52     $xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
53
54     $xml .= "<database name=\"$this->connection_name\"".$this->getAttributesFor($this->database).">\n";
55
56     // tables
57     foreach ($this->getChildren($this->database) as $tb_name => $table)
58     {
59       $xml .= "\n  <table name=\"$tb_name\"".$this->getAttributesFor($table).">\n";
60
61       // columns
62       foreach ($this->getChildren($table) as $col_name => $column)
63       {
64         $xml .= "    <column name=\"$col_name\"".$this->getAttributesForColumn($tb_name, $col_name, $column);
65       }
66
67       // indexes
68       if (isset($table['_indexes']))
69       {
70         foreach ($table['_indexes'] as $index_name => $index)
71         {
72           $xml .= "    <index name=\"$index_name\">\n";
73           foreach ($index as $index_column)
74           {
75             preg_match('/^(.+?)\(([\d]+)\)$/', $index_column, $matches);
76             if (isset($matches[2]))
77             {
78               $xml .= "      <index-column name=\"{$matches[1]}\" size=\"{$matches[2]}\" />\n";
79             }
80             else
81             {
82               $xml .= "      <index-column name=\"$index_column\" />\n";
83             }
84           }
85           $xml .= "    </index>\n";
86         }
87       }
88
89       // uniques
90       if (isset($table['_uniques']))
91       {
92         foreach ($table['_uniques'] as $unique_name => $index)
93         {
94           $xml .= "    <unique name=\"$unique_name\">\n";
95           foreach ($index as $unique_column)
96           {
97             preg_match('/^(.+?)\(([\d]+)\)$/', $unique_column, $matches);
98             if (isset($matches[2]))
99             {
100               $xml .= "      <unique-column name=\"{$matches[1]}\" size=\"{$matches[2]}\" />\n";
101             }
102             else
103             {
104               $xml .= "      <unique-column name=\"$unique_column\" />\n";
105             }
106           }
107           $xml .= "    </unique>\n";
108         }
109       }
110
111       // foreign-keys
112       if (isset($table['_foreignKeys']))
113       {
114         foreach ($table['_foreignKeys'] as $fkey_name => $fkey)
115         {
116           $xml .= "    <foreign-key foreignTable=\"$fkey[foreignTable]\"";
117
118           // foreign key name
119           if (!is_numeric($fkey_name))
120           {
121             $xml .= " name=\"$fkey_name\"";
122           }
123
124           // onDelete
125           if (isset($fkey['onDelete']))
126           {
127             $xml .= " onDelete=\"$fkey[onDelete]\"";
128           }
129
130           // onUpdate
131           if (isset($fkey['onUpdate']))
132           {
133             $xml .= " onUpdate=\"$fkey[onUpdate]\"";
134           }
135           $xml .= ">\n";
136
137           // references
138           if (isset($fkey['references']))
139           {
140             foreach ($fkey['references'] as $reference)
141             {
142               $xml .= "      <reference local=\"$reference[local]\" foreign=\"$reference[foreign]\" />\n";
143             }
144           }
145           $xml .= "    </foreign-key>\n";
146         }
147       }
148
149       $xml .= "  </table>\n";
150     }
151     $xml .= "\n</database>\n";
152
153     return $xml;
154   }
155
156   protected function fixYAMLDatabase()
157   {
158     if (!isset($this->database['_attributes']))
159     {
160       $this->database['_attributes'] = array();
161     }
162
163     // conventions for database attributes
164     $this->setIfNotSet($this->database['_attributes'], 'defaultIdMethod', 'native');
165     $this->setIfNotSet($this->database['_attributes'], 'noXsd', true);
166     $this->setIfNotSet($this->database['_attributes'], 'package', 'lib.model');
167   }
168
169   protected function fixYAMLI18n()
170   {
171     foreach ($this->getTables() as $i18n_table => $columns)
172     {
173       $pos = strpos($i18n_table, '_i18n');
174
175       $has_primary_key = false;
176       foreach ($columns as $column => $attributes)
177       {
178         if (is_array($attributes) && array_key_exists('primaryKey', $attributes))
179         {
180            $has_primary_key = true;
181         }
182       }
183
184       if ($pos > 0 && $pos == strlen($i18n_table) - 5 && !$has_primary_key)
185       {
186         // i18n table without primary key
187         $main_table = $this->findTable(substr($i18n_table, 0, $pos));
188
189         if ($main_table)
190         {
191           // set i18n attributes for main table
192           $this->setIfNotSet($this->database[$main_table]['_attributes'], 'isI18N', 1);
193           $this->setIfNotSet($this->database[$main_table]['_attributes'], 'i18nTable', $i18n_table);
194
195           // set id and culture columns for i18n table
196           $this->setIfNotSet($this->database[$i18n_table], 'id', array(
197             'type'             => 'integer',
198             'required'         => true,
199             'primaryKey'       => true,
200             'foreignTable'     => $main_table,
201             'foreignReference' => 'id',
202             'onDelete'         => 'cascade'
203           ));
204           $this->setIfNotSet($this->database[$i18n_table], 'culture', array(
205             'isCulture'  => true,
206             'type'       => 'varchar',
207             'size'       => '7',
208             'required'   => true,
209             'primaryKey' => true
210           ));
211         }
212         else
213         {
214           throw new sfException(sprintf('Missing main table for internationalized table "%s".', $i18n_table));
215         }
216       }
217     }
218   }
219
220   protected function fixYAMLColumns()
221   {
222     foreach ($this->getTables() as $table => $columns)
223     {
224       $has_primary_key = false;
225       
226       foreach ($columns as $column => $attributes)
227       {
228         if ($attributes == null)
229         {
230           // conventions for null attributes
231           if ($column == 'created_at' || $column == 'updated_at')
232           {
233             // timestamp convention
234             $this->database[$table][$column]['type']= 'timestamp';
235           }
236
237           if ($column == 'id')
238           {
239             // primary key convention
240             $this->database[$table]['id'] = array(
241               'type'          => 'integer',
242               'required'      => true,
243               'primaryKey'    => true,
244               'autoincrement' => true
245             );
246             $has_primary_key = true;
247           }
248           
249           $pos = strpos($column, '_id');
250           if ($pos > 0 && $pos == strlen($column) - 3)
251           {
252             // foreign key convention
253             $foreign_table = $this->findTable(substr($column, 0, $pos));
254             if ($foreign_table)
255             {
256               $this->database[$table][$column] = array(
257                 'type'             => 'integer',
258                 'foreignTable'     => $foreign_table,
259                 'foreignReference' => 'id'
260               );
261             }
262             else
263             {
264               throw new sfException(sprintf('Unable to resolve foreign table for column "%s"', $column));
265             }
266           }
267           
268         }
269         else
270         {
271           if (!is_array($attributes))
272           {
273             // compact type given as single attribute
274             $this->database[$table][$column] = $this->getAttributesFromCompactType($attributes);
275           }
276           else
277           {
278             if (isset($attributes['type']))
279             {
280               // compact type given as value of the type attribute
281               $this->database[$table][$column] = array_merge($this->database[$table][$column], $this->getAttributesFromCompactType($attributes['type']));
282             }
283             if (isset($attributes['primaryKey']))
284             {
285               $has_primary_key = true;
286             }
287           }
288         }
289       }
290
291       if (!$has_primary_key)
292       {
293         // convention for tables without primary key
294         $this->database[$table]['id'] = array(
295           'type'          => 'integer',
296           'required'      => true,
297           'primaryKey'    => true,
298           'autoincrement' => true
299         );
300       }
301     }
302   }
303
304   protected function getAttributesFromCompactType($type)
305   {
306     preg_match('/varchar\(([\d]+)\)/', $type, $matches);
307     if (isset($matches[1]))
308     {
309       return array('type' => 'varchar', 'size' => $matches[1]);
310     }
311     else
312     {
313       return array('type' => $type);
314     }
315   }
316
317   protected function setIfNotSet(&$entry, $key, $value)
318   {
319     if (!isset($entry[$key]))
320     {
321       $entry[$key] = $value;
322     }
323   }
324
325   protected function findTable($table_name)
326   {
327     // find a table from a phpName or a name
328     $table_match = false;
329     foreach ($this->getTables() as $tb_name => $table)
330     {
331       if ((isset($table['_attributes']['phpName']) && $table['_attributes']['phpName'] == sfInflector::camelize($table_name)) || ($tb_name == $table_name))
332       {
333         $table_match = $tb_name;
334       }
335     }
336
337     return $table_match;
338   }
339
340   protected function getAttributesForColumn($tb_name, $col_name, $column)
341   {
342     $attributes_string = '';
343     if (is_array($column))
344     {
345       foreach ($column as $key => $value)
346       {
347         if (!in_array($key, array('foreignTable', 'foreignReference', 'onDelete', 'onUpdate', 'index', 'unique')))
348         {
349           $attributes_string .= " $key=\"".htmlspecialchars($this->getCorrectValueFor($key, $value), ENT_QUOTES, sfConfig::get('sf_charset'))."\"";
350         }
351       }
352       $attributes_string .= " />\n";
353     }
354     else
355     {
356       throw new sfException('Incorrect settings for column '.$col_name);
357     }
358
359     // conventions for foreign key attributes
360     if (is_array($column) && isset($column['foreignTable']))
361     {
362       $attributes_string .= "    <foreign-key foreignTable=\"$column[foreignTable]\"";
363       if (isset($column['onDelete']))
364       {
365         $attributes_string .= " onDelete=\"$column[onDelete]\"";
366       }
367       if (isset($column['onUpdate']))
368       {
369         $attributes_string .= " onUpdate=\"$column[onUpdate]\"";
370       }
371       $attributes_string .= ">\n";
372       $attributes_string .= "      <reference local=\"$col_name\" foreign=\"$column[foreignReference]\" />\n";
373       $attributes_string .= "    </foreign-key>\n";
374     }
375
376     // conventions for index and unique index attributes
377     if (is_array($column) && isset($column['index']))
378     {
379       if ($column['index'] === 'unique')
380       {
381         $attributes_string .= "    <unique name=\"${tb_name}_${col_name}_unique\">\n";
382         $attributes_string .= "      <unique-column name=\"$col_name\" />\n";
383         $attributes_string .= "    </unique>\n";
384       }
385       else
386       {
387         $attributes_string .= "    <index name=\"${tb_name}_${col_name}_index\">\n";
388         $attributes_string .= "      <index-column name=\"$col_name\" />\n";
389         $attributes_string .= "    </index>\n";
390       }
391     }
392
393     // conventions for sequence name attributes
394     // required for databases using sequences for auto-increment columns (e.g. PostgreSQL or Oracle)
395     if (is_array($column) && isset($column['sequence']))
396     {
397       $attributes_string .= "    <id-method-parameter value=\"$column[sequence]\" />\n";
398     }
399
400     return $attributes_string;
401   }
402
403   protected function getAttributesFor($tag)
404   {
405     if (!isset($tag['_attributes']))
406     {
407       return '';
408     }
409     $attributes = $tag['_attributes'];
410     $attributes_string = '';
411     foreach ($attributes as $key => $value)
412     {
413       $attributes_string .= ' '.$key.'="'.htmlspecialchars($this->getCorrectValueFor($key, $value), ENT_QUOTES, sfConfig::get('sf_charset')).'"';
414     }
415
416     return $attributes_string;
417   }
418
419   protected function getCorrectValueFor($key, $value)
420   {
421     $booleans = array('required', 'primaryKey', 'autoincrement', 'autoIncrement', 'noXsd', 'isI18N', 'isCulture');
422     if (in_array($key, $booleans))
423     {
424       return $value == 1 ? 'true' : 'false';
425     }
426     else
427     {
428       return is_null($value) ? 'null' : $value;
429     }
430   }
431
432   public function getTables()
433   {
434     return $this->getChildren($this->database);
435   }
436
437   public function getChildren($hash)
438   {
439     foreach ($hash as $key => $value)
440     {
441       // ignore special children (starting with _)
442       if ($key[0] == '_')
443       {
444         unset($hash[$key]);
445       }
446     }
447
448     return $hash;
449   }
450
451   public function loadXML($file)
452   {
453     $schema = simplexml_load_file($file);
454     $database = array();
455
456     // database
457     list($database_name, $database_attributes) = $this->getNameAndAttributes($schema->attributes());
458     if ($database_name)
459     {
460       $this->connection_name = $database_name;
461     }
462     else
463     {
464       throw new sfException('The database tag misses a name attribute');
465     }
466     if ($database_attributes)
467     {
468       $database['_attributes'] = $database_attributes;
469     }
470
471     // tables
472     foreach ($schema as $table)
473     {
474       list($table_name, $table_attributes) = $this->getNameAndAttributes($table->attributes());
475       if ($table_name)
476       {
477         $database[$table_name] = array();
478       }
479       else
480       {
481         throw new sfException('A table tag misses the name attribute');
482       }
483       if ($table_attributes)
484       {
485         $database[$table_name]['_attributes'] = $table_attributes;
486       }
487
488       // columns
489       foreach ($table->xpath('column') as $column)
490       {
491         list($column_name, $column_attributes) = $this->getNameAndAttributes($column->attributes());
492         if ($column_name)
493         {
494           $database[$table_name][$column_name] = $column_attributes;
495         }
496         else
497         {
498           throw new sfException('A column tag misses the name attribute');
499         }
500       }
501
502       // foreign-keys
503       $database[$table_name]['_foreignKeys'] = array();
504       foreach ($table->xpath('foreign-key') as $foreign_key)
505       {
506         $foreign_key_table = array();
507
508         // foreign key attributes
509         if (isset($foreign_key['foreignTable']))
510         {
511           $foreign_key_table['foreignTable'] = (string) $foreign_key['foreignTable'];
512         }
513         else
514         {
515           throw new sfException('A foreign key misses the foreignTable attribute');
516         }
517         if (isset($foreign_key['onDelete']))
518         {
519           $foreign_key_table['onDelete'] = (string) $foreign_key['onDelete'];
520         }
521         if (isset($foreign_key['onUpdate']))
522         {
523           $foreign_key_table['onUpdate'] = (string) $foreign_key['onUpdate'];
524         }
525
526         // foreign key references
527         $foreign_key_table['references'] = array();
528         foreach ($foreign_key->xpath('reference') as $reference)
529         {
530           $reference_attributes = array();
531           foreach ($reference->attributes() as $reference_attribute_name => $reference_attribute_value)
532           {
533             $reference_attributes[$reference_attribute_name] = strval($reference_attribute_value);
534           }
535           $foreign_key_table['references'][] = $reference_attributes;
536         }
537
538         if (isset($foreign_key['name']))
539         {
540           $database[$table_name]['_foreignKeys'][(string)$foreign_key['name']] = $foreign_key_table;
541         }
542         else
543         {
544           $database[$table_name]['_foreignKeys'][] = $foreign_key_table;
545         }
546
547       }
548       $this->removeEmptyKey($database[$table_name], '_foreignKeys');
549
550       // indexes
551       $database[$table_name]['_indexes'] = array();
552       foreach ($table->xpath('index') as $index)
553       {
554         $index_keys = array();
555         foreach ($index->xpath('index-column') as $index_key)
556         {
557           $index_keys[] = strval($index_key['name']);
558         }
559         $database[$table_name]['_indexes'][strval($index['name'])] = $index_keys;
560       }
561       $this->removeEmptyKey($database[$table_name], '_indexes');
562
563       // unique indexes
564       $database[$table_name]['_uniques'] = array();
565       foreach ($table->xpath('unique') as $index)
566       {
567         $unique_keys = array();
568         foreach ($index->xpath('unique-column') as $unique_key)
569         {
570           $unique_keys[] = strval($unique_key['name']);
571         }
572         $database[$table_name]['_uniques'][strval($index['name'])] = $unique_keys;
573       }
574       $this->removeEmptyKey($database[$table_name], '_uniques');
575     }
576     $this->database = $database;
577     
578     $this->fixXML();
579   }
580
581   public function fixXML()
582   {
583     $this->fixXMLForeignKeys();
584     $this->fixXMLIndexes();
585     // $this->fixXMLColumns();
586   }
587
588   protected function fixXMLForeignKeys()
589   {
590     foreach ($this->getTables() as $table => $columns)
591     {
592       if (isset($this->database[$table]['_foreignKeys']))
593       {
594         $foreign_keys = $this->database[$table]['_foreignKeys'];
595         foreach ($foreign_keys as $foreign_key_name => $foreign_key_attributes)
596         {
597           // Only single foreign keys can be simplified
598           if (count($foreign_key_attributes['references']) == 1)
599           {
600             $reference = $foreign_key_attributes['references'][0];
601
602             // set simple foreign key
603             $this->database[$table][$reference['local']]['foreignTable'] = $foreign_key_attributes['foreignTable'];
604             $this->database[$table][$reference['local']]['foreignReference'] = $reference['foreign'];
605             if (isset($foreign_key_attributes['onDelete']))
606             {
607               $this->database[$table][$reference['local']]['onDelete'] = $foreign_key_attributes['onDelete'];
608             }
609             if (isset($foreign_key_attributes['onUpdate']))
610             {
611               $this->database[$table][$reference['local']]['onUpdate'] = $foreign_key_attributes['onUpdate'];
612             }
613
614             // remove complex foreign key
615             unset($this->database[$table]['_foreignKeys'][$foreign_key_name]);
616           }
617
618           $this->removeEmptyKey($this->database[$table], '_foreignKeys');
619         }
620       }
621     }
622   }
623
624   protected function fixXMLIndexes()
625   {
626     foreach ($this->getTables() as $table => $columns)
627     {
628       if (isset($this->database[$table]['_indexes']))
629       {
630         $indexes = $this->database[$table]['_indexes'];
631         foreach ($indexes as $index => $references)
632         {
633           // Only single indexes can be simplified
634           if (count($references) == 1 && array_key_exists(substr($index, 0, strlen($index) - 6), $columns))
635           {
636             $reference = $references[0];
637
638             // set simple index
639             $this->database[$table][$reference]['index'] = 'true';
640
641             // remove complex index
642             unset($this->database[$table]['_indexes'][$index]);
643           }
644           
645           $this->removeEmptyKey($this->database[$table], '_indexes');
646         }
647       }
648       if (isset($this->database[$table]['_uniques']))
649       {
650         $uniques = $this->database[$table]['_uniques'];
651         foreach ($uniques as $index => $references)
652         {
653           // Only single unique indexes can be simplified
654           if (count($references) == 1 && array_key_exists(substr($index, 0, strlen($index) - 7), $columns))
655           {
656             $reference = $references[0];
657
658             // set simple index
659             $this->database[$table][$reference]['index'] = 'unique';
660
661             // remove complex unique index
662             unset($this->database[$table]['_uniques'][$index]);
663           }
664
665           $this->removeEmptyKey($this->database[$table], '_uniques');
666         }
667       }
668     }
669   }
670
671   protected function fixXMLColumns()
672   {
673     foreach ($this->getTables() as $table => $columns)
674     {
675       foreach ($columns as $column => $attributes)
676       {
677         if ($column == 'id' && !array_diff($attributes, array('type' => 'integer', 'required' => 'true', 'primaryKey' => 'true', 'autoincrement' => 'true')))
678         {
679           // simplify primary keys
680           $this->database[$table]['id'] = null;
681         }
682
683         if (($column == 'created_at') || ($column == 'updated_at') && !array_diff($attributes, array('type' => 'timestamp')))
684         {
685           // simplify timestamps
686           $this->database[$table][$column] = null;
687         }
688
689         $pos                 = strpos($column, '_id');
690         $has_fk_name         = $pos > 0 && $pos == strlen($column) - 3;
691         $is_foreign_key      = isset($attributes['type']) && $attributes['type'] == 'integer' && isset($attributes['foreignReference']) && $attributes['foreignReference'] == 'id';
692         $has_foreign_table   = isset($attributes['foreignTable']) && array_key_exists($attributes['foreignTable'], $this->getTables());
693         $has_other_attribute = isset($attributes['onDelete']);
694         if ($has_fk_name && $has_foreign_table && $is_foreign_key && !$has_other_attribute)
695         {
696           // simplify foreign key
697           $this->database[$table][$column] = null;
698         }
699       }
700     }
701   }
702
703   public function asYAML()
704   {
705     return sfYaml::dump(array($this->connection_name => $this->database));
706   }
707
708   protected function getNameAndAttributes($hash, $name_attribute = 'name')
709   {
710     // tag name
711     $name = '';
712     if (isset($hash[$name_attribute]))
713     {
714       $name = strval($hash[$name_attribute]);
715       unset($hash[$name_attribute]);
716     }
717
718     // tag attributes
719     $attributes = array();
720     foreach ($hash as $attribute => $value)
721     {
722       $attributes[$attribute] = strval($value);
723     }
724
725     return array($name, $attributes);
726   }
727
728   protected function removeEmptyKey(&$hash, $key)
729   {
730     if (isset($hash[$key]) && !$hash[$key])
731     {
732       unset($hash[$key]);
733     }
734   }
735 }
736
Note: See TracBrowser for help on using the browser.