PageRenderTime 57ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Cake/Model/CakeSchema.php

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