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