CakeSchema.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720
  1. <?php
  2. /**
  3. * Schema database management for CakePHP.
  4. *
  5. * PHP 5
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
  9. *
  10. * Licensed under The MIT License
  11. * Redistributions of files must retain the above copyright notice.
  12. *
  13. * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
  14. * @link http://cakephp.org CakePHP(tm) Project
  15. * @package Cake.Model
  16. * @since CakePHP(tm) v 1.2.0.5550
  17. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  18. */
  19. App::uses('Model', 'Model');
  20. App::uses('AppModel', 'Model');
  21. App::uses('ConnectionManager', 'Model');
  22. App::uses('File', 'Utility');
  23. /**
  24. * Base Class for Schema management
  25. *
  26. * @package Cake.Model
  27. */
  28. class CakeSchema extends Object {
  29. /**
  30. * Name of the schema
  31. *
  32. * @var string
  33. */
  34. public $name = null;
  35. /**
  36. * Path to write location
  37. *
  38. * @var string
  39. */
  40. public $path = null;
  41. /**
  42. * File to write
  43. *
  44. * @var string
  45. */
  46. public $file = 'schema.php';
  47. /**
  48. * Connection used for read
  49. *
  50. * @var string
  51. */
  52. public $connection = 'default';
  53. /**
  54. * plugin name.
  55. *
  56. * @var string
  57. */
  58. public $plugin = null;
  59. /**
  60. * Set of tables
  61. *
  62. * @var array
  63. */
  64. public $tables = array();
  65. /**
  66. * Constructor
  67. *
  68. * @param array $options optional load object properties
  69. */
  70. public function __construct($options = array()) {
  71. parent::__construct();
  72. if (empty($options['name'])) {
  73. $this->name = preg_replace('/schema$/i', '', get_class($this));
  74. }
  75. if (!empty($options['plugin'])) {
  76. $this->plugin = $options['plugin'];
  77. }
  78. if (strtolower($this->name) === 'cake') {
  79. $this->name = Inflector::camelize(Inflector::slug(Configure::read('App.dir')));
  80. }
  81. if (empty($options['path'])) {
  82. $this->path = APP . 'Config' . DS . 'Schema';
  83. }
  84. $options = array_merge(get_object_vars($this), $options);
  85. $this->build($options);
  86. }
  87. /**
  88. * Builds schema object properties
  89. *
  90. * @param array $data loaded object properties
  91. * @return void
  92. */
  93. public function build($data) {
  94. $file = null;
  95. foreach ($data as $key => $val) {
  96. if (!empty($val)) {
  97. if (!in_array($key, array('plugin', 'name', 'path', 'file', 'connection', 'tables', '_log'))) {
  98. if ($key[0] === '_') {
  99. continue;
  100. }
  101. $this->tables[$key] = $val;
  102. unset($this->{$key});
  103. } elseif ($key !== 'tables') {
  104. if ($key === 'name' && $val !== $this->name && !isset($data['file'])) {
  105. $file = Inflector::underscore($val) . '.php';
  106. }
  107. $this->{$key} = $val;
  108. }
  109. }
  110. }
  111. if (file_exists($this->path . DS . $file) && is_file($this->path . DS . $file)) {
  112. $this->file = $file;
  113. } elseif (!empty($this->plugin)) {
  114. $this->path = CakePlugin::path($this->plugin) . 'Config' . DS . 'Schema';
  115. }
  116. }
  117. /**
  118. * Before callback to be implemented in subclasses
  119. *
  120. * @param array $event schema object properties
  121. * @return boolean Should process continue
  122. */
  123. public function before($event = array()) {
  124. return true;
  125. }
  126. /**
  127. * After callback to be implemented in subclasses
  128. *
  129. * @param array $event schema object properties
  130. * @return void
  131. */
  132. public function after($event = array()) {
  133. }
  134. /**
  135. * Reads database and creates schema tables
  136. *
  137. * @param array $options schema object properties
  138. * @return array Set of name and tables
  139. */
  140. public function load($options = array()) {
  141. if (is_string($options)) {
  142. $options = array('path' => $options);
  143. }
  144. $this->build($options);
  145. extract(get_object_vars($this));
  146. $class = $name . 'Schema';
  147. if (!class_exists($class)) {
  148. if (file_exists($path . DS . $file) && is_file($path . DS . $file)) {
  149. require_once $path . DS . $file;
  150. } elseif (file_exists($path . DS . 'schema.php') && is_file($path . DS . 'schema.php')) {
  151. require_once $path . DS . 'schema.php';
  152. }
  153. }
  154. if (class_exists($class)) {
  155. $Schema = new $class($options);
  156. return $Schema;
  157. }
  158. return false;
  159. }
  160. /**
  161. * Reads database and creates schema tables
  162. *
  163. * Options
  164. *
  165. * - 'connection' - the db connection to use
  166. * - 'name' - name of the schema
  167. * - 'models' - a list of models to use, or false to ignore models
  168. *
  169. * @param array $options schema object properties
  170. * @return array Array indexed by name and tables
  171. */
  172. public function read($options = array()) {
  173. extract(array_merge(
  174. array(
  175. 'connection' => $this->connection,
  176. 'name' => $this->name,
  177. 'models' => true,
  178. ),
  179. $options
  180. ));
  181. $db = ConnectionManager::getDataSource($connection);
  182. if (isset($this->plugin)) {
  183. App::uses($this->plugin . 'AppModel', $this->plugin . '.Model');
  184. }
  185. $tables = array();
  186. $currentTables = (array)$db->listSources();
  187. $prefix = null;
  188. if (isset($db->config['prefix'])) {
  189. $prefix = $db->config['prefix'];
  190. }
  191. if (!is_array($models) && $models !== false) {
  192. if (isset($this->plugin)) {
  193. $models = App::objects($this->plugin . '.Model', null, false);
  194. } else {
  195. $models = App::objects('Model');
  196. }
  197. }
  198. if (is_array($models)) {
  199. foreach ($models as $model) {
  200. $importModel = $model;
  201. $plugin = null;
  202. if ($model == 'AppModel') {
  203. continue;
  204. }
  205. if (isset($this->plugin)) {
  206. if ($model == $this->plugin . 'AppModel') {
  207. continue;
  208. }
  209. $importModel = $model;
  210. $plugin = $this->plugin . '.';
  211. }
  212. App::uses($importModel, $plugin . 'Model');
  213. if (!class_exists($importModel)) {
  214. continue;
  215. }
  216. $vars = get_class_vars($model);
  217. if (empty($vars['useDbConfig']) || $vars['useDbConfig'] != $connection) {
  218. continue;
  219. }
  220. try {
  221. $Object = ClassRegistry::init(array('class' => $model, 'ds' => $connection));
  222. } catch (CakeException $e) {
  223. continue;
  224. }
  225. if (!is_object($Object) || $Object->useTable === false) {
  226. continue;
  227. }
  228. $db = $Object->getDataSource();
  229. $fulltable = $table = $db->fullTableName($Object, false, false);
  230. if ($prefix && strpos($table, $prefix) !== 0) {
  231. continue;
  232. }
  233. if (!in_array($fulltable, $currentTables)) {
  234. continue;
  235. }
  236. $table = $this->_noPrefixTable($prefix, $table);
  237. $key = array_search($fulltable, $currentTables);
  238. if (empty($tables[$table])) {
  239. $tables[$table] = $this->_columns($Object);
  240. $tables[$table]['indexes'] = $db->index($Object);
  241. $tables[$table]['tableParameters'] = $db->readTableParameters($fulltable);
  242. unset($currentTables[$key]);
  243. }
  244. if (empty($Object->hasAndBelongsToMany)) {
  245. continue;
  246. }
  247. foreach ($Object->hasAndBelongsToMany as $assocData) {
  248. if (isset($assocData['with'])) {
  249. $class = $assocData['with'];
  250. }
  251. if (!is_object($Object->$class)) {
  252. continue;
  253. }
  254. $withTable = $db->fullTableName($Object->$class, false, false);
  255. if ($prefix && strpos($withTable, $prefix) !== 0) {
  256. continue;
  257. }
  258. if (in_array($withTable, $currentTables)) {
  259. $key = array_search($withTable, $currentTables);
  260. $noPrefixWith = $this->_noPrefixTable($prefix, $withTable);
  261. $tables[$noPrefixWith] = $this->_columns($Object->$class);
  262. $tables[$noPrefixWith]['indexes'] = $db->index($Object->$class);
  263. $tables[$noPrefixWith]['tableParameters'] = $db->readTableParameters($withTable);
  264. unset($currentTables[$key]);
  265. }
  266. }
  267. }
  268. }
  269. if (!empty($currentTables)) {
  270. foreach ($currentTables as $table) {
  271. if ($prefix) {
  272. if (strpos($table, $prefix) !== 0) {
  273. continue;
  274. }
  275. $table = $this->_noPrefixTable($prefix, $table);
  276. }
  277. $Object = new AppModel(array(
  278. 'name' => Inflector::classify($table), 'table' => $table, 'ds' => $connection
  279. ));
  280. $systemTables = array(
  281. 'aros', 'acos', 'aros_acos', Configure::read('Session.table'), 'i18n'
  282. );
  283. $fulltable = $db->fullTableName($Object, false, false);
  284. if (in_array($table, $systemTables)) {
  285. $tables[$Object->table] = $this->_columns($Object);
  286. $tables[$Object->table]['indexes'] = $db->index($Object);
  287. $tables[$Object->table]['tableParameters'] = $db->readTableParameters($fulltable);
  288. } elseif ($models === false) {
  289. $tables[$table] = $this->_columns($Object);
  290. $tables[$table]['indexes'] = $db->index($Object);
  291. $tables[$table]['tableParameters'] = $db->readTableParameters($fulltable);
  292. } else {
  293. $tables['missing'][$table] = $this->_columns($Object);
  294. $tables['missing'][$table]['indexes'] = $db->index($Object);
  295. $tables['missing'][$table]['tableParameters'] = $db->readTableParameters($fulltable);
  296. }
  297. }
  298. }
  299. ksort($tables);
  300. return compact('name', 'tables');
  301. }
  302. /**
  303. * Writes schema file from object or options
  304. *
  305. * @param array|object $object schema object or options array
  306. * @param array $options schema object properties to override object
  307. * @return mixed false or string written to file
  308. */
  309. public function write($object, $options = array()) {
  310. if (is_object($object)) {
  311. $object = get_object_vars($object);
  312. $this->build($object);
  313. }
  314. if (is_array($object)) {
  315. $options = $object;
  316. unset($object);
  317. }
  318. extract(array_merge(
  319. get_object_vars($this), $options
  320. ));
  321. $out = "class {$name}Schema extends CakeSchema {\n\n";
  322. if ($path !== $this->path) {
  323. $out .= "\tpublic \$path = '{$path}';\n\n";
  324. }
  325. if ($file !== $this->file) {
  326. $out .= "\tpublic \$file = '{$file}';\n\n";
  327. }
  328. if ($connection !== 'default') {
  329. $out .= "\tpublic \$connection = '{$connection}';\n\n";
  330. }
  331. $out .= "\tpublic function before(\$event = array()) {\n\t\treturn true;\n\t}\n\n\tpublic function after(\$event = array()) {\n\t}\n\n";
  332. if (empty($tables)) {
  333. $this->read();
  334. }
  335. foreach ($tables as $table => $fields) {
  336. if (!is_numeric($table) && $table !== 'missing') {
  337. $out .= $this->generateTable($table, $fields);
  338. }
  339. }
  340. $out .= "}\n";
  341. $file = new File($path . DS . $file, true);
  342. $content = "<?php \n{$out}";
  343. if ($file->write($content)) {
  344. return $content;
  345. }
  346. return false;
  347. }
  348. /**
  349. * Generate the code for a table. Takes a table name and $fields array
  350. * Returns a completed variable declaration to be used in schema classes
  351. *
  352. * @param string $table Table name you want returned.
  353. * @param array $fields Array of field information to generate the table with.
  354. * @return string Variable declaration for a schema class
  355. */
  356. public function generateTable($table, $fields) {
  357. $out = "\tpublic \${$table} = array(\n";
  358. if (is_array($fields)) {
  359. $cols = array();
  360. foreach ($fields as $field => $value) {
  361. if ($field != 'indexes' && $field != 'tableParameters') {
  362. if (is_string($value)) {
  363. $type = $value;
  364. $value = array('type' => $type);
  365. }
  366. $col = "\t\t'{$field}' => array('type' => '" . $value['type'] . "', ";
  367. unset($value['type']);
  368. $col .= implode(', ', $this->_values($value));
  369. } elseif ($field == 'indexes') {
  370. $col = "\t\t'indexes' => array(\n\t\t\t";
  371. $props = array();
  372. foreach ((array)$value as $key => $index) {
  373. $props[] = "'{$key}' => array(" . implode(', ', $this->_values($index)) . ")";
  374. }
  375. $col .= implode(",\n\t\t\t", $props) . "\n\t\t";
  376. } elseif ($field == 'tableParameters') {
  377. $col = "\t\t'tableParameters' => array(";
  378. $props = array();
  379. foreach ((array)$value as $key => $param) {
  380. $props[] = "'{$key}' => '$param'";
  381. }
  382. $col .= implode(', ', $props);
  383. }
  384. $col .= ")";
  385. $cols[] = $col;
  386. }
  387. $out .= implode(",\n", $cols);
  388. }
  389. $out .= "\n\t);\n\n";
  390. return $out;
  391. }
  392. /**
  393. * Compares two sets of schemas
  394. *
  395. * @param array|object $old Schema object or array
  396. * @param array|object $new Schema object or array
  397. * @return array Tables (that are added, dropped, or changed)
  398. */
  399. public function compare($old, $new = null) {
  400. if (empty($new)) {
  401. $new = $this;
  402. }
  403. if (is_array($new)) {
  404. if (isset($new['tables'])) {
  405. $new = $new['tables'];
  406. }
  407. } else {
  408. $new = $new->tables;
  409. }
  410. if (is_array($old)) {
  411. if (isset($old['tables'])) {
  412. $old = $old['tables'];
  413. }
  414. } else {
  415. $old = $old->tables;
  416. }
  417. $tables = array();
  418. foreach ($new as $table => $fields) {
  419. if ($table == 'missing') {
  420. continue;
  421. }
  422. if (!array_key_exists($table, $old)) {
  423. $tables[$table]['add'] = $fields;
  424. } else {
  425. $diff = $this->_arrayDiffAssoc($fields, $old[$table]);
  426. if (!empty($diff)) {
  427. $tables[$table]['add'] = $diff;
  428. }
  429. $diff = $this->_arrayDiffAssoc($old[$table], $fields);
  430. if (!empty($diff)) {
  431. $tables[$table]['drop'] = $diff;
  432. }
  433. }
  434. foreach ($fields as $field => $value) {
  435. if (!empty($old[$table][$field])) {
  436. $diff = $this->_arrayDiffAssoc($value, $old[$table][$field]);
  437. if (!empty($diff) && $field !== 'indexes' && $field !== 'tableParameters') {
  438. $tables[$table]['change'][$field] = $value;
  439. }
  440. }
  441. if (isset($tables[$table]['add'][$field]) && $field !== 'indexes' && $field !== 'tableParameters') {
  442. $wrapper = array_keys($fields);
  443. if ($column = array_search($field, $wrapper)) {
  444. if (isset($wrapper[$column - 1])) {
  445. $tables[$table]['add'][$field]['after'] = $wrapper[$column - 1];
  446. }
  447. }
  448. }
  449. }
  450. if (isset($old[$table]['indexes']) && isset($new[$table]['indexes'])) {
  451. $diff = $this->_compareIndexes($new[$table]['indexes'], $old[$table]['indexes']);
  452. if ($diff) {
  453. if (!isset($tables[$table])) {
  454. $tables[$table] = array();
  455. }
  456. if (isset($diff['drop'])) {
  457. $tables[$table]['drop']['indexes'] = $diff['drop'];
  458. }
  459. if ($diff && isset($diff['add'])) {
  460. $tables[$table]['add']['indexes'] = $diff['add'];
  461. }
  462. }
  463. }
  464. if (isset($old[$table]['tableParameters']) && isset($new[$table]['tableParameters'])) {
  465. $diff = $this->_compareTableParameters($new[$table]['tableParameters'], $old[$table]['tableParameters']);
  466. if ($diff) {
  467. $tables[$table]['change']['tableParameters'] = $diff;
  468. }
  469. }
  470. }
  471. return $tables;
  472. }
  473. /**
  474. * Extended array_diff_assoc noticing change from/to NULL values
  475. *
  476. * It behaves almost the same way as array_diff_assoc except for NULL values: if
  477. * one of the values is not NULL - change is detected. It is useful in situation
  478. * where one value is strval('') ant other is strval(null) - in string comparing
  479. * methods this results as EQUAL, while it is not.
  480. *
  481. * @param array $array1 Base array
  482. * @param array $array2 Corresponding array checked for equality
  483. * @return array Difference as array with array(keys => values) from input array
  484. * where match was not found.
  485. */
  486. protected function _arrayDiffAssoc($array1, $array2) {
  487. $difference = array();
  488. foreach ($array1 as $key => $value) {
  489. if (!array_key_exists($key, $array2)) {
  490. $difference[$key] = $value;
  491. continue;
  492. }
  493. $correspondingValue = $array2[$key];
  494. if (is_null($value) !== is_null($correspondingValue)) {
  495. $difference[$key] = $value;
  496. continue;
  497. }
  498. if (is_bool($value) !== is_bool($correspondingValue)) {
  499. $difference[$key] = $value;
  500. continue;
  501. }
  502. if (is_array($value) && is_array($correspondingValue)) {
  503. continue;
  504. }
  505. if ($value === $correspondingValue) {
  506. continue;
  507. }
  508. $difference[$key] = $value;
  509. }
  510. return $difference;
  511. }
  512. /**
  513. * Formats Schema columns from Model Object
  514. *
  515. * @param array $values options keys(type, null, default, key, length, extra)
  516. * @return array Formatted values
  517. */
  518. protected function _values($values) {
  519. $vals = array();
  520. if (is_array($values)) {
  521. foreach ($values as $key => $val) {
  522. if (is_array($val)) {
  523. $vals[] = "'{$key}' => array(" . implode(", ", $this->_values($val)) . ")";
  524. } else {
  525. $val = var_export($val, true);
  526. if ($val === 'NULL') {
  527. $val = 'null';
  528. }
  529. if (!is_numeric($key)) {
  530. $vals[] = "'{$key}' => {$val}";
  531. } else {
  532. $vals[] = "{$val}";
  533. }
  534. }
  535. }
  536. }
  537. return $vals;
  538. }
  539. /**
  540. * Formats Schema columns from Model Object
  541. *
  542. * @param array $Obj model object
  543. * @return array Formatted columns
  544. */
  545. protected function _columns(&$Obj) {
  546. $db = $Obj->getDataSource();
  547. $fields = $Obj->schema(true);
  548. $columns = array();
  549. foreach ($fields as $name => $value) {
  550. if ($Obj->primaryKey == $name) {
  551. $value['key'] = 'primary';
  552. }
  553. if (!isset($db->columns[$value['type']])) {
  554. 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);
  555. continue;
  556. } else {
  557. $defaultCol = $db->columns[$value['type']];
  558. if (isset($defaultCol['limit']) && $defaultCol['limit'] == $value['length']) {
  559. unset($value['length']);
  560. } elseif (isset($defaultCol['length']) && $defaultCol['length'] == $value['length']) {
  561. unset($value['length']);
  562. }
  563. unset($value['limit']);
  564. }
  565. if (isset($value['default']) && ($value['default'] === '' || $value['default'] === false)) {
  566. unset($value['default']);
  567. }
  568. if (empty($value['length'])) {
  569. unset($value['length']);
  570. }
  571. if (empty($value['key'])) {
  572. unset($value['key']);
  573. }
  574. $columns[$name] = $value;
  575. }
  576. return $columns;
  577. }
  578. /**
  579. * Compare two schema files table Parameters
  580. *
  581. * @param array $new New indexes
  582. * @param array $old Old indexes
  583. * @return mixed False on failure, or an array of parameters to add & drop.
  584. */
  585. protected function _compareTableParameters($new, $old) {
  586. if (!is_array($new) || !is_array($old)) {
  587. return false;
  588. }
  589. $change = $this->_arrayDiffAssoc($new, $old);
  590. return $change;
  591. }
  592. /**
  593. * Compare two schema indexes
  594. *
  595. * @param array $new New indexes
  596. * @param array $old Old indexes
  597. * @return mixed false on failure or array of indexes to add and drop
  598. */
  599. protected function _compareIndexes($new, $old) {
  600. if (!is_array($new) || !is_array($old)) {
  601. return false;
  602. }
  603. $add = $drop = array();
  604. $diff = $this->_arrayDiffAssoc($new, $old);
  605. if (!empty($diff)) {
  606. $add = $diff;
  607. }
  608. $diff = $this->_arrayDiffAssoc($old, $new);
  609. if (!empty($diff)) {
  610. $drop = $diff;
  611. }
  612. foreach ($new as $name => $value) {
  613. if (isset($old[$name])) {
  614. $newUnique = isset($value['unique']) ? $value['unique'] : 0;
  615. $oldUnique = isset($old[$name]['unique']) ? $old[$name]['unique'] : 0;
  616. $newColumn = $value['column'];
  617. $oldColumn = $old[$name]['column'];
  618. $diff = false;
  619. if ($newUnique != $oldUnique) {
  620. $diff = true;
  621. } elseif (is_array($newColumn) && is_array($oldColumn)) {
  622. $diff = ($newColumn !== $oldColumn);
  623. } elseif (is_string($newColumn) && is_string($oldColumn)) {
  624. $diff = ($newColumn != $oldColumn);
  625. } else {
  626. $diff = true;
  627. }
  628. if ($diff) {
  629. $drop[$name] = null;
  630. $add[$name] = $value;
  631. }
  632. }
  633. }
  634. return array_filter(compact('add', 'drop'));
  635. }
  636. /**
  637. * Trim the table prefix from the full table name, and return the prefix-less table
  638. *
  639. * @param string $prefix Table prefix
  640. * @param string $table Full table name
  641. * @return string Prefix-less table name
  642. */
  643. protected function _noPrefixTable($prefix, $table) {
  644. return preg_replace('/^' . preg_quote($prefix) . '/', '', $table);
  645. }
  646. }