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