PageRenderTime 51ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Cake/Model/CakeSchema.php

https://bitbucket.org/tlorens/cakefoundation
PHP | 710 lines | 477 code | 72 blank | 161 comment | 137 complexity | cf9722ba343af230eb9a5486ef03c4fb MD5 | raw file
  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. $db = $Object->getDataSource();
  226. if (is_object($Object) && $Object->useTable !== false) {
  227. $fulltable = $table = $db->fullTableName($Object, false, false);
  228. if ($prefix && strpos($table, $prefix) !== 0) {
  229. continue;
  230. }
  231. $table = $this->_noPrefixTable($prefix, $table);
  232. if (in_array($fulltable, $currentTables)) {
  233. $key = array_search($fulltable, $currentTables);
  234. if (empty($tables[$table])) {
  235. $tables[$table] = $this->_columns($Object);
  236. $tables[$table]['indexes'] = $db->index($Object);
  237. $tables[$table]['tableParameters'] = $db->readTableParameters($fulltable);
  238. unset($currentTables[$key]);
  239. }
  240. if (!empty($Object->hasAndBelongsToMany)) {
  241. foreach ($Object->hasAndBelongsToMany as $assocData) {
  242. if (isset($assocData['with'])) {
  243. $class = $assocData['with'];
  244. }
  245. if (is_object($Object->$class)) {
  246. $withTable = $db->fullTableName($Object->$class, false, false);
  247. if ($prefix && strpos($withTable, $prefix) !== 0) {
  248. continue;
  249. }
  250. if (in_array($withTable, $currentTables)) {
  251. $key = array_search($withTable, $currentTables);
  252. $noPrefixWith = $this->_noPrefixTable($prefix, $withTable);
  253. $tables[$noPrefixWith] = $this->_columns($Object->$class);
  254. $tables[$noPrefixWith]['indexes'] = $db->index($Object->$class);
  255. $tables[$noPrefixWith]['tableParameters'] = $db->readTableParameters($withTable);
  256. unset($currentTables[$key]);
  257. }
  258. }
  259. }
  260. }
  261. }
  262. }
  263. }
  264. }
  265. if (!empty($currentTables)) {
  266. foreach ($currentTables as $table) {
  267. if ($prefix) {
  268. if (strpos($table, $prefix) !== 0) {
  269. continue;
  270. }
  271. $table = $this->_noPrefixTable($prefix, $table);
  272. }
  273. $Object = new AppModel(array(
  274. 'name' => Inflector::classify($table), 'table' => $table, 'ds' => $connection
  275. ));
  276. $systemTables = array(
  277. 'aros', 'acos', 'aros_acos', Configure::read('Session.table'), 'i18n'
  278. );
  279. $fulltable = $db->fullTableName($Object, false, false);
  280. if (in_array($table, $systemTables)) {
  281. $tables[$Object->table] = $this->_columns($Object);
  282. $tables[$Object->table]['indexes'] = $db->index($Object);
  283. $tables[$Object->table]['tableParameters'] = $db->readTableParameters($fulltable);
  284. } elseif ($models === false) {
  285. $tables[$table] = $this->_columns($Object);
  286. $tables[$table]['indexes'] = $db->index($Object);
  287. $tables[$table]['tableParameters'] = $db->readTableParameters($fulltable);
  288. } else {
  289. $tables['missing'][$table] = $this->_columns($Object);
  290. $tables['missing'][$table]['indexes'] = $db->index($Object);
  291. $tables['missing'][$table]['tableParameters'] = $db->readTableParameters($fulltable);
  292. }
  293. }
  294. }
  295. ksort($tables);
  296. return compact('name', 'tables');
  297. }
  298. /**
  299. * Writes schema file from object or options
  300. *
  301. * @param array|object $object schema object or options array
  302. * @param array $options schema object properties to override object
  303. * @return mixed false or string written to file
  304. */
  305. public function write($object, $options = array()) {
  306. if (is_object($object)) {
  307. $object = get_object_vars($object);
  308. $this->build($object);
  309. }
  310. if (is_array($object)) {
  311. $options = $object;
  312. unset($object);
  313. }
  314. extract(array_merge(
  315. get_object_vars($this), $options
  316. ));
  317. $out = "class {$name}Schema extends CakeSchema {\n\n";
  318. if ($path !== $this->path) {
  319. $out .= "\tpublic \$path = '{$path}';\n\n";
  320. }
  321. if ($file !== $this->file) {
  322. $out .= "\tpublic \$file = '{$file}';\n\n";
  323. }
  324. if ($connection !== 'default') {
  325. $out .= "\tpublic \$connection = '{$connection}';\n\n";
  326. }
  327. $out .= "\tpublic function before(\$event = array()) {\n\t\treturn true;\n\t}\n\n\tpublic function after(\$event = array()) {\n\t}\n\n";
  328. if (empty($tables)) {
  329. $this->read();
  330. }
  331. foreach ($tables as $table => $fields) {
  332. if (!is_numeric($table) && $table !== 'missing') {
  333. $out .= $this->generateTable($table, $fields);
  334. }
  335. }
  336. $out .= "}\n";
  337. $file = new File($path . DS . $file, true);
  338. $content = "<?php \n{$out}";
  339. if ($file->write($content)) {
  340. return $content;
  341. }
  342. return false;
  343. }
  344. /**
  345. * Generate the code for a table. Takes a table name and $fields array
  346. * Returns a completed variable declaration to be used in schema classes
  347. *
  348. * @param string $table Table name you want returned.
  349. * @param array $fields Array of field information to generate the table with.
  350. * @return string Variable declaration for a schema class
  351. */
  352. public function generateTable($table, $fields) {
  353. $out = "\tpublic \${$table} = array(\n";
  354. if (is_array($fields)) {
  355. $cols = array();
  356. foreach ($fields as $field => $value) {
  357. if ($field != 'indexes' && $field != 'tableParameters') {
  358. if (is_string($value)) {
  359. $type = $value;
  360. $value = array('type' => $type);
  361. }
  362. $col = "\t\t'{$field}' => array('type' => '" . $value['type'] . "', ";
  363. unset($value['type']);
  364. $col .= join(', ', $this->_values($value));
  365. } elseif ($field == 'indexes') {
  366. $col = "\t\t'indexes' => array(\n\t\t\t";
  367. $props = array();
  368. foreach ((array)$value as $key => $index) {
  369. $props[] = "'{$key}' => array(" . join(', ', $this->_values($index)) . ")";
  370. }
  371. $col .= join(",\n\t\t\t", $props) . "\n\t\t";
  372. } elseif ($field == 'tableParameters') {
  373. $col = "\t\t'tableParameters' => array(";
  374. $props = array();
  375. foreach ((array)$value as $key => $param) {
  376. $props[] = "'{$key}' => '$param'";
  377. }
  378. $col .= join(', ', $props);
  379. }
  380. $col .= ")";
  381. $cols[] = $col;
  382. }
  383. $out .= join(",\n", $cols);
  384. }
  385. $out .= "\n\t);\n";
  386. return $out;
  387. }
  388. /**
  389. * Compares two sets of schemas
  390. *
  391. * @param array|object $old Schema object or array
  392. * @param array|object $new Schema object or array
  393. * @return array Tables (that are added, dropped, or changed)
  394. */
  395. public function compare($old, $new = null) {
  396. if (empty($new)) {
  397. $new = $this;
  398. }
  399. if (is_array($new)) {
  400. if (isset($new['tables'])) {
  401. $new = $new['tables'];
  402. }
  403. } else {
  404. $new = $new->tables;
  405. }
  406. if (is_array($old)) {
  407. if (isset($old['tables'])) {
  408. $old = $old['tables'];
  409. }
  410. } else {
  411. $old = $old->tables;
  412. }
  413. $tables = array();
  414. foreach ($new as $table => $fields) {
  415. if ($table == 'missing') {
  416. continue;
  417. }
  418. if (!array_key_exists($table, $old)) {
  419. $tables[$table]['add'] = $fields;
  420. } else {
  421. $diff = $this->_arrayDiffAssoc($fields, $old[$table]);
  422. if (!empty($diff)) {
  423. $tables[$table]['add'] = $diff;
  424. }
  425. $diff = $this->_arrayDiffAssoc($old[$table], $fields);
  426. if (!empty($diff)) {
  427. $tables[$table]['drop'] = $diff;
  428. }
  429. }
  430. foreach ($fields as $field => $value) {
  431. if (!empty($old[$table][$field])) {
  432. $diff = $this->_arrayDiffAssoc($value, $old[$table][$field]);
  433. if (!empty($diff) && $field !== 'indexes' && $field !== 'tableParameters') {
  434. $tables[$table]['change'][$field] = $value;
  435. }
  436. }
  437. if (isset($tables[$table]['add'][$field]) && $field !== 'indexes' && $field !== 'tableParameters') {
  438. $wrapper = array_keys($fields);
  439. if ($column = array_search($field, $wrapper)) {
  440. if (isset($wrapper[$column - 1])) {
  441. $tables[$table]['add'][$field]['after'] = $wrapper[$column - 1];
  442. }
  443. }
  444. }
  445. }
  446. if (isset($old[$table]['indexes']) && isset($new[$table]['indexes'])) {
  447. $diff = $this->_compareIndexes($new[$table]['indexes'], $old[$table]['indexes']);
  448. if ($diff) {
  449. if (!isset($tables[$table])) {
  450. $tables[$table] = array();
  451. }
  452. if (isset($diff['drop'])) {
  453. $tables[$table]['drop']['indexes'] = $diff['drop'];
  454. }
  455. if ($diff && isset($diff['add'])) {
  456. $tables[$table]['add']['indexes'] = $diff['add'];
  457. }
  458. }
  459. }
  460. if (isset($old[$table]['tableParameters']) && isset($new[$table]['tableParameters'])) {
  461. $diff = $this->_compareTableParameters($new[$table]['tableParameters'], $old[$table]['tableParameters']);
  462. if ($diff) {
  463. $tables[$table]['change']['tableParameters'] = $diff;
  464. }
  465. }
  466. }
  467. return $tables;
  468. }
  469. /**
  470. * Extended array_diff_assoc noticing change from/to NULL values
  471. *
  472. * It behaves almost the same way as array_diff_assoc except for NULL values: if
  473. * one of the values is not NULL - change is detected. It is useful in situation
  474. * where one value is strval('') ant other is strval(null) - in string comparing
  475. * methods this results as EQUAL, while it is not.
  476. *
  477. * @param array $array1 Base array
  478. * @param array $array2 Corresponding array checked for equality
  479. * @return array Difference as array with array(keys => values) from input array
  480. * where match was not found.
  481. */
  482. protected function _arrayDiffAssoc($array1, $array2) {
  483. $difference = array();
  484. foreach ($array1 as $key => $value) {
  485. if (!array_key_exists($key, $array2)) {
  486. $difference[$key] = $value;
  487. continue;
  488. }
  489. $correspondingValue = $array2[$key];
  490. if (is_null($value) !== is_null($correspondingValue)) {
  491. $difference[$key] = $value;
  492. continue;
  493. }
  494. if (is_bool($value) !== is_bool($correspondingValue)) {
  495. $difference[$key] = $value;
  496. continue;
  497. }
  498. if (is_array($value) && is_array($correspondingValue)) {
  499. continue;
  500. }
  501. if ($value === $correspondingValue) {
  502. continue;
  503. }
  504. $difference[$key] = $value;
  505. }
  506. return $difference;
  507. }
  508. /**
  509. * Formats Schema columns from Model Object
  510. *
  511. * @param array $values options keys(type, null, default, key, length, extra)
  512. * @return array Formatted values
  513. */
  514. protected function _values($values) {
  515. $vals = array();
  516. if (is_array($values)) {
  517. foreach ($values as $key => $val) {
  518. if (is_array($val)) {
  519. $vals[] = "'{$key}' => array('" . implode("', '", $val) . "')";
  520. } elseif (!is_numeric($key)) {
  521. $val = var_export($val, true);
  522. if ($val === 'NULL') {
  523. $val = 'null';
  524. }
  525. $vals[] = "'{$key}' => {$val}";
  526. }
  527. }
  528. }
  529. return $vals;
  530. }
  531. /**
  532. * Formats Schema columns from Model Object
  533. *
  534. * @param array $Obj model object
  535. * @return array Formatted columns
  536. */
  537. protected function _columns(&$Obj) {
  538. $db = $Obj->getDataSource();
  539. $fields = $Obj->schema(true);
  540. $columns = array();
  541. foreach ($fields as $name => $value) {
  542. if ($Obj->primaryKey == $name) {
  543. $value['key'] = 'primary';
  544. }
  545. if (!isset($db->columns[$value['type']])) {
  546. 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);
  547. continue;
  548. } else {
  549. $defaultCol = $db->columns[$value['type']];
  550. if (isset($defaultCol['limit']) && $defaultCol['limit'] == $value['length']) {
  551. unset($value['length']);
  552. } elseif (isset($defaultCol['length']) && $defaultCol['length'] == $value['length']) {
  553. unset($value['length']);
  554. }
  555. unset($value['limit']);
  556. }
  557. if (isset($value['default']) && ($value['default'] === '' || $value['default'] === false)) {
  558. unset($value['default']);
  559. }
  560. if (empty($value['length'])) {
  561. unset($value['length']);
  562. }
  563. if (empty($value['key'])) {
  564. unset($value['key']);
  565. }
  566. $columns[$name] = $value;
  567. }
  568. return $columns;
  569. }
  570. /**
  571. * Compare two schema files table Parameters
  572. *
  573. * @param array $new New indexes
  574. * @param array $old Old indexes
  575. * @return mixed False on failure, or an array of parameters to add & drop.
  576. */
  577. protected function _compareTableParameters($new, $old) {
  578. if (!is_array($new) || !is_array($old)) {
  579. return false;
  580. }
  581. $change = $this->_arrayDiffAssoc($new, $old);
  582. return $change;
  583. }
  584. /**
  585. * Compare two schema indexes
  586. *
  587. * @param array $new New indexes
  588. * @param array $old Old indexes
  589. * @return mixed false on failure or array of indexes to add and drop
  590. */
  591. protected function _compareIndexes($new, $old) {
  592. if (!is_array($new) || !is_array($old)) {
  593. return false;
  594. }
  595. $add = $drop = array();
  596. $diff = $this->_arrayDiffAssoc($new, $old);
  597. if (!empty($diff)) {
  598. $add = $diff;
  599. }
  600. $diff = $this->_arrayDiffAssoc($old, $new);
  601. if (!empty($diff)) {
  602. $drop = $diff;
  603. }
  604. foreach ($new as $name => $value) {
  605. if (isset($old[$name])) {
  606. $newUnique = isset($value['unique']) ? $value['unique'] : 0;
  607. $oldUnique = isset($old[$name]['unique']) ? $old[$name]['unique'] : 0;
  608. $newColumn = $value['column'];
  609. $oldColumn = $old[$name]['column'];
  610. $diff = false;
  611. if ($newUnique != $oldUnique) {
  612. $diff = true;
  613. } elseif (is_array($newColumn) && is_array($oldColumn)) {
  614. $diff = ($newColumn !== $oldColumn);
  615. } elseif (is_string($newColumn) && is_string($oldColumn)) {
  616. $diff = ($newColumn != $oldColumn);
  617. } else {
  618. $diff = true;
  619. }
  620. if ($diff) {
  621. $drop[$name] = null;
  622. $add[$name] = $value;
  623. }
  624. }
  625. }
  626. return array_filter(compact('add', 'drop'));
  627. }
  628. /**
  629. * Trim the table prefix from the full table name, and return the prefix-less table
  630. *
  631. * @param string $prefix Table prefix
  632. * @param string $table Full table name
  633. * @return string Prefix-less table name
  634. */
  635. protected function _noPrefixTable($prefix, $table) {
  636. return preg_replace('/^' . preg_quote($prefix) . '/', '', $table);
  637. }
  638. }