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 = CONFIG . '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: $class = $this->name . 'Schema';
163:
164: if (!class_exists($class) && !$this->_requireFile($this->path, $this->file)) {
165: $class = Inflector::camelize(Inflector::slug(Configure::read('App.dir'))) . 'Schema';
166: if (!class_exists($class)) {
167: $this->_requireFile($this->path, $this->file);
168: }
169: }
170:
171: if (class_exists($class)) {
172: $Schema = new $class($options);
173: return $Schema;
174: }
175: return false;
176: }
177:
178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189:
190: public function read($options = array()) {
191: $options = array_merge(
192: array(
193: 'connection' => $this->connection,
194: 'name' => $this->name,
195: 'models' => true,
196: ),
197: $options
198: );
199: $db = ConnectionManager::getDataSource($options['connection']);
200:
201: if (isset($this->plugin)) {
202: App::uses($this->plugin . 'AppModel', $this->plugin . '.Model');
203: }
204:
205: $tables = array();
206: $currentTables = (array)$db->listSources();
207:
208: $prefix = null;
209: if (isset($db->config['prefix'])) {
210: $prefix = $db->config['prefix'];
211: }
212:
213: if (!is_array($options['models']) && $options['models'] !== false) {
214: if (isset($this->plugin)) {
215: $options['models'] = App::objects($this->plugin . '.Model', null, false);
216: } else {
217: $options['models'] = App::objects('Model');
218: }
219: }
220:
221: if (is_array($options['models'])) {
222: foreach ($options['models'] as $model) {
223: $importModel = $model;
224: $plugin = null;
225: if ($model === 'AppModel') {
226: continue;
227: }
228:
229: if (isset($this->plugin)) {
230: if ($model === $this->plugin . 'AppModel') {
231: continue;
232: }
233: $importModel = $model;
234: $plugin = $this->plugin . '.';
235: }
236:
237: App::uses($importModel, $plugin . 'Model');
238: if (!class_exists($importModel)) {
239: continue;
240: }
241:
242: $vars = get_class_vars($model);
243: if (empty($vars['useDbConfig']) || $vars['useDbConfig'] != $options['connection']) {
244: continue;
245: }
246:
247: try {
248: $Object = ClassRegistry::init(array('class' => $model, 'ds' => $options['connection']));
249: } catch (CakeException $e) {
250: continue;
251: }
252:
253: if (!is_object($Object) || $Object->useTable === false) {
254: continue;
255: }
256: $db = $Object->getDataSource();
257:
258: $fulltable = $table = $db->fullTableName($Object, false, false);
259: if ($prefix && strpos($table, $prefix) !== 0) {
260: continue;
261: }
262: if (!in_array($fulltable, $currentTables)) {
263: continue;
264: }
265:
266: $table = $this->_noPrefixTable($prefix, $table);
267:
268: $key = array_search($fulltable, $currentTables);
269: if (empty($tables[$table])) {
270: $tables[$table] = $this->_columns($Object);
271: $tables[$table]['indexes'] = $db->index($Object);
272: $tables[$table]['tableParameters'] = $db->readTableParameters($fulltable);
273: unset($currentTables[$key]);
274: }
275: if (empty($Object->hasAndBelongsToMany)) {
276: continue;
277: }
278: foreach ($Object->hasAndBelongsToMany as $assocData) {
279: if (isset($assocData['with'])) {
280: $class = $assocData['with'];
281: }
282: if (!is_object($Object->$class)) {
283: continue;
284: }
285: $withTable = $db->fullTableName($Object->$class, false, false);
286: if ($prefix && strpos($withTable, $prefix) !== 0) {
287: continue;
288: }
289: if (in_array($withTable, $currentTables)) {
290: $key = array_search($withTable, $currentTables);
291: $noPrefixWith = $this->_noPrefixTable($prefix, $withTable);
292:
293: $tables[$noPrefixWith] = $this->_columns($Object->$class);
294: $tables[$noPrefixWith]['indexes'] = $db->index($Object->$class);
295: $tables[$noPrefixWith]['tableParameters'] = $db->readTableParameters($withTable);
296: unset($currentTables[$key]);
297: }
298: }
299: }
300: }
301:
302: if (!empty($currentTables)) {
303: foreach ($currentTables as $table) {
304: if ($prefix) {
305: if (strpos($table, $prefix) !== 0) {
306: continue;
307: }
308: $table = $this->_noPrefixTable($prefix, $table);
309: }
310: $Object = new AppModel(array(
311: 'name' => Inflector::classify($table), 'table' => $table, 'ds' => $options['connection']
312: ));
313:
314: $systemTables = array(
315: 'aros', 'acos', 'aros_acos', Configure::read('Session.table'), 'i18n'
316: );
317:
318: $fulltable = $db->fullTableName($Object, false, false);
319:
320: if (in_array($table, $systemTables)) {
321: $tables[$Object->table] = $this->_columns($Object);
322: $tables[$Object->table]['indexes'] = $db->index($Object);
323: $tables[$Object->table]['tableParameters'] = $db->readTableParameters($fulltable);
324: } elseif ($options['models'] === false) {
325: $tables[$table] = $this->_columns($Object);
326: $tables[$table]['indexes'] = $db->index($Object);
327: $tables[$table]['tableParameters'] = $db->readTableParameters($fulltable);
328: } else {
329: $tables['missing'][$table] = $this->_columns($Object);
330: $tables['missing'][$table]['indexes'] = $db->index($Object);
331: $tables['missing'][$table]['tableParameters'] = $db->readTableParameters($fulltable);
332: }
333: }
334: }
335:
336: ksort($tables);
337: return array('name' => $options['name'], 'tables' => $tables);
338: }
339:
340: 341: 342: 343: 344: 345: 346:
347: public function write($object, $options = array()) {
348: if (is_object($object)) {
349: $object = get_object_vars($object);
350: $this->build($object);
351: }
352:
353: if (is_array($object)) {
354: $options = $object;
355: unset($object);
356: }
357:
358: $options = array_merge(
359: get_object_vars($this), $options
360: );
361:
362: $out = "class {$options['name']}Schema extends CakeSchema {\n\n";
363:
364: if ($options['path'] !== $this->path) {
365: $out .= "\tpublic \$path = '{$options['path']}';\n\n";
366: }
367:
368: if ($options['file'] !== $this->file) {
369: $out .= "\tpublic \$file = '{$options['file']}';\n\n";
370: }
371:
372: if ($options['connection'] !== 'default') {
373: $out .= "\tpublic \$connection = '{$options['connection']}';\n\n";
374: }
375:
376: $out .= "\tpublic function before(\$event = array()) {\n\t\treturn true;\n\t}\n\n\tpublic function after(\$event = array()) {\n\t}\n\n";
377:
378: if (empty($options['tables'])) {
379: $this->read();
380: }
381:
382: foreach ($options['tables'] as $table => $fields) {
383: if (!is_numeric($table) && $table !== 'missing') {
384: $out .= $this->generateTable($table, $fields);
385: }
386: }
387: $out .= "}\n";
388:
389: $file = new File($options['path'] . DS . $options['file'], true);
390: $content = "<?php \n{$out}";
391: if ($file->write($content)) {
392: return $content;
393: }
394: return false;
395: }
396:
397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407:
408: public function generateTable($table, $fields) {
409:
410: if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $table)) {
411: throw new Exception("Invalid table name '{$table}'");
412: }
413:
414: $out = "\tpublic \${$table} = array(\n";
415: if (is_array($fields)) {
416: $cols = array();
417: foreach ($fields as $field => $value) {
418: if ($field !== 'indexes' && $field !== 'tableParameters') {
419: if (is_string($value)) {
420: $type = $value;
421: $value = array('type' => $type);
422: }
423: $value['type'] = addslashes($value['type']);
424: $col = "\t\t'{$field}' => array('type' => '" . $value['type'] . "', ";
425: unset($value['type']);
426: $col .= implode(', ', $this->_values($value));
427: } elseif ($field === 'indexes') {
428: $col = "\t\t'indexes' => array(\n\t\t\t";
429: $props = array();
430: foreach ((array)$value as $key => $index) {
431: $props[] = "'{$key}' => array(" . implode(', ', $this->_values($index)) . ")";
432: }
433: $col .= implode(",\n\t\t\t", $props) . "\n\t\t";
434: } elseif ($field === 'tableParameters') {
435: $col = "\t\t'tableParameters' => array(";
436: $props = $this->_values($value);
437: $col .= implode(', ', $props);
438: }
439: $col .= ")";
440: $cols[] = $col;
441: }
442: $out .= implode(",\n", $cols);
443: }
444: $out .= "\n\t);\n\n";
445: return $out;
446: }
447:
448: 449: 450: 451: 452: 453: 454:
455: public function compare($old, $new = null) {
456: if (empty($new)) {
457: $new = $this;
458: }
459: if (is_array($new)) {
460: if (isset($new['tables'])) {
461: $new = $new['tables'];
462: }
463: } else {
464: $new = $new->tables;
465: }
466:
467: if (is_array($old)) {
468: if (isset($old['tables'])) {
469: $old = $old['tables'];
470: }
471: } else {
472: $old = $old->tables;
473: }
474: $tables = array();
475: foreach ($new as $table => $fields) {
476: if ($table === 'missing') {
477: continue;
478: }
479: if (!array_key_exists($table, $old)) {
480: $tables[$table]['create'] = $fields;
481: } else {
482: $diff = $this->_arrayDiffAssoc($fields, $old[$table]);
483: if (!empty($diff)) {
484: $tables[$table]['add'] = $diff;
485: }
486: $diff = $this->_arrayDiffAssoc($old[$table], $fields);
487: if (!empty($diff)) {
488: $tables[$table]['drop'] = $diff;
489: }
490: }
491:
492: foreach ($fields as $field => $value) {
493: if (!empty($old[$table][$field])) {
494: $diff = $this->_arrayDiffAssoc($value, $old[$table][$field]);
495: if (empty($diff)) {
496: $diff = $this->_arrayDiffAssoc($old[$table][$field], $value);
497: }
498: if (!empty($diff) && $field !== 'indexes' && $field !== 'tableParameters') {
499: $tables[$table]['change'][$field] = $value;
500: }
501: }
502:
503: if (isset($tables[$table]['add'][$field]) && $field !== 'indexes' && $field !== 'tableParameters') {
504: $wrapper = array_keys($fields);
505: if ($column = array_search($field, $wrapper)) {
506: if (isset($wrapper[$column - 1])) {
507: $tables[$table]['add'][$field]['after'] = $wrapper[$column - 1];
508: }
509: }
510: }
511: }
512:
513: if (isset($old[$table]['indexes']) && isset($new[$table]['indexes'])) {
514: $diff = $this->_compareIndexes($new[$table]['indexes'], $old[$table]['indexes']);
515: if ($diff) {
516: if (!isset($tables[$table])) {
517: $tables[$table] = array();
518: }
519: if (isset($diff['drop'])) {
520: $tables[$table]['drop']['indexes'] = $diff['drop'];
521: }
522: if ($diff && isset($diff['add'])) {
523: $tables[$table]['add']['indexes'] = $diff['add'];
524: }
525: }
526: }
527: if (isset($old[$table]['tableParameters']) && isset($new[$table]['tableParameters'])) {
528: $diff = $this->_compareTableParameters($new[$table]['tableParameters'], $old[$table]['tableParameters']);
529: if ($diff) {
530: $tables[$table]['change']['tableParameters'] = $diff;
531: }
532: }
533: }
534: return $tables;
535: }
536:
537: 538: 539: 540: 541: 542: 543: 544: 545: 546: 547: 548: 549:
550: protected function _arrayDiffAssoc($array1, $array2) {
551: $difference = array();
552: foreach ($array1 as $key => $value) {
553: if (!array_key_exists($key, $array2)) {
554: $difference[$key] = $value;
555: continue;
556: }
557: $correspondingValue = $array2[$key];
558: if (($value === null) !== ($correspondingValue === null)) {
559: $difference[$key] = $value;
560: continue;
561: }
562: if (is_bool($value) !== is_bool($correspondingValue)) {
563: $difference[$key] = $value;
564: continue;
565: }
566: if (is_array($value) && is_array($correspondingValue)) {
567: continue;
568: }
569: if ($value === $correspondingValue) {
570: continue;
571: }
572: $difference[$key] = $value;
573: }
574: return $difference;
575: }
576:
577: 578: 579: 580: 581: 582:
583: protected function _values($values) {
584: $vals = array();
585: if (is_array($values)) {
586: foreach ($values as $key => $val) {
587: if (is_array($val)) {
588: $vals[] = "'{$key}' => array(" . implode(", ", $this->_values($val)) . ")";
589: } else {
590: $val = var_export($val, true);
591: if ($val === 'NULL') {
592: $val = 'null';
593: }
594: if (!is_numeric($key)) {
595: $vals[] = "'{$key}' => {$val}";
596: } else {
597: $vals[] = "{$val}";
598: }
599: }
600: }
601: }
602: return $vals;
603: }
604:
605: 606: 607: 608: 609: 610:
611: protected function _columns(&$Obj) {
612: $db = $Obj->getDataSource();
613: $fields = $Obj->schema(true);
614:
615: $hasPrimaryAlready = false;
616: foreach ($fields as $value) {
617: if (isset($value['key']) && $value['key'] === 'primary') {
618: $hasPrimaryAlready = true;
619: break;
620: }
621: }
622:
623: $columns = array();
624: foreach ($fields as $name => $value) {
625: if ($Obj->primaryKey === $name && !$hasPrimaryAlready && !isset($value['key'])) {
626: $value['key'] = 'primary';
627: }
628: if (substr($value['type'], 0, 4) !== 'enum') {
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:
643: if (isset($value['default']) && ($value['default'] === '' || ($value['default'] === false && $value['type'] !== 'boolean'))) {
644: unset($value['default']);
645: }
646: if (empty($value['length'])) {
647: unset($value['length']);
648: }
649: if (empty($value['key'])) {
650: unset($value['key']);
651: }
652: $columns[$name] = $value;
653: }
654:
655: return $columns;
656: }
657:
658: 659: 660: 661: 662: 663: 664:
665: protected function _compareTableParameters($new, $old) {
666: if (!is_array($new) || !is_array($old)) {
667: return false;
668: }
669: $change = $this->_arrayDiffAssoc($new, $old);
670: return $change;
671: }
672:
673: 674: 675: 676: 677: 678: 679:
680: protected function _compareIndexes($new, $old) {
681: if (!is_array($new) || !is_array($old)) {
682: return false;
683: }
684:
685: $add = $drop = array();
686:
687: $diff = $this->_arrayDiffAssoc($new, $old);
688: if (!empty($diff)) {
689: $add = $diff;
690: }
691:
692: $diff = $this->_arrayDiffAssoc($old, $new);
693: if (!empty($diff)) {
694: $drop = $diff;
695: }
696:
697: foreach ($new as $name => $value) {
698: if (isset($old[$name])) {
699: $newUnique = isset($value['unique']) ? $value['unique'] : 0;
700: $oldUnique = isset($old[$name]['unique']) ? $old[$name]['unique'] : 0;
701: $newColumn = $value['column'];
702: $oldColumn = $old[$name]['column'];
703:
704: $diff = false;
705:
706: if ($newUnique != $oldUnique) {
707: $diff = true;
708: } elseif (is_array($newColumn) && is_array($oldColumn)) {
709: $diff = ($newColumn !== $oldColumn);
710: } elseif (is_string($newColumn) && is_string($oldColumn)) {
711: $diff = ($newColumn != $oldColumn);
712: } else {
713: $diff = true;
714: }
715: if ($diff) {
716: $drop[$name] = null;
717: $add[$name] = $value;
718: }
719: }
720: }
721: return array_filter(compact('add', 'drop'));
722: }
723:
724: 725: 726: 727: 728: 729: 730: 731:
732: protected function _noPrefixTable($prefix, $table) {
733: return preg_replace('/^' . preg_quote($prefix) . '/', '', $table);
734: }
735:
736: 737: 738: 739: 740: 741: 742:
743: protected function _requireFile($path, $file) {
744: if (file_exists($path . DS . $file) && is_file($path . DS . $file)) {
745: require_once $path . DS . $file;
746: return true;
747: } elseif (file_exists($path . DS . 'schema.php') && is_file($path . DS . 'schema.php')) {
748: require_once $path . DS . 'schema.php';
749: return true;
750: }
751: return false;
752: }
753:
754: }
755: