1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17:
18:
19: App::uses('Model', 'Model');
20: App::uses('AppModel', 'Model');
21: App::uses('ConnectionManager', 'Model');
22: App::uses('File', 'Utility');
23:
24: 25: 26: 27: 28:
29: class CakeSchema extends CakeObject {
30:
31: 32: 33: 34: 35:
36: public $name = null;
37:
38: 39: 40: 41: 42:
43: public $path = null;
44:
45: 46: 47: 48: 49:
50: public $file = 'schema.php';
51:
52: 53: 54: 55: 56:
57: public $connection = 'default';
58:
59: 60: 61: 62: 63:
64: public $plugin = null;
65:
66: 67: 68: 69: 70:
71: public $tables = array();
72:
73: 74: 75: 76: 77:
78: public function __construct($options = array()) {
79: parent::__construct();
80:
81: if (empty($options['name'])) {
82: $this->name = preg_replace('/schema$/i', '', get_class($this));
83: }
84: if (!empty($options['plugin'])) {
85: $this->plugin = $options['plugin'];
86: }
87:
88: if (strtolower($this->name) === 'cake') {
89: $this->name = 'App';
90: }
91:
92: if (empty($options['path'])) {
93: $this->path = APP . 'Config' . DS . 'Schema';
94: }
95:
96: $options = array_merge(get_object_vars($this), $options);
97: $this->build($options);
98: }
99:
100: 101: 102: 103: 104: 105:
106: public function build($data) {
107: $file = null;
108: foreach ($data as $key => $val) {
109: if (!empty($val)) {
110: if (!in_array($key, array('plugin', 'name', 'path', 'file', 'connection', 'tables', '_log'))) {
111: if ($key[0] === '_') {
112: continue;
113: }
114: $this->tables[$key] = $val;
115: unset($this->{$key});
116: } elseif ($key !== 'tables') {
117: if ($key === 'name' && $val !== $this->name && !isset($data['file'])) {
118: $file = Inflector::underscore($val) . '.php';
119: }
120: $this->{$key} = $val;
121: }
122: }
123: }
124: if (file_exists($this->path . DS . $file) && is_file($this->path . DS . $file)) {
125: $this->file = $file;
126: } elseif (!empty($this->plugin)) {
127: $this->path = CakePlugin::path($this->plugin) . 'Config' . DS . 'Schema';
128: }
129: }
130:
131: 132: 133: 134: 135: 136:
137: public function before($event = array()) {
138: return true;
139: }
140:
141: 142: 143: 144: 145: 146:
147: public function after($event = array()) {
148: }
149:
150: 151: 152: 153: 154: 155:
156: public function load($options = array()) {
157: if (is_string($options)) {
158: $options = array('path' => $options);
159: }
160:
161: $this->build($options);
162: extract(get_object_vars($this));
163:
164: $class = $name . 'Schema';
165:
166: if (!class_exists($class) && !$this->_requireFile($path, $file)) {
167: $class = Inflector::camelize(Inflector::slug(Configure::read('App.dir'))) . 'Schema';
168: if (!class_exists($class)) {
169: $this->_requireFile($path, $file);
170: }
171: }
172:
173: if (class_exists($class)) {
174: $Schema = new $class($options);
175: return $Schema;
176: }
177: return false;
178: }
179:
180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191:
192: public function read($options = array()) {
193: extract(array_merge(
194: array(
195: 'connection' => $this->connection,
196: 'name' => $this->name,
197: 'models' => true,
198: ),
199: $options
200: ));
201: $db = ConnectionManager::getDataSource($connection);
202:
203: if (isset($this->plugin)) {
204: App::uses($this->plugin . 'AppModel', $this->plugin . '.Model');
205: }
206:
207: $tables = array();
208: $currentTables = (array)$db->listSources();
209:
210: $prefix = null;
211: if (isset($db->config['prefix'])) {
212: $prefix = $db->config['prefix'];
213: }
214:
215: if (!is_array($models) && $models !== false) {
216: if (isset($this->plugin)) {
217: $models = App::objects($this->plugin . '.Model', null, false);
218: } else {
219: $models = App::objects('Model');
220: }
221: }
222:
223: if (is_array($models)) {
224: foreach ($models as $model) {
225: $importModel = $model;
226: $plugin = null;
227: if ($model === 'AppModel') {
228: continue;
229: }
230:
231: if (isset($this->plugin)) {
232: if ($model === $this->plugin . 'AppModel') {
233: continue;
234: }
235: $importModel = $model;
236: $plugin = $this->plugin . '.';
237: }
238:
239: App::uses($importModel, $plugin . 'Model');
240: if (!class_exists($importModel)) {
241: continue;
242: }
243:
244: $vars = get_class_vars($model);
245: if (empty($vars['useDbConfig']) || $vars['useDbConfig'] != $connection) {
246: continue;
247: }
248:
249: try {
250: $Object = ClassRegistry::init(array('class' => $model, 'ds' => $connection));
251: } catch (CakeException $e) {
252: continue;
253: }
254:
255: if (!is_object($Object) || $Object->useTable === false) {
256: continue;
257: }
258: $db = $Object->getDataSource();
259:
260: $fulltable = $table = $db->fullTableName($Object, false, false);
261: if ($prefix && strpos($table, $prefix) !== 0) {
262: continue;
263: }
264: if (!in_array($fulltable, $currentTables)) {
265: continue;
266: }
267:
268: $table = $this->_noPrefixTable($prefix, $table);
269:
270: $key = array_search($fulltable, $currentTables);
271: if (empty($tables[$table])) {
272: $tables[$table] = $this->_columns($Object);
273: $tables[$table]['indexes'] = $db->index($Object);
274: $tables[$table]['tableParameters'] = $db->readTableParameters($fulltable);
275: unset($currentTables[$key]);
276: }
277: if (empty($Object->hasAndBelongsToMany)) {
278: continue;
279: }
280: foreach ($Object->hasAndBelongsToMany as $assocData) {
281: if (isset($assocData['with'])) {
282: $class = $assocData['with'];
283: }
284: if (!is_object($Object->$class)) {
285: continue;
286: }
287: $withTable = $db->fullTableName($Object->$class, false, false);
288: if ($prefix && strpos($withTable, $prefix) !== 0) {
289: continue;
290: }
291: if (in_array($withTable, $currentTables)) {
292: $key = array_search($withTable, $currentTables);
293: $noPrefixWith = $this->_noPrefixTable($prefix, $withTable);
294:
295: $tables[$noPrefixWith] = $this->_columns($Object->$class);
296: $tables[$noPrefixWith]['indexes'] = $db->index($Object->$class);
297: $tables[$noPrefixWith]['tableParameters'] = $db->readTableParameters($withTable);
298: unset($currentTables[$key]);
299: }
300: }
301: }
302: }
303:
304: if (!empty($currentTables)) {
305: foreach ($currentTables as $table) {
306: if ($prefix) {
307: if (strpos($table, $prefix) !== 0) {
308: continue;
309: }
310: $table = $this->_noPrefixTable($prefix, $table);
311: }
312: $Object = new AppModel(array(
313: 'name' => Inflector::classify($table), 'table' => $table, 'ds' => $connection
314: ));
315:
316: $systemTables = array(
317: 'aros', 'acos', 'aros_acos', Configure::read('Session.table'), 'i18n'
318: );
319:
320: $fulltable = $db->fullTableName($Object, false, false);
321:
322: if (in_array($table, $systemTables)) {
323: $tables[$Object->table] = $this->_columns($Object);
324: $tables[$Object->table]['indexes'] = $db->index($Object);
325: $tables[$Object->table]['tableParameters'] = $db->readTableParameters($fulltable);
326: } elseif ($models === false) {
327: $tables[$table] = $this->_columns($Object);
328: $tables[$table]['indexes'] = $db->index($Object);
329: $tables[$table]['tableParameters'] = $db->readTableParameters($fulltable);
330: } else {
331: $tables['missing'][$table] = $this->_columns($Object);
332: $tables['missing'][$table]['indexes'] = $db->index($Object);
333: $tables['missing'][$table]['tableParameters'] = $db->readTableParameters($fulltable);
334: }
335: }
336: }
337:
338: ksort($tables);
339: return compact('name', 'tables');
340: }
341:
342: 343: 344: 345: 346: 347: 348:
349: public function write($object, $options = array()) {
350: if (is_object($object)) {
351: $object = get_object_vars($object);
352: $this->build($object);
353: }
354:
355: if (is_array($object)) {
356: $options = $object;
357: unset($object);
358: }
359:
360: extract(array_merge(
361: get_object_vars($this), $options
362: ));
363:
364: $out = "class {$name}Schema extends CakeSchema {\n\n";
365:
366: if ($path !== $this->path) {
367: $out .= "\tpublic \$path = '{$path}';\n\n";
368: }
369:
370: if ($file !== $this->file) {
371: $out .= "\tpublic \$file = '{$file}';\n\n";
372: }
373:
374: if ($connection !== 'default') {
375: $out .= "\tpublic \$connection = '{$connection}';\n\n";
376: }
377:
378: $out .= "\tpublic function before(\$event = array()) {\n\t\treturn true;\n\t}\n\n\tpublic function after(\$event = array()) {\n\t}\n\n";
379:
380: if (empty($tables)) {
381: $this->read();
382: }
383:
384: foreach ($tables as $table => $fields) {
385: if (!is_numeric($table) && $table !== 'missing') {
386: $out .= $this->generateTable($table, $fields);
387: }
388: }
389: $out .= "}\n";
390:
391: $file = new File($path . DS . $file, true);
392: $content = "<?php \n{$out}";
393: if ($file->write($content)) {
394: return $content;
395: }
396: return false;
397: }
398:
399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409:
410: public function generateTable($table, $fields) {
411:
412: if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $table)) {
413: throw new Exception("Invalid table name '{$table}'");
414: }
415:
416: $out = "\tpublic \${$table} = array(\n";
417: if (is_array($fields)) {
418: $cols = array();
419: foreach ($fields as $field => $value) {
420: if ($field !== 'indexes' && $field !== 'tableParameters') {
421: if (is_string($value)) {
422: $type = $value;
423: $value = array('type' => $type);
424: }
425: $col = "\t\t'{$field}' => array('type' => '" . $value['type'] . "', ";
426: unset($value['type']);
427: $col .= implode(', ', $this->_values($value));
428: } elseif ($field === 'indexes') {
429: $col = "\t\t'indexes' => array(\n\t\t\t";
430: $props = array();
431: foreach ((array)$value as $key => $index) {
432: $props[] = "'{$key}' => array(" . implode(', ', $this->_values($index)) . ")";
433: }
434: $col .= implode(",\n\t\t\t", $props) . "\n\t\t";
435: } elseif ($field === 'tableParameters') {
436: $col = "\t\t'tableParameters' => array(";
437: $props = $this->_values($value);
438: $col .= implode(', ', $props);
439: }
440: $col .= ")";
441: $cols[] = $col;
442: }
443: $out .= implode(",\n", $cols);
444: }
445: $out .= "\n\t);\n\n";
446: return $out;
447: }
448:
449: 450: 451: 452: 453: 454: 455:
456: public function compare($old, $new = null) {
457: if (empty($new)) {
458: $new = $this;
459: }
460: if (is_array($new)) {
461: if (isset($new['tables'])) {
462: $new = $new['tables'];
463: }
464: } else {
465: $new = $new->tables;
466: }
467:
468: if (is_array($old)) {
469: if (isset($old['tables'])) {
470: $old = $old['tables'];
471: }
472: } else {
473: $old = $old->tables;
474: }
475: $tables = array();
476: foreach ($new as $table => $fields) {
477: if ($table === 'missing') {
478: continue;
479: }
480: if (!array_key_exists($table, $old)) {
481: $tables[$table]['create'] = $fields;
482: } else {
483: $diff = $this->_arrayDiffAssoc($fields, $old[$table]);
484: if (!empty($diff)) {
485: $tables[$table]['add'] = $diff;
486: }
487: $diff = $this->_arrayDiffAssoc($old[$table], $fields);
488: if (!empty($diff)) {
489: $tables[$table]['drop'] = $diff;
490: }
491: }
492:
493: foreach ($fields as $field => $value) {
494: if (!empty($old[$table][$field])) {
495: $diff = $this->_arrayDiffAssoc($value, $old[$table][$field]);
496: if (empty($diff)) {
497: $diff = $this->_arrayDiffAssoc($old[$table][$field], $value);
498: }
499: if (!empty($diff) && $field !== 'indexes' && $field !== 'tableParameters') {
500: $tables[$table]['change'][$field] = $value;
501: }
502: }
503:
504: if (isset($tables[$table]['add'][$field]) && $field !== 'indexes' && $field !== 'tableParameters') {
505: $wrapper = array_keys($fields);
506: if ($column = array_search($field, $wrapper)) {
507: if (isset($wrapper[$column - 1])) {
508: $tables[$table]['add'][$field]['after'] = $wrapper[$column - 1];
509: }
510: }
511: }
512: }
513:
514: if (isset($old[$table]['indexes']) && isset($new[$table]['indexes'])) {
515: $diff = $this->_compareIndexes($new[$table]['indexes'], $old[$table]['indexes']);
516: if ($diff) {
517: if (!isset($tables[$table])) {
518: $tables[$table] = array();
519: }
520: if (isset($diff['drop'])) {
521: $tables[$table]['drop']['indexes'] = $diff['drop'];
522: }
523: if ($diff && isset($diff['add'])) {
524: $tables[$table]['add']['indexes'] = $diff['add'];
525: }
526: }
527: }
528: if (isset($old[$table]['tableParameters']) && isset($new[$table]['tableParameters'])) {
529: $diff = $this->_compareTableParameters($new[$table]['tableParameters'], $old[$table]['tableParameters']);
530: if ($diff) {
531: $tables[$table]['change']['tableParameters'] = $diff;
532: }
533: }
534: }
535: return $tables;
536: }
537:
538: 539: 540: 541: 542: 543: 544: 545: 546: 547: 548: 549: 550:
551: protected function _arrayDiffAssoc($array1, $array2) {
552: $difference = array();
553: foreach ($array1 as $key => $value) {
554: if (!array_key_exists($key, $array2)) {
555: $difference[$key] = $value;
556: continue;
557: }
558: $correspondingValue = $array2[$key];
559: if (($value === null) !== ($correspondingValue === null)) {
560: $difference[$key] = $value;
561: continue;
562: }
563: if (is_bool($value) !== is_bool($correspondingValue)) {
564: $difference[$key] = $value;
565: continue;
566: }
567: if (is_array($value) && is_array($correspondingValue)) {
568: continue;
569: }
570: if ($value === $correspondingValue) {
571: continue;
572: }
573: $difference[$key] = $value;
574: }
575: return $difference;
576: }
577:
578: 579: 580: 581: 582: 583:
584: protected function _values($values) {
585: $vals = array();
586: if (is_array($values)) {
587: foreach ($values as $key => $val) {
588: if (is_array($val)) {
589: $vals[] = "'{$key}' => array(" . implode(", ", $this->_values($val)) . ")";
590: } else {
591: $val = var_export($val, true);
592: if ($val === 'NULL') {
593: $val = 'null';
594: }
595: if (!is_numeric($key)) {
596: $vals[] = "'{$key}' => {$val}";
597: } else {
598: $vals[] = "{$val}";
599: }
600: }
601: }
602: }
603: return $vals;
604: }
605:
606: 607: 608: 609: 610: 611:
612: protected function _columns(&$Obj) {
613: $db = $Obj->getDataSource();
614: $fields = $Obj->schema(true);
615:
616: $hasPrimaryAlready = false;
617: foreach ($fields as $value) {
618: if (isset($value['key']) && $value['key'] === 'primary') {
619: $hasPrimaryAlready = true;
620: break;
621: }
622: }
623:
624: $columns = array();
625: foreach ($fields as $name => $value) {
626: if ($Obj->primaryKey === $name && !$hasPrimaryAlready && !isset($value['key'])) {
627: $value['key'] = 'primary';
628: }
629: if (!isset($db->columns[$value['type']])) {
630: trigger_error(__d('cake_dev', 'Schema generation error: invalid column type %s for %s.%s does not exist in DBO', $value['type'], $Obj->name, $name), E_USER_NOTICE);
631: continue;
632: } else {
633: $defaultCol = $db->columns[$value['type']];
634: if (isset($defaultCol['limit']) && $defaultCol['limit'] == $value['length']) {
635: unset($value['length']);
636: } elseif (isset($defaultCol['length']) && $defaultCol['length'] == $value['length']) {
637: unset($value['length']);
638: }
639: unset($value['limit']);
640: }
641:
642: if (isset($value['default']) && ($value['default'] === '' || ($value['default'] === false && $value['type'] !== 'boolean'))) {
643: unset($value['default']);
644: }
645: if (empty($value['length'])) {
646: unset($value['length']);
647: }
648: if (empty($value['key'])) {
649: unset($value['key']);
650: }
651: $columns[$name] = $value;
652: }
653:
654: return $columns;
655: }
656:
657: 658: 659: 660: 661: 662: 663:
664: protected function _compareTableParameters($new, $old) {
665: if (!is_array($new) || !is_array($old)) {
666: return false;
667: }
668: $change = $this->_arrayDiffAssoc($new, $old);
669: return $change;
670: }
671:
672: 673: 674: 675: 676: 677: 678:
679: protected function _compareIndexes($new, $old) {
680: if (!is_array($new) || !is_array($old)) {
681: return false;
682: }
683:
684: $add = $drop = array();
685:
686: $diff = $this->_arrayDiffAssoc($new, $old);
687: if (!empty($diff)) {
688: $add = $diff;
689: }
690:
691: $diff = $this->_arrayDiffAssoc($old, $new);
692: if (!empty($diff)) {
693: $drop = $diff;
694: }
695:
696: foreach ($new as $name => $value) {
697: if (isset($old[$name])) {
698: $newUnique = isset($value['unique']) ? $value['unique'] : 0;
699: $oldUnique = isset($old[$name]['unique']) ? $old[$name]['unique'] : 0;
700: $newColumn = $value['column'];
701: $oldColumn = $old[$name]['column'];
702:
703: $diff = false;
704:
705: if ($newUnique != $oldUnique) {
706: $diff = true;
707: } elseif (is_array($newColumn) && is_array($oldColumn)) {
708: $diff = ($newColumn !== $oldColumn);
709: } elseif (is_string($newColumn) && is_string($oldColumn)) {
710: $diff = ($newColumn != $oldColumn);
711: } else {
712: $diff = true;
713: }
714: if ($diff) {
715: $drop[$name] = null;
716: $add[$name] = $value;
717: }
718: }
719: }
720: return array_filter(compact('add', 'drop'));
721: }
722:
723: 724: 725: 726: 727: 728: 729: 730:
731: protected function _noPrefixTable($prefix, $table) {
732: return preg_replace('/^' . preg_quote($prefix) . '/', '', $table);
733: }
734:
735: 736: 737: 738: 739: 740: 741:
742: protected function _requireFile($path, $file) {
743: if (file_exists($path . DS . $file) && is_file($path . DS . $file)) {
744: require_once $path . DS . $file;
745: return true;
746: } elseif (file_exists($path . DS . 'schema.php') && is_file($path . DS . 'schema.php')) {
747: require_once $path . DS . 'schema.php';
748: return true;
749: }
750: return false;
751: }
752:
753: }
754: