PageRenderTime 50ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/Kernel/Entity/DatabaseSchema.php

https://bitbucket.org/adamus/symlike-core
PHP | 336 lines | 278 code | 39 blank | 19 comment | 91 complexity | e8c49357c62e401995d6378dd5644eba MD5 | raw file
Possible License(s): BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. namespace Core\Kernel\Entity;
  3. use Core\Kernel\Entity\SharedCache;
  4. use Core\Kernel\Driver\CacheController;
  5. class DatabaseSchema extends SharedCache
  6. {
  7. protected $_maxcache = 1440;
  8. function __construct()
  9. {
  10. $this->onCacheLimit();
  11. }
  12. protected function refreshTableList()
  13. {
  14. $this->_cached_data = array();
  15. $table_list = (array)\App::getDB()->fetch_all('SHOW TABLES');
  16. foreach( $table_list as $table ) {
  17. $table_name = current($table);
  18. $this->_cached_data[ $table_name ]= $this->commonFromDatabase( $table_name );
  19. }
  20. }
  21. protected function commonFromDatabase( $table_name )
  22. {
  23. $common = array( 'index' => array(), 'col' => array(), 'key' => array(), 'constraint'=>array(), 'engine'=>'', 'charset'=>'' );
  24. $create = \App::getDB()->fetch_all("SHOW CREATE TABLE {$table_name}");
  25. if( is_array($create) && isset($create[0]['Create Table']) ) {
  26. $create = $create[0]['Create Table'];
  27. $preg_table_name = "/^CREATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF NOT EXISTS\s+)?([^\s]+)\s+\((?:\s+)?/i";
  28. $preg_table_settings = "/\)\s+(ENGINE)=([^\s]+)(?:\s+(AUTO_INCREMENT)=([^\s]+))?(?:\s+(DEFAULT CHARSET)=([^\s]+))?$/i";
  29. $preg_table_primary = "/^(?:\s+)?PRIMARY KEY\s+\(([^\s]+)\)/i";
  30. $preg_table_key = "/^(?:\s+)?KEY\s+([^\s]+)\s+\(([^\s]+)\)/i";
  31. $preg_table_constraint = "/^(?:\s+)?CONSTRAINT\s+([^\s]+)\s+FOREIGN KEY\s+\(([^\s]+)\)\s+REFERENCES\s+([^\s]+)\s+\(([^\s]+)\)(?:\s+(ON\s+[^\s]+)\s+(RESTRICT|CASCADE|SET NULL)|$|,)(?:\s+(ON\s+[^\s]+)\s+(RESTRICT|CASCADE|SET NULL)|$|,)/i";
  32. $preg_table_col = "/^(?:\s+)?\s+([^\s]+)\s+([^\s]+)/i";
  33. $table_info = array();
  34. if( preg_match($preg_table_name, $create, $name_matches) && preg_match($preg_table_settings, $create, $settings_matches) ) {
  35. for( $i=1; $i<count($settings_matches); $i+=2 ) {
  36. switch($settings_matches[$i]) {
  37. case 'ENGINE': $common['engine'] = $settings_matches[$i+1]; break;
  38. case 'DEFAULT CHARSET': $common['charset'] =$settings_matches[$i+1]; break;
  39. }
  40. }
  41. foreach( explode("\n",trim(substr($create, strpos($create,'(')+1, strrpos($create,')')-strpos($create,'(')-1 ))) as $table_col ) {
  42. if( preg_match($preg_table_constraint, $table_col, $matches) ) {
  43. $common['constraint'][ $name=str_replace(array('`','"',"'"),'',$matches[1]) ]= array(
  44. 'foreign' => str_replace(array('`','"',"'"),'',$matches[2]),
  45. 'reftable' => str_replace(array('`','"',"'"),'',$matches[3]),
  46. 'refcolumns' => explode(',',str_replace(array('`','"',"'"),'',$matches[4]))
  47. );
  48. if( isset($matches[5]) && isset($matches[6]) && $matches[5] == 'ON DELETE' ) {
  49. if( isset($matches[7]) && isset($matches[8]) && $matches[7] == 'ON UPDATE' ) {
  50. $common['constraint'][ $name ]['onupdate'] = $matches[8];
  51. }
  52. $common['constraint'][ $name ]['ondelete'] = $matches[6];
  53. } elseif( isset($matches[5]) && isset($matches[6]) && $matches[5] == 'ON UPDATE' ) {
  54. $common['constraint'][ $name ]['onupdate'] = $matches[6];
  55. if( isset($matches[7]) && isset($matches[8]) && $matches[7] == 'ON DELETE' ) {
  56. $common['constraint'][ $name ]['ondelete'] = $matches[8];
  57. }
  58. }
  59. ksort($common['constraint'][ $name ]);
  60. }
  61. }
  62. ksort($common['constraint']);
  63. }
  64. }
  65. foreach( (array)\App::getDB()->fetch_all( "SHOW INDEX FROM {$table_name}" ) as $key ) {
  66. $type = $key['Non_unique'] == '0' ? 'UNIQUE' : 'INDEX';
  67. if( !isset($common['index'][ $key['Key_name'] ])) {
  68. $common['index'][ $key['Key_name'] ] = array(
  69. 'type' => $type,
  70. 'columns' => array($key['Column_name'])
  71. );
  72. } else {
  73. $common['index'][ $key['Key_name'] ]['columns'][]=$key['Column_name'];
  74. }
  75. if( $type == 'UNIQUE' || $key['Key_name'] == 'PRIMARY' ) {
  76. $common['key'][]=$key['Column_name'];
  77. }
  78. }
  79. foreach( (array)\App::getDB()->fetch_all( "DESCRIBE {$table_name}" ) as $col ) {
  80. $nullable = $col['Null'] != 'NO';
  81. $common['col'][ $col['Field'] ] = array(
  82. 'type' => $col['Type'],
  83. 'null' => $nullable ? ' NULL' : ' NOT NULL',
  84. 'default' => $col['Default'] !== NULL ? " DEFAULT '{$col['Default']}'" : ( $nullable ? ' DEFAULT NULL' : ''),
  85. 'extra' => empty($col['Extra']) ? '' : " {$col['Extra']}"
  86. );
  87. }
  88. return $common;
  89. }
  90. function onCacheLimit()
  91. {
  92. $this->_cached = time();
  93. $this->refreshTableList();
  94. \Core\Kernel\Driver\CacheController::putSharedObject( $this );
  95. }
  96. public function describe( $table )
  97. {
  98. if( isset($this->_cached_data[ (string)$table ]) ) {
  99. return $this->_cached_data[ (string)$table ];
  100. } else {
  101. throw new \Exception("TABLE NOT FOUND");
  102. }
  103. }
  104. public function schemaUpdate()
  105. {
  106. $this->refreshTableList();
  107. $result = "";
  108. foreach( $this->collectSchemas() as $table ) {
  109. try {
  110. $current = $this->describe($table['name']);
  111. $result .= $this->updateTable( $current, $table );
  112. } catch( \Exception $e ) {
  113. $result .= $this->createTable( $table );
  114. }
  115. }
  116. if( !empty($result) ) $this->refreshTableList();
  117. return $result;
  118. }
  119. /**
  120. * @param array $current from database
  121. * @param SimpleXmlElement $table from schema file
  122. */
  123. protected function updateTable( $current, $table )
  124. {
  125. $result = "";
  126. if( strtolower($current['engine']) != strtolower($table['engine']) ) {
  127. $result .= $SQL = "ALTER TABLE `{$table['name']}` ENGINE = {$table['engine']}\n";
  128. \App::getDB()->exec($SQL);
  129. }
  130. if( strtolower($current['charset']) != strtolower($table['charset']) ) {
  131. $charset = strtolower($table['charset']);
  132. $result .= $SQL = "ALTER TABLE `{$table['name']}` CONVERT TO CHARACTER SET {$charset}\n";
  133. \App::getDB()->exec($SQL);
  134. }
  135. $schema_keys = array();
  136. foreach( $table->xpath("key") as $key ) {
  137. $schema_keys[ (string)$key['name'] ] = array(
  138. 'type' => isset($key['unique']) && $key['unique'] == 'true' ? 'UNIQUE' : 'INDEX',
  139. 'columns' => explode(',', (string)$key['columns'])
  140. );
  141. }
  142. $schema_constraint = array();
  143. foreach( $table->xpath("constraint") as $constraint ) {
  144. $schema_constraint[ (string)$constraint['name'] ] = array(
  145. 'foreign' => (string)$constraint['foreign'],
  146. 'reftable' => (string)$constraint['reftable'],
  147. 'refcolumns' => explode(',',(string)$constraint['refcolumns'])
  148. );
  149. if( isset($constraint['onupdate']) && !empty($constraint['onupdate']) ) {
  150. $schema_constraint[ (string)$constraint['name'] ]['onupdate'] = (string)$constraint['onupdate'];
  151. }
  152. if( isset($constraint['ondelete']) && !empty($constraint['ondelete']) ) {
  153. $schema_constraint[ (string)$constraint['name'] ]['ondelete'] = (string)$constraint['ondelete'];
  154. }
  155. ksort($schema_constraint[ (string)$constraint['name'] ]);
  156. }
  157. ksort($schema_constraint);
  158. /*
  159. * Compare current - defined indexes/keys and update on difference.
  160. */
  161. $keys_mismatch = md5(serialize($current['index'])) != md5(serialize($schema_keys));
  162. if( $keys_mismatch ) {
  163. foreach( $current['index'] as $name=>$key ) {
  164. if( !isset($schema_keys[ $name ]) || md5(serialize($key)) != md5(serialize($schema_keys[ $name ])) ) {
  165. if( $name == 'PRIMARY' ) {
  166. $result .= $SQL = "ALTER TABLE {$table['name']} DROP PRIMARY KEY\n";
  167. \App::getDB()->exec($SQL);
  168. } else {
  169. $result .= $SQL = "ALTER TABLE `{$table['name']}` DROP INDEX `{$name}`\n";
  170. \App::getDB()->exec($SQL);
  171. }
  172. } else {
  173. unset( $schema_keys[ $name ] ); // If the current index matches with the schema, then delete it from sync-queue.
  174. }
  175. }
  176. }
  177. /*
  178. * Compare current - defined foreign keys and update on difference.
  179. */
  180. $constraint_mismatch = md5(serialize($current['constraint'])) != md5(serialize($schema_constraint));
  181. if( $constraint_mismatch ) {
  182. foreach( $current['constraint'] as $name => $constraint ) {
  183. if( !isset($schema_constraint[ $name ]) || md5(serialize($constraint)) != md5(serialize($schema_constraint[ $name ])) ) {
  184. $result .= $SQL = "ALTER TABLE {$table['name']} DROP FOREIGN KEY `{$name}`\n";
  185. \App::getDB()->exec($SQL);
  186. } else {
  187. unset( $schema_constraint[ $name ] );
  188. }
  189. }
  190. }
  191. /*
  192. * Compare current - defined columns and update on difference.
  193. */
  194. $schema_columns = array();
  195. foreach( $table->xpath("column") as $col ) {
  196. $nullable = isset($col['nullable']) && $col['nullable'] == 'true';
  197. $schema_columns[ (string)$col['name'] ] = array(
  198. 'type' => (string)$col['type'],
  199. 'null' => $nullable ? ' NULL' : ' NOT NULL',
  200. 'default' => isset($col['default']) ? " DEFAULT '{$col['default']}'" : ( $nullable ? ' DEFAULT NULL' : ''),
  201. 'extra' => isset($col['extra']) ? " {$col['extra']}" : ''
  202. );
  203. }
  204. if( md5(serialize($current['col'])) != md5(serialize($schema_columns)) ) {
  205. $AFTER = "FIRST";
  206. foreach( $schema_columns as $name => $col ) {
  207. if( !isset($current['col'][ $name ]) ) {
  208. $result .= $SQL = "ALTER TABLE `{$table['name']}` ADD `{$name}` {$col['type']}{$col['null']}{$col['default']}{$col['extra']} {$AFTER};\n";
  209. \App::getDB()->exec($SQL);
  210. } elseif( md5(serialize($current['col'][ $name ])) != md5(serialize($schema_columns[ $name ])) ) {
  211. $result .= $SQL = "ALTER TABLE `{$table['name']}` CHANGE `{$name}` `{$name}` {$col['type']}{$col['null']}{$col['default']}{$col['extra']};\n";
  212. \App::getDB()->exec($SQL);
  213. }
  214. $AFTER = "AFTER `{$name}`";
  215. }
  216. }
  217. /*
  218. * Re-add keys.
  219. */
  220. if( $keys_mismatch ) {
  221. foreach( $schema_keys as $name=>$key ) {
  222. $type = $name == 'PRIMARY' ? 'PRIMARY KEY' : "{$key['type']} `{$name}`";
  223. $result .= $SQL = "ALTER TABLE `{$table['name']}` ADD {$type} (`" . implode('`,`',$key['columns'] ) ."`);\n" ;
  224. \App::getDB()->exec($SQL);
  225. }
  226. }
  227. /*
  228. * Re-add constraints.
  229. */
  230. if( $constraint_mismatch ) {
  231. foreach( $schema_constraint as $name=>$constraint) {
  232. $ondelete = isset($constraint['ondelete']) ? " ON DELETE ".strtoupper($constraint['ondelete']) : '';
  233. $onupdate = isset($constraint['onupdate']) ? " ON UPDATE ".strtoupper($constraint['onupdate']) : '';
  234. $result .= $SQL = "ALTER TABLE `{$table['name']}` ADD CONSTRAINT `{$name}` FOREIGN KEY (`{$constraint['foreign']}`) REFERENCES `{$constraint['reftable']}` (`" . implode('`,`',$constraint['refcolumns'] ) ."`){$onupdate}{$ondelete};\n" ;
  235. \App::getDB()->exec($SQL);
  236. }
  237. }
  238. return $result;
  239. }
  240. protected function createTable( $table )
  241. {
  242. $COLUMNS=array();
  243. foreach( $table->xpath("column") as $column ) {
  244. $default = isset($column['default']) ? " DEFAULT '{$column['default']}'" : '';
  245. $nullable = isset($column['nullable']) && $column['nullable'] == 'true' ? '' : " NOT NULL";
  246. $extra = isset($column['extra']) ? " {$column['extra']}" : '';
  247. $COLUMNS []= " \n`{$column['name']}` {$column['type']}{$nullable}{$default}{$extra}";
  248. }
  249. $KEYS="";
  250. foreach( $table->xpath("key") as $key ) {
  251. $key_cols = '`' . implode('`,`', explode(',', $key['columns']) ) .'`';
  252. if( $key['name'] == 'PRIMARY' ) {
  253. $KEYS .= ", \nPRIMARY KEY ({$key_cols})";
  254. } else {
  255. $unique = isset($key['unique']) && $key['unique'] == 'true' ? 'UNIQUE ' : '';
  256. $KEYS .= ", \n{$unique}KEY `{$key['name']}` ({$key_cols})";
  257. }
  258. }
  259. foreach( $table->xpath("constraint") as $constraint ) {
  260. $KEYS .= ", \nCONSTRAINT `{$constraint['name']}` FOREIGN KEY ({$constraint['foreign']}) REFERENCES `{$constraint['reftable']}` ({$constraint['refcolumns']})";
  261. if( isset($constraint['onupdate']) && !empty($constraint['onupdate']) ) {
  262. $KEYS.=" ON UPDATE {$constraint['onupdate']}";
  263. }
  264. if( isset($constraint['ondelete']) && !empty($constraint['ondelete']) ) {
  265. $KEYS.=" ON DELETE {$constraint['onupdate']}";
  266. }
  267. }
  268. $SQL = "CREATE TABLE IF NOT EXISTS `{$table['name']}` (".implode(',',$COLUMNS)."{$KEYS}\n) ENGINE={$table['engine']} DEFAULT CHARSET={$table['charset']}\n";
  269. \App::getDB()->exec($SQL);
  270. return $SQL;
  271. }
  272. protected function collectSchemas()
  273. {
  274. $tables = array();
  275. $dir = \App::getDocumentRoot() . 'modules';
  276. if ( $modules = opendir( $dir ) ) {
  277. while (false !== ($module = readdir($modules))) { // Each module
  278. if( substr($module, 0, 1) == '.' || !is_dir($module_dir =$dir . "/{$module}") ) continue;
  279. if ($submodules = opendir( $module_dir )) {
  280. while (false !== ($submodule = readdir($submodules))) { // Each submodule
  281. if( substr($submodule, 0, 1) == '.' || !is_dir($schema_dir = "{$module_dir}/{$submodule}/Resources/Schema") ) continue;
  282. if ( $schemas = opendir( $schema_dir )) {
  283. while (false !== ($schema_file = readdir($schemas))) { // Each schema file
  284. if( substr($schema_file, 0, 1) == '.' ) continue;
  285. $tables[]=simplexml_load_file("{$schema_dir}/{$schema_file}");
  286. }
  287. closedir($schemas);
  288. }
  289. }
  290. }
  291. closedir($submodules);
  292. }
  293. closedir($modules);
  294. }
  295. return $tables;
  296. }
  297. }