Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
 
3
/*
4
* This file is part of the symfony package.
5
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
6
* (c) Francois Zaninotto <francois.zaninotto@symfony-project.com>
7
*
8
* For the full copyright and license information, please view the LICENSE
9
* file that was distributed with this source code.
10
*/
11
 
12
/**
13
 * Manages propel database schemas as YAML and XML.
14
 *
15
 * @package    symfony
16
 * @subpackage propel
17
 * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
18
 * @author     François Zaninotto <francois.zaninotto@symfony-project.com>
19
 * @version    SVN: $Id: sfPropelDatabaseSchema.class.php 24392 2009-11-25 18:35:39Z FabianLange $
20
 */
21
class sfPropelDatabaseSchema
22
{
23
  protected $connection_name = '';
24
  protected $database        = array();
25
 
26
  /**
27
   * Dumps schema as array
28
   *
29
   * @return array
30
   */
31
  public function asArray()
32
  {
33
    return array($this->connection_name => $this->database);
34
  }
35
 
36
  /**
37
   * Load schema from array
38
   *
39
   * @param array $schema_array
40
   */
41
  public function loadArray($schema_array)
42
  {
43
    if (is_array($schema_array) && !empty($schema_array))
44
    {
45
      $database = array();
46
      $connection_name = '';
47
 
48
      if (isset($schema_array['classes']))
49
      {
50
        // New schema syntax
51
        $schema_array = $this->convertNewToOldYaml($schema_array);
52
      }
53
 
54
      if (count($schema_array) > 1)
55
      {
56
        throw new sfException('A schema.yml must only contain 1 database entry.');
57
      }
58
 
59
      $tmp = array_keys($schema_array);
60
      $connection_name = array_shift($tmp);
61
 
62
      if ($connection_name)
63
      {
64
        $database = $schema_array[$connection_name];
65
      }
66
 
67
      $this->connection_name = $connection_name;
68
      $this->database = $database;
69
 
70
      $this->fixYAMLDatabase();
71
      $this->fixYAMLI18n();
72
      $this->fixYAMLColumns();
73
    }
74
  }
75
 
76
  /**
77
   * Load schema from YAML file
78
   *
79
   * @param string $file
80
   */
81
  public function loadYAML($file)
82
  {
83
    $schema_array = sfYaml::load($file);
84
 
85
    if (!is_array($schema_array))
86
    {
87
      return; // No defined schema here, skipping
88
    }
89
 
90
    if (!isset($schema_array['classes']))
91
    {
92
      // Old schema syntax: we convert it
93
      $schema_array = $this->convertOldToNewYaml($schema_array);
94
    }
95
 
96
    $this->loadArray($schema_array);
97
  }
98
 
99
  /**
100
   * Converts old yaml format schema to new yaml schema
101
   *
102
   * @param array $schema
103
   *
104
   * @return array
105
   */
106
  public function convertOldToNewYaml($schema)
107
  {
108
    if (is_array($schema) && !empty($schema))
109
    {
110
      $new_schema = array();
111
 
112
      $tmp = array_keys($schema);
113
      $connection_name = array_shift($tmp);
114
 
115
      if (!empty($connection_name) && isset($schema[$connection_name]))
116
      {
117
        $new_schema['connection'] = $connection_name;
118
 
119
        $classes = array();
120
        foreach($schema[$connection_name] as $table => $table_params)
121
        {
122
          if ($table == '_attributes')
123
          {
124
            // Database attributes
125
            $new_schema = array_merge($new_schema, $table_params);
126
          }
127
          else if ('_propel_behaviors' == $table)
128
          {
129
            // Database behaviors
130
            $new_schema['propel_behaviors'] = $table_params;
131
          }
132
          else
133
          {
134
            // Table
135
            $phpName = sfInflector::camelize($table);
136
            if (isset($table_params['_attributes']))
137
            {
138
              $table_attributes = $table_params['_attributes'];
139
              unset($table_params['_attributes']);
140
              if (isset($table_attributes['phpName']))
141
              {
142
                $phpName = $table_attributes['phpName'];
143
                unset($table_attributes['phpName']);
144
              }
145
            }
146
            else
147
            {
148
              $table_attributes = array();
149
            }
150
            $classes[$phpName] = $table_attributes;
151
            $classes[$phpName]['tableName'] = $table;
152
            $classes[$phpName]['columns'] = array();
153
            foreach($table_params as $column => $column_params)
154
            {
155
              switch($column)
156
              {
157
                case '_behaviors':
158
                  $classes[$phpName]['behaviors'] = $column_params;
159
                  break;
160
                case '_propel_behaviors':
161
                  $classes[$phpName]['propel_behaviors'] = $column_params;
162
                  break;
163
                case '_inheritance':
164
                  $classes[$phpName]['inheritance'] = $column_params;
165
                  break;
166
                case '_nestedSet':
167
                  $classes[$phpName]['nestedSet'] = $column_params;
168
                  break;
169
                case '_foreignKeys':
170
                  $classes[$phpName]['foreignKeys'] = $column_params;
171
                  break;
172
                case '_indexes':
173
                  $classes[$phpName]['indexes'] = $column_params;
174
                  break;
175
                case '_uniques':
176
                  $classes[$phpName]['uniques'] = $column_params;
177
                  break;
178
                default:
179
                  $classes[$phpName]['columns'][$column] = $column_params;
180
              }
181
            }
182
          }
183
        }
184
 
185
        $new_schema['classes'] = $classes;
186
 
187
      }
188
 
189
      return $new_schema;
190
    }
191
  }
192
 
193
  /**
194
   * Converts new yaml schema format to old yaml schema
195
   *
196
   * @param array $schema
197
   *
198
   * @return array
199
   */
200
  public function convertNewToOldYaml($schema)
201
  {
202
    if (isset($schema['connection']))
203
    {
204
      $connection_name = $schema['connection'];
205
      unset($schema['connection']);
206
    }
207
    else
208
    {
209
      $connection_name = 'propel';
210
    }
211
 
212
    $database = array();
213
 
214
    // Tables
215
    if (isset($schema['classes']))
216
    {
217
      $tables = array();
218
      foreach ($schema['classes'] as $className => $classParams)
219
      {
220
        $tableParams = array();
221
 
222
        // Columns
223
        if (isset($classParams['columns']))
224
        {
225
          $tableParams = array_merge($classParams['columns'], $tableParams);
226
          unset($classParams['columns']);
227
        }
228
 
229
        // Indexes and foreign keys
230
        if (isset($classParams['indexes']))
231
        {
232
          $tableParams['_indexes'] = $classParams['indexes'];
233
          unset($classParams['indexes']);
234
        }
235
        if (isset($classParams['uniques']))
236
        {
237
          $tableParams['_uniques'] = $classParams['uniques'];
238
          unset($classParams['uniques']);
239
        }
240
        if (isset($classParams['foreignKeys']))
241
        {
242
          $tableParams['_foreignKeys'] = $classParams['foreignKeys'];
243
          unset($classParams['foreignKeys']);
244
        }
245
 
246
        // Behaviors
247
        if (isset($classParams['behaviors']))
248
        {
249
          $tableParams['_behaviors'] = $classParams['behaviors'];
250
          unset($classParams['behaviors']);
251
        }
252
        if (isset($classParams['propel_behaviors']))
253
        {
254
          $tableParams['_propel_behaviors'] = $classParams['propel_behaviors'];
255
          unset($classParams['propel_behaviors']);
256
        }
257
 
258
        // Inheritance
259
        if (isset($classParams['inheritance']))
260
        {
261
          $tableParams['_inheritance'] = $classParams['inheritance'];
262
          unset($classParams['inheritance']);
263
        }
264
 
265
        // Nested sets
266
        if (isset($classParams['nestedSet']))
267
        {
268
          $tableParams['_nestedSet'] = $classParams['nestedSet'];
269
          unset($classParams['nestedSet']);
270
        }
271
 
272
        // Table attributes
273
        $tableAttributes = array();
274
        if (isset($classParams['tableName']))
275
        {
276
          $tableName = $classParams['tableName'];
277
          unset($classParams['tableName']);
278
        }
279
        else
280
        {
281
          $tableName = sfInflector::underscore($className);
282
        }
283
 
284
        if (sfInflector::camelize($tableName) != $className)
285
        {
286
          $tableAttributes['phpName'] = $className;
287
        }
288
 
289
        if ($tableAttributes || $classParams)
290
        {
291
          $tableParams['_attributes'] = array_merge($tableAttributes, $classParams);
292
        }
293
 
294
        $tables[$tableName] = $tableParams;
295
      }
296
      $database = array_merge($database, $tables);
297
      unset($schema['classes']);
298
    }
299
 
300
    // Database behaviors
301
    if (isset($schema['propel_behaviors']))
302
    {
303
      $database['_propel_behaviors'] = $schema['propel_behaviors'];
304
      unset($schema['propel_behaviors']);
305
    }
306
 
307
    // Database attributes
308
    if ($schema)
309
    {
310
      $database['_attributes'] = $schema;
311
    }
312
 
313
    return array($connection_name => $database);
314
  }
315
 
316
  /**
317
   * Dumps schema as propel xml
318
   *
319
   * @return unknown
320
   */
321
  public function asXML()
322
  {
323
    $xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
324
 
325
    $xml .= "<database name=\"$this->connection_name\"".$this->getAttributesFor($this->database).">\n";
326
 
327
    if (isset($this->database['_propel_behaviors']))
328
    {
329
      foreach ($this->database['_propel_behaviors'] as $name => $parameters)
330
      {
331
        $xml .= "\n  <behavior name=\"$name\"";
332
        if (is_array($parameters) && count($parameters))
333
        {
334
          $xml .= ">\n";
335
          foreach ($parameters as $key => $value)
336
          {
337
            $xml .= "    <parameter name=\"$key\" value=\"{$this->fixXMLBoolean($value)}\"/>\n";
338
          }
339
          $xml .= "  </behavior>\n";
340
        }
341
        else
342
        {
343
          $xml .= "/>\n";
344
        }
345
      }
346
    }
347
 
348
    // tables
349
    foreach ($this->getChildren($this->database) as $tb_name => $table)
350
    {
351
      // capture nested set config for this table
352
      if ($isNestedSet = isset($table['_nestedSet']))
353
      {
354
        $treeConfig = $table['_nestedSet'];
355
        if (
356
          !isset($treeConfig['left'])
357
          ||
358
          !isset($treeConfig['right'])
359
          ||
360
          !isset($table[$treeConfig['left']])
361
          ||
362
          !isset($table[$treeConfig['right']])
363
          ||
364
          (isset($treeConfig['scope']) && !isset($table[$treeConfig['scope']]))
365
        )
366
        {
367
          throw new sfException(sprintf('Incorrect NestedSet configuration for "%s" table.', $tb_name));
368
        }
369
      }
370
 
371
      $xml .= "\n  <table name=\"$tb_name\"".$this->getAttributesFor($table);
372
      if ($isNestedSet)
373
      {
374
        $xml .= ' treeMode="NestedSet"';
375
      }
376
      if (isset($table['_behaviors']))
377
      {
378
        $xml .= sprintf(" behaviors=\"%s\"", htmlspecialchars(serialize($table['_behaviors'])), ENT_QUOTES, sfConfig::get('sf_charset'));
379
      }
380
      $xml .= ">\n";
381
 
382
      // behaviors
383
      if (isset($table['_propel_behaviors']))
384
      {
385
        foreach ($table['_propel_behaviors'] as $behavior_name => $parameters)
386
        {
387
          if ($parameters)
388
          {
389
            $xml .= "    <behavior name=\"$behavior_name\">\n";
390
            foreach ($parameters as $param_name => $param_value)
391
            {
392
              $xml .= "      <parameter name=\"$param_name\" value=\"{$this->fixXMLBoolean($param_value)}\" />\n";
393
            }
394
            $xml .= "    </behavior>\n";
395
          }
396
          else
397
          {
398
            $xml .= "    <behavior name=\"$behavior_name\" />\n";
399
          }
400
        }
401
      }
402
 
403
      // columns
404
      foreach ($this->getChildren($table) as $col_name => $column)
405
      {
406
        // inheritance
407
        if (
408
          isset($table['_inheritance'])
409
          &&
410
          isset($table['_inheritance']['column'])
411
          &&
412
          $col_name == $table['_inheritance']['column']
413
          &&
414
          isset($table['_inheritance']['classes'])
415
          &&
416
          is_array($table['_inheritance']['classes'])
417
        )
418
        {
419
          $column['inheritance'] = $table['_inheritance']['classes'];
420
          unset($table['_inheritance']);
421
        }
422
 
423
        // add nested set attributes to this column
424
        if ($isNestedSet && in_array($col_name, $treeConfig))
425
        {
426
          if ($col_name == $treeConfig['left'])
427
          {
428
            $column['nestedSetLeftKey'] = 'true';
429
          }
430
          elseif ($col_name == $treeConfig['right'])
431
          {
432
            $column['nestedSetRightKey'] = 'true';
433
          }
434
          elseif (isset($treeConfig['scope']) && $col_name == $treeConfig['scope'])
435
          {
436
            $column['treeScopeKey'] = 'true';
437
          }
438
        }
439
 
440
        $xml .= "    <column name=\"$col_name\"".$this->getAttributesForColumn($tb_name, $col_name, $column);
441
      }
442
 
443
      // indexes
444
      if (isset($table['_indexes']))
445
      {
446
        foreach ($table['_indexes'] as $index_name => $index)
447
        {
448
          $xml .= "    <index name=\"$index_name\">\n";
449
          foreach ($index as $index_column)
450
          {
451
            preg_match('/^(.+?)\(([\d]+)\)$/', $index_column, $matches);
452
            if (isset($matches[2]))
453
            {
454
              $xml .= "      <index-column name=\"{$matches[1]}\" size=\"{$matches[2]}\" />\n";
455
            }
456
            else
457
            {
458
              $xml .= "      <index-column name=\"$index_column\" />\n";
459
            }
460
          }
461
          $xml .= "    </index>\n";
462
        }
463
      }
464
 
465
      // uniques
466
      if (isset($table['_uniques']))
467
      {
468
        foreach ($table['_uniques'] as $unique_name => $index)
469
        {
470
          $xml .= "    <unique name=\"$unique_name\">\n";
471
          foreach ($index as $unique_column)
472
          {
473
            preg_match('/^(.+?)\(([\d]+)\)$/', $unique_column, $matches);
474
            if (isset($matches[2]))
475
            {
476
              $xml .= "      <unique-column name=\"{$matches[1]}\" size=\"{$matches[2]}\" />\n";
477
            }
478
            else
479
            {
480
              $xml .= "      <unique-column name=\"$unique_column\" />\n";
481
            }
482
          }
483
          $xml .= "    </unique>\n";
484
        }
485
      }
486
 
487
      // foreign-keys
488
      if (isset($table['_foreignKeys']))
489
      {
490
        foreach ($table['_foreignKeys'] as $fkey_name => $fkey)
491
        {
492
          if (!isset($fkey['foreignTable']))
493
          {
494
            if (
495
              !isset($fkey['foreignClass'])
496
              ||
497
              !$fkey['foreignTable'] = $this->findTable($fkey['foreignClass'])
498
            )
499
            {
500
              throw new sfException(sprintf('Unable to resolve foreign table for foreign key "%s": %s', $fkey_name, var_export($fkey, true)));
501
            }
502
          }
503
          unset($fkey['foreignClass']);
504
 
505
          $xml .= '    <foreign-key';
506
 
507
          // foreign key name
508
          if (!is_numeric($fkey_name))
509
          {
510
            $xml .= " name=\"$fkey_name\"";
511
          }
512
 
513
          // other attributes
514
          foreach ($fkey as $attribute_name => $attribute_value)
515
          {
516
            if (is_string($attribute_value))
517
            {
518
              $xml .= " $attribute_name=\"$attribute_value\"";
519
            }
520
          }
521
 
522
          $xml .= ">\n";
523
 
524
          // references
525
          if (isset($fkey['references']))
526
          {
527
            foreach ($fkey['references'] as $reference)
528
            {
529
              $xml .= "      <reference local=\"{$reference['local']}\" foreign=\"{$reference['foreign']}\" />\n";
530
            }
531
          }
532
          $xml .= "    </foreign-key>\n";
533
        }
534
      }
535
 
536
      $xml .= "  </table>\n";
537
    }
538
    $xml .= "\n</database>\n";
539
 
540
    return $xml;
541
  }
542
 
543
  /**
544
   * Fixes databases in yaml shorthand schema
545
   *
546
   */
547
  protected function fixYAMLDatabase()
548
  {
549
    if (!isset($this->database['_attributes']))
550
    {
551
      $this->database['_attributes'] = array();
552
    }
553
 
554
    // conventions for database attributes
555
    $this->setIfNotSet($this->database['_attributes'], 'defaultIdMethod', 'native');
556
    $this->setIfNotSet($this->database['_attributes'], 'package', 'lib.model');
557
  }
558
 
559
  /**
560
   * Fixes i18n tables in yaml shorthand schema
561
   *
562
   */
563
  protected function fixYAMLI18n()
564
  {
565
    foreach ($this->getTables() as $i18n_table => $columns)
566
    {
567
      $pos = strpos($i18n_table, '_i18n');
568
 
569
      $has_primary_key = false;
570
      foreach ($columns as $column => $attributes)
571
      {
572
        if (is_array($attributes) && array_key_exists('primaryKey', $attributes))
573
        {
574
          $has_primary_key = true;
575
        }
576
      }
577
 
578
      if ($pos > 0 && $pos == strlen($i18n_table) - 5 && !$has_primary_key)
579
      {
580
        // i18n table without primary key
581
        $main_table = $this->findTable(substr($i18n_table, 0, $pos));
582
 
583
        if ($main_table)
584
        {
585
          // set i18n attributes for main table
586
          $this->setIfNotSet($this->database[$main_table]['_attributes'], 'isI18N', 1);
587
          $this->setIfNotSet($this->database[$main_table]['_attributes'], 'i18nTable', $i18n_table);
588
 
589
          // set id and culture columns for i18n table
590
          $this->setIfNotSet($this->database[$i18n_table], 'id', array(
591
            'type'             => 'integer',
592
            'required'         => true,
593
            'primaryKey'       => true,
594
            'foreignTable'     => $main_table,
595
            'foreignReference' => 'id',
596
            'onDelete'         => 'cascade',
597
          ));
598
          $this->setIfNotSet($this->database[$i18n_table], 'culture', array(
599
            'isCulture'  => true,
600
            'type'       => 'varchar',
601
            'size'       => '7',
602
            'required'   => true,
603
            'primaryKey' => true,
604
          ));
605
        }
606
        else
607
        {
608
          throw new sfException(sprintf('Missing main table for internationalized table "%s".', $i18n_table));
609
        }
610
      }
611
    }
612
  }
613
 
614
  /**
615
   * Fixes columns in yaml shorthand schema
616
   *
617
   */
618
  protected function fixYAMLColumns()
619
  {
620
    foreach ($this->getTables() as $table => $columns)
621
    {
622
      $has_primary_key = false;
623
 
624
      foreach ($columns as $column => $attributes)
625
      {
626
        if ($attributes == null)
627
        {
628
          // conventions for null attributes
629
          if ($column == 'created_at' || $column == 'updated_at')
630
          {
631
            // timestamp convention
632
            $this->database[$table][$column]['type']= 'timestamp';
633
          }
634
 
635
          if ($column == 'id')
636
          {
637
            // primary key convention
638
            $this->database[$table]['id'] = array(
639
            'type'          => 'integer',
640
            'required'      => true,
641
            'primaryKey'    => true,
642
            'autoIncrement' => true
643
            );
644
            $has_primary_key = true;
645
          }
646
 
647
          $pos = strpos($column, '_id');
648
          if ($pos > 0 && $pos == strlen($column) - 3)
649
          {
650
            // foreign key convention
651
            $foreign_table = $this->findTable(substr($column, 0, $pos));
652
            if ($foreign_table)
653
            {
654
              $this->database[$table][$column] = array(
655
                'type'             => 'integer',
656
                'foreignTable'     => $foreign_table,
657
                'foreignReference' => 'id',
658
              );
659
            }
660
            else
661
            {
662
              throw new sfException(sprintf('Unable to resolve foreign table for column "%s".', $column));
663
            }
664
          }
665
 
666
        }
667
        else
668
        {
669
          if (!is_array($attributes))
670
          {
671
            // compact type given as single attribute
672
            $this->database[$table][$column] = $this->getAttributesFromCompactType($attributes);
673
          }
674
          else
675
          {
676
            if (isset($attributes['type']))
677
            {
678
              // compact type given as value of the type attribute
679
              $this->database[$table][$column] = array_merge($this->database[$table][$column], $this->getAttributesFromCompactType($attributes['type']));
680
            }
681
            if (isset($attributes['primaryKey']))
682
            {
683
              $has_primary_key = true;
684
            }
685
          }
686
        }
687
      }
688
 
689
      if (!$has_primary_key)
690
      {
691
        // convention for tables without primary key
692
        $this->database[$table]['id'] = array(
693
        'type'          => 'integer',
694
        'required'      => true,
695
        'primaryKey'    => true,
696
        'autoIncrement' => true
697
        );
698
      }
699
    }
700
  }
701
 
702
  /**
703
   * Returns attributes for compact type
704
   *
705
   * @param string $type
706
   *
707
   * @return array The attributes for type
708
   */
709
  protected function getAttributesFromCompactType($type)
710
  {
711
    preg_match('/varchar\(([\d]+)\)/', $type, $matches);
712
    if (isset($matches[1]))
713
    {
714
      return array('type' => 'varchar', 'size' => $matches[1]);
715
    }
716
    else
717
    {
718
      return array('type' => $type);
719
    }
720
  }
721
 
722
  /**
723
   * Sets entry if not set
724
   *
725
   * @param string $entry
726
   * @param string $key
727
   * @param string $value
728
   */
729
  protected function setIfNotSet(&$entry, $key, $value)
730
  {
731
    if (!isset($entry[$key]))
732
    {
733
      $entry[$key] = $value;
734
    }
735
  }
736
 
737
  /**
738
   * Find table by name
739
   *
740
   * @param string $table_name
741
   *
742
   * @return array
743
   */
744
  protected function findTable($table_name)
745
  {
746
    // find a table from a phpName or a name
747
    $table_match = false;
748
    foreach ($this->getTables() as $tb_name => $table)
749
    {
750
      if (
751
      ($tb_name == $table_name)
752
      || (isset($table['_attributes']['phpName']) &&
753
      (
754
      $table['_attributes']['phpName'] == sfInflector::camelize($table_name)
755
      || $table['_attributes']['phpName'] == $table_name
756
      )
757
      || (sfInflector::underscore($table_name) == $tb_name))
758
      )
759
      {
760
        $table_match = $tb_name;
761
        break;
762
      }
763
    }
764
 
765
    return $table_match;
766
  }
767
 
768
  /**
769
   * Get attributes for column
770
   *
771
   * @param string $tb_name
772
   * @param string $col_name
773
   * @param string $column
774
   *
775
   * @return array
776
   */
777
  protected function getAttributesForColumn($tb_name, $col_name, $column)
778
  {
779
    $attributes_string = '';
780
    if (is_array($column))
781
    {
782
      foreach ($column as $key => $value)
783
      {
784
        if (!in_array($key, array('foreignClass', 'foreignTable', 'foreignReference', 'fkPhpName', 'fkRefPhpName', 'onDelete', 'onUpdate', 'index', 'unique', 'sequence', 'inheritance')))
785
        {
786
          $attributes_string .= " $key=\"".htmlspecialchars($this->getCorrectValueFor($key, $value), ENT_QUOTES, sfConfig::get('sf_charset'))."\"";
787
        }
788
      }
789
      if (isset($column['inheritance']))
790
      {
791
        $attributes_string .= ' inheritance="single">'."\n";
792
 
793
        $extended_package = isset($this->database[$tb_name]['_attributes']['package']) ? $this->database[$tb_name]['_attributes']['package'] : $this->database['_attributes']['package'];
794
        $extended_class   = isset($this->database[$tb_name]['_attributes']['phpName']) ? $this->database[$tb_name]['_attributes']['phpName'] : sfInflector::camelize($tb_name);
795
 
796
        foreach ($column['inheritance'] as $key => $class)
797
        {
798
          // each inheritance class can have its own package
799
          $package = null;
800
          if (is_array($class))
801
          {
802
            $package = isset($class['package']) ? $class['package'] : null;
803
            $class   = $class['phpName'];
804
          }
805
 
806
          $attributes_string .= vsprintf('      <inheritance extends="%s.%s" key="%s" class="%s"%s />', array(
807
            $extended_package,
808
            $extended_class,
809
            $key,
810
            $class,
811
            $package ? " package=\"$package\"" : '',
812
          ))."\n";
813
        }
814
 
815
        $attributes_string .= '    </column>'."\n";
816
      }
817
      else
818
      {
819
        $attributes_string .= " />\n";
820
      }
821
    }
822
    else
823
    {
824
      throw new sfException(sprintf('Incorrect settings for column "%s" of table "%s".', $col_name, $tb_name));
825
    }
826
 
827
    // conventions for foreign key attributes
828
    if (is_array($column) && (isset($column['foreignTable']) || isset($column['foreignClass'])))
829
    {
830
      if (isset($column['foreignTable']))
831
      {
832
        $attributes_string .= "    <foreign-key foreignTable=\"{$column['foreignTable']}\"";
833
      }
834
      else
835
      {
836
        $foreignTable = $this->findTable($column['foreignClass']);
837
        if (!$foreignTable)
838
        {
839
          // Let's assume that the class given is from another schema
840
          // We have no access to the other schema's phpNames
841
          // So our last guess is to try to underscore the class name
842
          $foreignTable = sfInflector::underscore($column['foreignClass']);
843
        }
844
        $attributes_string .= "    <foreign-key foreignTable=\"".$foreignTable."\"";
845
      }
846
 
847
      if (isset($column['onDelete']))
848
      {
849
        $attributes_string .= " onDelete=\"{$column['onDelete']}\"";
850
      }
851
      if (isset($column['onUpdate']))
852
      {
853
        $attributes_string .= " onUpdate=\"{$column['onUpdate']}\"";
854
      }
855
      if (isset($column['fkPhpName']))
856
      {
857
        $attributes_string .= " phpName=\"{$column['fkPhpName']}\"";
858
      }
859
      if (isset($column['fkRefPhpName']))
860
      {
861
        $attributes_string .= " refPhpName=\"{$column['fkRefPhpName']}\"";
862
      }
863
      $attributes_string .= ">\n";
864
      $attributes_string .= "      <reference local=\"$col_name\" foreign=\"{$column['foreignReference']}\" />\n";
865
      $attributes_string .= "    </foreign-key>\n";
866
    }
867
 
868
    // conventions for index and unique index attributes
869
    if (is_array($column) && isset($column['index']))
870
    {
871
      if ($column['index'] === 'unique')
872
      {
873
        $attributes_string .= "    <unique>\n";
874
        $attributes_string .= "      <unique-column name=\"$col_name\" />\n";
875
        $attributes_string .= "    </unique>\n";
876
      }
877
      else
878
      {
879
        $attributes_string .= "    <index>\n";
880
        $attributes_string .= "      <index-column name=\"$col_name\" />\n";
881
        $attributes_string .= "    </index>\n";
882
      }
883
    }
884
 
885
    // conventions for sequence name attributes
886
    // required for databases using sequences for auto-increment columns (e.g. PostgreSQL or Oracle)
887
    if (is_array($column) && isset($column['sequence']))
888
    {
889
      $attributes_string .= "    <id-method-parameter value=\"{$column['sequence']}\" />\n";
890
    }
891
 
892
    return $attributes_string;
893
  }
894
 
895
  /**
896
   * Returns attributes for a tag
897
   *
898
   * @param string $tag
899
   *
900
   * @return string
901
   */
902
  protected function getAttributesFor($tag)
903
  {
904
    if (!isset($tag['_attributes']))
905
    {
906
      return '';
907
    }
908
    $attributes = $tag['_attributes'];
909
    $attributes_string = '';
910
    foreach ($attributes as $key => $value)
911
    {
912
      $attributes_string .= ' '.$key.'="'.htmlspecialchars($this->getCorrectValueFor($key, $value), ENT_QUOTES, sfConfig::get('sf_charset')).'"';
913
    }
914
 
915
    return $attributes_string;
916
  }
917
 
918
  protected function getCorrectValueFor($key, $value)
919
  {
920
    $booleans = array('required', 'primaryKey', 'autoincrement', 'autoIncrement', 'isI18N', 'isCulture');
921
    if (in_array($key, $booleans))
922
    {
923
      return $value == 1 ? 'true' : 'false';
924
    }
925
    else
926
    {
927
      return null === $value ? 'null' : $value;
928
    }
929
  }
930
 
931
  /**
932
   * Returns the tables for database
933
   *
934
   * @return array
935
   */
936
  public function getTables()
937
  {
938
    return $this->getChildren($this->database);
939
  }
940
 
941
  /**
942
   * Returns the children for a given hash
943
   *
944
   * @param string $hash
945
   *
946
   * @return array
947
   */
948
  public function getChildren($hash)
949
  {
950
    foreach ($hash as $key => $value)
951
    {
952
      // ignore special children (starting with _)
953
      if ($key[0] == '_')
954
      {
955
        unset($hash[$key]);
956
      }
957
    }
958
 
959
    return $hash;
960
  }
961
 
962
  /**
963
   * Loads propel xml schema
964
   *
965
   * @param string $file The path to the propel xml schema
966
   */
967
  public function loadXML($file)
968
  {
969
    $schema = simplexml_load_file($file);
970
    $database = array();
971
 
972
    // database
973
    list($database_name, $database_attributes) = $this->getNameAndAttributes($schema->attributes());
974
    if ($database_name)
975
    {
976
      $this->connection_name = $database_name;
977
    }
978
    else
979
    {
980
      throw new sfException('The database tag misses a name attribute.');
981
    }
982
    if ($database_attributes)
983
    {
984
      $database['_attributes'] = $database_attributes;
985
    }
986
 
987
    // tables
988
    foreach ($schema as $table)
989
    {
990
      list($table_name, $table_attributes) = $this->getNameAndAttributes($table->attributes());
991
      if ($table_name)
992
      {
993
        $database[$table_name] = array();
994
      }
995
      else
996
      {
997
        throw new sfException('A table tag misses the name attribute.');
998
      }
999
      if ($table_attributes)
1000
      {
1001
        $database[$table_name]['_attributes'] = $table_attributes;
1002
      }
1003
 
1004
      // columns
1005
      foreach ($table->xpath('column') as $column)
1006
      {
1007
        list($column_name, $column_attributes) = $this->getNameAndAttributes($column->attributes());
1008
        if ($column_name)
1009
        {
1010
          $database[$table_name][$column_name] = $column_attributes;
1011
        }
1012
        else
1013
        {
1014
          throw new sfException('A column tag misses the name attribute.');
1015
        }
1016
      }
1017
 
1018
      // foreign-keys
1019
      $database[$table_name]['_foreignKeys'] = array();
1020
      foreach ($table->xpath('foreign-key') as $foreign_key)
1021
      {
1022
        $foreign_key_table = array();
1023
 
1024
        // foreign key attributes
1025
        if (isset($foreign_key['foreignTable']))
1026
        {
1027
          $foreign_key_table['foreignTable'] = (string) $foreign_key['foreignTable'];
1028
        }
1029
        else
1030
        {
1031
          throw new sfException('A foreign key misses the foreignTable attribute.');
1032
        }
1033
        if (isset($foreign_key['onDelete']))
1034
        {
1035
          $foreign_key_table['onDelete'] = (string) $foreign_key['onDelete'];
1036
        }
1037
        if (isset($foreign_key['onUpdate']))
1038
        {
1039
          $foreign_key_table['onUpdate'] = (string) $foreign_key['onUpdate'];
1040
        }
1041
 
1042
        // foreign key references
1043
        $foreign_key_table['references'] = array();
1044
        foreach ($foreign_key->xpath('reference') as $reference)
1045
        {
1046
          $reference_attributes = array();
1047
          foreach ($reference->attributes() as $reference_attribute_name => $reference_attribute_value)
1048
          {
1049
            $reference_attributes[$reference_attribute_name] = strval($reference_attribute_value);
1050
          }
1051
          $foreign_key_table['references'][] = $reference_attributes;
1052
        }
1053
 
1054
        if (isset($foreign_key['name']))
1055
        {
1056
          $database[$table_name]['_foreignKeys'][(string)$foreign_key['name']] = $foreign_key_table;
1057
        }
1058
        else
1059
        {
1060
          $database[$table_name]['_foreignKeys'][] = $foreign_key_table;
1061
        }
1062
 
1063
      }
1064
      $this->removeEmptyKey($database[$table_name], '_foreignKeys');
1065
 
1066
      // indexes
1067
      $database[$table_name]['_indexes'] = array();
1068
      foreach ($table->xpath('index') as $index)
1069
      {
1070
        $index_keys = array();
1071
        foreach ($index->xpath('index-column') as $index_key)
1072
        {
1073
          $index_keys[] = strval($index_key['name']);
1074
        }
1075
        $database[$table_name]['_indexes'][strval($index['name'])] = $index_keys;
1076
      }
1077
      $this->removeEmptyKey($database[$table_name], '_indexes');
1078
 
1079
      // unique indexes
1080
      $database[$table_name]['_uniques'] = array();
1081
      foreach ($table->xpath('unique') as $index)
1082
      {
1083
        $unique_keys = array();
1084
        foreach ($index->xpath('unique-column') as $unique_key)
1085
        {
1086
          $unique_keys[] = strval($unique_key['name']);
1087
        }
1088
        $database[$table_name]['_uniques'][strval($index['name'])] = $unique_keys;
1089
      }
1090
      $this->removeEmptyKey($database[$table_name], '_uniques');
1091
    }
1092
    $this->database = $database;
1093
 
1094
    $this->fixXML();
1095
  }
1096
 
1097
  /**
1098
   * Fixed xml for using short hand syntax
1099
   *
1100
   * @return void
1101
   */
1102
  public function fixXML()
1103
  {
1104
    $this->fixXMLForeignKeys();
1105
    $this->fixXMLIndexes();
1106
    // $this->fixXMLColumns();
1107
  }
1108
 
1109
  /**
1110
   * Fixes xml foreign keys
1111
   *
1112
   * @return void
1113
   */
1114
  protected function fixXMLForeignKeys()
1115
  {
1116
    foreach ($this->getTables() as $table => $columns)
1117
    {
1118
      if (isset($this->database[$table]['_foreignKeys']))
1119
      {
1120
        $foreign_keys = $this->database[$table]['_foreignKeys'];
1121
        foreach ($foreign_keys as $foreign_key_name => $foreign_key_attributes)
1122
        {
1123
          // Only single foreign keys can be simplified
1124
          if (count($foreign_key_attributes['references']) == 1)
1125
          {
1126
            $reference = $foreign_key_attributes['references'][0];
1127
 
1128
            // set simple foreign key
1129
            $this->database[$table][$reference['local']]['foreignTable'] = $foreign_key_attributes['foreignTable'];
1130
            $this->database[$table][$reference['local']]['foreignReference'] = $reference['foreign'];
1131
            if (isset($foreign_key_attributes['onDelete']))
1132
            {
1133
              $this->database[$table][$reference['local']]['onDelete'] = $foreign_key_attributes['onDelete'];
1134
            }
1135
            if (isset($foreign_key_attributes['onUpdate']))
1136
            {
1137
              $this->database[$table][$reference['local']]['onUpdate'] = $foreign_key_attributes['onUpdate'];
1138
            }
1139
 
1140
            // remove complex foreign key
1141
            unset($this->database[$table]['_foreignKeys'][$foreign_key_name]);
1142
          }
1143
 
1144
          $this->removeEmptyKey($this->database[$table], '_foreignKeys');
1145
        }
1146
      }
1147
    }
1148
  }
1149
 
1150
  /**
1151
   * Fixes xml indices
1152
   *
1153
   * @return void
1154
   */
1155
  protected function fixXMLIndexes()
1156
  {
1157
    foreach ($this->getTables() as $table => $columns)
1158
    {
1159
      if (isset($this->database[$table]['_indexes']))
1160
      {
1161
        $indexes = $this->database[$table]['_indexes'];
1162
        foreach ($indexes as $index => $references)
1163
        {
1164
          // Only single indexes can be simplified
1165
          if (count($references) == 1 && false !== substr($index, 0, strlen($index) - 6) && array_key_exists(substr($index, 0, strlen($index) - 6), $columns))
1166
          {
1167
            $reference = $references[0];
1168
 
1169
            // set simple index
1170
            $this->database[$table][$reference]['index'] = 'true';
1171
 
1172
            // remove complex index
1173
            unset($this->database[$table]['_indexes'][$index]);
1174
          }
1175
 
1176
          $this->removeEmptyKey($this->database[$table], '_indexes');
1177
        }
1178
      }
1179
      if (isset($this->database[$table]['_uniques']))
1180
      {
1181
        $uniques = $this->database[$table]['_uniques'];
1182
        foreach ($uniques as $index => $references)
1183
        {
1184
          // Only single unique indexes can be simplified
1185
          if (count($references) == 1 && false !== substr($index, 0, strlen($index) - 7) && array_key_exists(substr($index, 0, strlen($index) - 7), $columns))
1186
          {
1187
            $reference = $references[0];
1188
 
1189
            // set simple index
1190
            $this->database[$table][$reference]['index'] = 'unique';
1191
 
1192
            // remove complex unique index
1193
            unset($this->database[$table]['_uniques'][$index]);
1194
          }
1195
 
1196
          $this->removeEmptyKey($this->database[$table], '_uniques');
1197
        }
1198
      }
1199
    }
1200
  }
1201
 
1202
  /**
1203
   * Fixes xml columns
1204
   *
1205
   * @return void
1206
   */
1207
  protected function fixXMLColumns()
1208
  {
1209
    foreach ($this->getTables() as $table => $columns)
1210
    {
1211
      foreach ($columns as $column => $attributes)
1212
      {
1213
        if ($column == 'id' && !array_diff($attributes, array('type' => 'integer', 'required' => 'true', 'primaryKey' => 'true', 'autoIncrement' => 'true')))
1214
        {
1215
          // simplify primary keys
1216
          $this->database[$table]['id'] = null;
1217
        }
1218
 
1219
        if (($column == 'created_at') || ($column == 'updated_at') && !array_diff($attributes, array('type' => 'timestamp')))
1220
        {
1221
          // simplify timestamps
1222
          $this->database[$table][$column] = null;
1223
        }
1224
 
1225
        $pos                 = strpos($column, '_id');
1226
        $has_fk_name         = $pos > 0 && $pos == strlen($column) - 3;
1227
        $is_foreign_key      = isset($attributes['type']) && $attributes['type'] == 'integer' && isset($attributes['foreignReference']) && $attributes['foreignReference'] == 'id';
1228
        $has_foreign_table   = isset($attributes['foreignTable']) && array_key_exists($attributes['foreignTable'], $this->getTables());
1229
        $has_other_attribute = isset($attributes['onDelete']);
1230
        if ($has_fk_name && $has_foreign_table && $is_foreign_key && !$has_other_attribute)
1231
        {
1232
          // simplify foreign key
1233
          $this->database[$table][$column] = null;
1234
        }
1235
      }
1236
    }
1237
  }
1238
 
1239
  protected function fixXMLBoolean($value)
1240
  {
1241
    switch (true)
1242
    {
1243
      case true === $value:
1244
        return 'true';
1245
      case false === $value:
1246
        return 'false';
1247
      default:
1248
        return $value;
1249
    }
1250
  }
1251
 
1252
  /**
1253
   * Dumps schema as yaml
1254
   *
1255
   * @return string
1256
   */
1257
  public function asYAML()
1258
  {
1259
    return sfYaml::dump(array($this->connection_name => $this->database), 3);
1260
  }
1261
 
1262
  /**
1263
   * Returns name and attributes from given hash
1264
   *
1265
   * @param string $hash
1266
   * @param string $name_attribute
1267
   *
1268
   * @return array
1269
   */
1270
  protected function getNameAndAttributes($hash, $name_attribute = 'name')
1271
  {
1272
    // tag name
1273
    $name = '';
1274
    if (isset($hash[$name_attribute]))
1275
    {
1276
      $name = strval($hash[$name_attribute]);
1277
      unset($hash[$name_attribute]);
1278
    }
1279
 
1280
    // tag attributes
1281
    $attributes = array();
1282
    foreach ($hash as $attribute => $value)
1283
    {
1284
      $value = (string) $value;
1285
      if (in_array($value, array('true', 'on')))
1286
      {
1287
        $value = true;
1288
      }
1289
      elseif (in_array($value, array('false', 'off')))
1290
      {
1291
        $value = false;
1292
      }
1293
      $attributes[$attribute] = $value;
1294
    }
1295
 
1296
    return array($name, $attributes);
1297
  }
1298
 
1299
  protected function removeEmptyKey(&$hash, $key)
1300
  {
1301
    if (isset($hash[$key]) && !$hash[$key])
1302
    {
1303
      unset($hash[$key]);
1304
    }
1305
  }
1306
}