upgrade-mysql-1.1.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. <?php
  2. /*
  3. * Copyright 2010-2015 by MODX, LLC.
  4. *
  5. * This file is part of xPDO.
  6. *
  7. * xPDO is free software; you can redistribute it and/or modify it under the
  8. * terms of the GNU General Public License as published by the Free Software
  9. * Foundation; either version 2 of the License, or (at your option) any later
  10. * version.
  11. *
  12. * xPDO is distributed in the hope that it will be useful, but WITHOUT ANY
  13. * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  14. * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License along with
  17. * xPDO; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
  18. * Suite 330, Boston, MA 02111-1307 USA
  19. */
  20. /**
  21. * Script that upgrades xPDO mysql models from version 1.0 to the 1.1 format.
  22. *
  23. * Can be run from CLI or web, accepting command-line arguments or $_REQUEST
  24. * variables to set the arguments.
  25. *
  26. * @package xpdo
  27. * @subpackage tools
  28. */
  29. $scriptTitle = basename(__FILE__, '.php');
  30. /**#@+
  31. * Arguments
  32. */
  33. /**
  34. * @var string The xPDO root path, where the xpdo.class.php file is located.
  35. */
  36. $xpdo_path= realpath(dirname(dirname(__DIR__))) . DIRECTORY_SEPARATOR;
  37. /**
  38. * @var string The name of the model package.
  39. */
  40. $pkg= '';
  41. /**
  42. * @var string The model root path.
  43. */
  44. $pkg_path= realpath(dirname($xpdo_path) . '/model') . DIRECTORY_SEPARATOR;
  45. /**
  46. * @var string The xPDO model schema filename.
  47. */
  48. $schema_name= '';
  49. /**
  50. * @var string The path to the model schema file.
  51. */
  52. $schema_path= realpath(dirname($xpdo_path) . '/schema') . DIRECTORY_SEPARATOR;
  53. /**
  54. * @var string The path to write the backup schema file.
  55. */
  56. $backup_path= '';
  57. /**
  58. * @var string A string to prepend to the backup file.
  59. */
  60. $backup_prefix= '~';
  61. /**
  62. * @var string A valid PDO DSN connection string.
  63. */
  64. $dsn= 'mysql:host=localhost';
  65. /**
  66. * @var string A valid database user name.
  67. */
  68. $dbuser= 'root';
  69. /**
  70. * @var string The password for the database user.
  71. */
  72. $dbpass= '';
  73. /**
  74. * @var boolean The debug setting for the script.
  75. */
  76. $debug= false;
  77. /**
  78. * @var integer The xPDO log level to use. Note 2 is LOG_LEVEL_WARN, 2 is LOG_LEVEL_INFO
  79. */
  80. $log_level= 2;
  81. /**
  82. * @var integer The PHP error_reporting level to use.
  83. */
  84. $error_reporting= -1;
  85. /**
  86. * @var boolean Indicates the PHP display_errors setting to use.
  87. */
  88. $display_errors= true;
  89. /**
  90. * @var boolean If true, will regenerate the model after the schema is updated.
  91. */
  92. $regen= false;
  93. /**
  94. * @var boolean If true, will write the updated schema changes to file, and backup the original.
  95. */
  96. $write= false;
  97. /**
  98. * @var boolean If true, will echo the updated schema.
  99. */
  100. $echo= false;
  101. /**
  102. * @var string An external file to include argument values from; will be overridden
  103. * by CLI or $_REQUEST arguments.
  104. */
  105. $include= realpath(dirname(__FILE__) . DIRECTORY_SEPARATOR . "{$scriptTitle}.properties");
  106. $defaultInclude= true;
  107. $properties= array();
  108. if (!empty($argv) && $argc > 1) {
  109. /* process CLI arguments */
  110. reset($argv);
  111. while ($argument = next($argv)) {
  112. if (strpos($argument, '=') > 0) {
  113. $arg = explode('=', $argument);
  114. $argKey = trim($arg[0], '-');
  115. $argValue = trim($arg[1], '"');
  116. if (strpos($argKey, '_path') > 0) {
  117. $argValue = realpath($argValue) . DIRECTORY_SEPARATOR;
  118. }
  119. if ($argKey == 'include') {
  120. $argValue = realpath($argValue);
  121. $defaultInclude= false;
  122. }
  123. $properties[$argKey] = $argValue;
  124. } else {
  125. $properties[trim($argument, '-')] = true;
  126. }
  127. }
  128. } elseif (!empty($_REQUEST)) {
  129. /* process $_REQUEST arguments */
  130. foreach ($_REQUEST as $argKey => $argValue) {
  131. if (in_array(strtolower($argValue), array('true','false'))) {
  132. $argValue = $argValue === 'true' ? true : false;
  133. }
  134. if (strpos($argKey, '_path') > 0) {
  135. $argValue = realpath($argValue) . DIRECTORY_SEPARATOR;
  136. }
  137. if ($argKey == 'include') {
  138. $argValue = realpath($argValue);
  139. $defaultInclude= false;
  140. }
  141. $properties[$argKey] = $argValue;
  142. }
  143. }
  144. /* load external properties from default or specified file */
  145. if (!empty($include) && file_exists($include)) {
  146. if (!include ($include)) {
  147. die("[{$scriptTitle}] FATAL: Error loading " . ($defaultInclude ? " default " : "") . "external properties file --include={$include}.\n");
  148. }
  149. } elseif (!empty($include) && !$defaultInclude) {
  150. die("[{$scriptTitle}] FATAL: External properties file not found --include={$include}.\n");
  151. }
  152. extract($properties, EXTR_OVERWRITE);
  153. if (empty($pkg)) {
  154. die("[{$scriptTitle}] FATAL: No valid pkg was specified.\n");
  155. }
  156. if (empty($pkg_path)) {
  157. die("[{$scriptTitle}] FATAL: No pkg_path was specified.\n");
  158. }
  159. if (empty($schema_name)) {
  160. die("[{$scriptTitle}] FATAL: No schema_name was specified.\n");
  161. }
  162. if (empty($schema_path)) {
  163. die("[{$scriptTitle}] FATAL: No schema_path was specified.\n");
  164. }
  165. if (empty($echo) && empty($write) && empty($regen) && $debug !== true) {
  166. die("[{$scriptTitle}] FATAL: At least one of the options --echo, --write, --regen, or --debug must be set.\n");
  167. }
  168. if ($debug === true && !empty($write)) {
  169. die("[{$scriptTitle}] FATAL: --write cannot be used when --debug is set.\n");
  170. }
  171. if (!file_exists("{$xpdo_path}xpdo.class.php")) {
  172. die("[{$scriptTitle}] FATAL: xPDO class not found; invalid xpdo_path: {$xpdo_path}.\n");
  173. }
  174. include_once ("{$xpdo_path}xpdo.class.php");
  175. $xpdo= new xPDO($dsn, $dbuser, $dbpass);
  176. if (!is_object($xpdo)) {
  177. die("[{$scriptTitle}] FATAL: Error getting instance of xPDO object.");
  178. }
  179. $xpdo->setLogTarget(XPDO_CLI_MODE ? 'ECHO' : 'HTML');
  180. $xpdo->setLogLevel($log_level);
  181. if (!empty($debug)) {
  182. if ($debug === true && empty($echo)) {
  183. $echo = true;
  184. }
  185. $xpdo->setDebug($debug);
  186. }
  187. error_reporting($error_reporting);
  188. ini_set('display_errors', (boolean) $display_errors);
  189. $xpdo->setPackage($pkg, $pkg_path);
  190. $xpdo->getCacheManager();
  191. if (!file_exists("{$schema_path}{$schema_name}")) {
  192. $xpdo->log(xPDO::LOG_LEVEL_FATAL, "Could not find schema: {$schema_path}{$schema_file}");
  193. }
  194. /* load the schema file as a DOMDocument */
  195. $schema = new DOMDocument();
  196. $loaded = $schema->load("{$schema_path}{$schema_name}");
  197. if ($loaded === false) {
  198. $xpdo->log(xPDO::LOG_LEVEL_FATAL, "Error loading schema file: {$schema_path}{$schema_file}");
  199. }
  200. $schema->formatOutput = true;
  201. if (!empty($write)) {
  202. /* backup the existing schema if the --write argument is set */
  203. $backupSchemaName = $backup_prefix . $schema_name;
  204. if (empty($backup_path)) $backup_path = $schema_path;
  205. if ($backupSchemaName === $schema_name && $backup_path === $schema_path) {
  206. $xpdo->log(xPDO::LOG_LEVEL_FATAL, "backup_prefix is empty and no backup_path was specified.");
  207. }
  208. if (!$xpdo->cacheManager->writeFile("{$backup_path}{$backupSchemaName}", $schema->saveXML())) {
  209. $xpdo->log(xPDO::LOG_LEVEL_FATAL, "Error backing up schema to {$backup_path}{$backupSchemaName}.");
  210. }
  211. }
  212. /* parse the schema's DOM */
  213. $rootIdx = 0;
  214. $root = $schema->documentElement;
  215. $xpdo->log(xPDO::LOG_LEVEL_INFO, "parsing root element: {$root->nodeName}");
  216. if (strtolower($root->nodeName) !== 'model') {
  217. $xpdo->log(xPDO::LOG_LEVEL_FATAL, "root element is not a valid model schema element: {$root->nodeName}");
  218. }
  219. $platform = $root->attributes->getNamedItem('platform');
  220. if (strtolower($platform->nodeValue) !== 'mysql') {
  221. $xpdo->log(xPDO::LOG_LEVEL_FATAL, "this model is not a mysql model; this migration is not intended for {$platform->nodeValue}");
  222. }
  223. $written = false;
  224. if ($root->hasAttribute('version')) {
  225. $version = $root->attributes->getNamedItem('version');
  226. if ($written = version_compare($version->nodeValue, '1.1', '>=')) {
  227. if (!empty($write) || empty($regen)) {
  228. $xpdo->log(xPDO::LOG_LEVEL_FATAL, "this model is already at version 1.1; this migration is intended for older models");
  229. }
  230. }
  231. }
  232. if (!empty($regen) && empty($write) && empty($written)) {
  233. die("[{$scriptTitle}] FATAL: --regen can only be used when --write is set or the schema is already at version 1.1.\n");
  234. }
  235. if (empty($written)) {
  236. $root->setAttribute('version', '1.1');
  237. $attrs = array();
  238. $attrIdx = 0;
  239. while ($attr = $root->attributes->item($attrIdx++)) $attrs[$attr->nodeName] = $attr->nodeValue;
  240. $xpdo->log(xPDO::LOG_LEVEL_INFO, "model element ({$rootIdx}): " . print_r($attrs, true));
  241. $objectIdx = 0;
  242. while ($object = $root->childNodes->item($objectIdx++)) {
  243. $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "parsing model child element: {$object->nodeName}");
  244. if (strtolower($object->nodeName !== 'object')) continue;
  245. $attrs = array();
  246. $attrIdx = 0;
  247. while ($attr = $object->attributes->item($attrIdx++)) $attrs[$attr->nodeName] = $attr->nodeValue;
  248. $xpdo->log(xPDO::LOG_LEVEL_INFO, "object element ({$objectIdx}): " . print_r($attrs, true));
  249. $indexes = array();
  250. $idx = 0;
  251. while ($node = $object->childNodes->item($idx++)) {
  252. $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "parsing object child element: {$node->nodeName}");
  253. if (strtolower($node->nodeName !== 'field')) continue;
  254. $attrs = array();
  255. $attrIdx = 0;
  256. while ($attr = $node->attributes->item($attrIdx++)) $attrs[$attr->nodeName] = $attr->nodeValue;
  257. $xpdo->log(xPDO::LOG_LEVEL_INFO, "field element ({$idx}): " . print_r($attrs, true));
  258. if (!isset($attrs['key']) || empty($attrs['key'])) {
  259. $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Skipping field element: no valid key attribute");
  260. continue;
  261. }
  262. $groupMember = false;
  263. $fieldKey = $attrs['key'];
  264. if (!isset($attrs['index']) || empty($attrs['index'])) {
  265. $xpdo->log(xPDO::LOG_LEVEL_INFO, "Skipping field element: no index attribute");
  266. continue;
  267. }
  268. $fieldIndex = $attrs['index'];
  269. $fieldNull = isset($attrs['null']) ? $attrs['null'] : 'true';
  270. $indexName = isset($attrs['indexgrp']) && !empty($attrs['indexgrp']) ? $attrs['indexgrp'] : $fieldKey;
  271. $indexColumn = $fieldKey;
  272. $indexColumnLength = '';
  273. $indexColumnCollation = 'A';
  274. $indexColumnNull = !empty($fieldNull) ? $fieldNull : 'true';
  275. $indexPrimary = 'false';
  276. $indexUnique = 'false';
  277. $indexType = 'BTREE';
  278. switch ($fieldIndex) {
  279. case 'pk':
  280. $indexName = 'PRIMARY';
  281. $indexPrimary = true;
  282. $indexUnique = true;
  283. break;
  284. case 'unique':
  285. $indexUnique = true;
  286. break;
  287. case 'fulltext':
  288. $indexType = 'FULLTEXT';
  289. break;
  290. case 'fk':
  291. case 'index':
  292. default:
  293. break;
  294. }
  295. if (!isset($indexes[$indexName])) {
  296. $indexes[$indexName] = array(
  297. 'attributes' => array(
  298. 'name' => $indexName,
  299. 'alias' => $indexName,
  300. 'primary' => $indexPrimary,
  301. 'unique' => $indexUnique,
  302. 'type' => $indexType
  303. ),
  304. 'columns' => array(
  305. $indexColumn => array(
  306. 'key' => $indexColumn,
  307. 'length' => $indexColumnLength,
  308. 'collation' => $indexColumnCollation,
  309. 'null' => $indexColumnNull
  310. )
  311. )
  312. );
  313. } else {
  314. $indexes[$indexName]['columns'][$indexColumn] = array(
  315. 'key' => $indexColumn,
  316. 'length' => $indexColumnLength,
  317. 'collation' => $indexColumnCollation,
  318. 'null' => $indexColumnNull
  319. );
  320. }
  321. }
  322. if (!empty($indexes)) {
  323. $xpdo->log(xPDO::LOG_LEVEL_INFO, "Generating XML for index elements: " . print_r($indexes, true));
  324. $fragment = $schema->createDocumentFragment();
  325. $fragment->appendChild($schema->createTextNode("\n "));
  326. $fragment->appendChild($schema->createComment("Element indexes automatically generated by script: {$scriptTitle}"));
  327. $fragment->appendChild($schema->createTextNode("\n"));
  328. foreach ($indexes as $indexName => $index) {
  329. $indexElement = $schema->createElement('index');
  330. $indexElement->appendChild($schema->createTextNode("\n "));
  331. foreach ($index['attributes'] as $attrKey => $attrVal) {
  332. $indexElement->setAttribute($attrKey, $attrVal);
  333. }
  334. foreach ($index['columns'] as $columnKey => $column) {
  335. $columnElement = $schema->createElement('column');
  336. foreach ($column as $colAttrKey => $colAttrVal) {
  337. $columnElement->setAttribute($colAttrKey, $colAttrVal);
  338. }
  339. $indexElement->appendChild($schema->createTextNode(" "));
  340. $indexElement->appendChild($columnElement);
  341. $indexElement->appendChild($schema->createTextNode("\n "));
  342. }
  343. $fragment->appendChild($schema->createTextNode(" "));
  344. $fragment->appendChild($indexElement);
  345. $fragment->appendChild($schema->createTextNode("\n"));
  346. }
  347. $fragment->appendChild($schema->createTextNode("\n "));
  348. $indexElementNode = $schema->importNode($fragment, true);
  349. $object->appendChild($indexElementNode);
  350. }
  351. }
  352. }
  353. $xml = $schema->saveXML();
  354. if (!empty($echo)) {
  355. echo (XPDO_CLI_MODE ? "{$schema_name}:\n{$xml}\n" : "<h1>{$schema_name}</h1>\n<pre>{$xml}</pre>\n<br />");
  356. }
  357. if (!empty($write) && empty($written)) {
  358. $xpdo->log(xPDO::LOG_LEVEL_INFO, "Updating schema file at {$schema_path}{$schema_name}...");
  359. if (!$xpdo->cacheManager->writeFile("{$schema_path}{$schema_name}", $xml)) {
  360. $xpdo->log(xPDO::LOG_LEVEL_FATAL, "Migration failed! Error writing content to file at {$schema_path}{$schema_name}");
  361. }
  362. $xpdo->log(xPDO::LOG_LEVEL_INFO, "Migration of schema file {$schema_name} for pkg {$pkg} completed successfully.");
  363. }
  364. if ((!empty($write) || !empty($written)) && !empty($regen)) {
  365. $xpdo->log(xPDO::LOG_LEVEL_INFO, "Attempting to regenerate model maps for pkg {$pkg} from migrated schema at {$pkg_path}...");
  366. if (!$xpdo->getManager() || !$xpdo->manager->getGenerator()) {
  367. $xpdo->log(xPDO::LOG_LEVEL_FATAL, "Model regeneration failed: could not get instance of the xPDOManager or xPDOGenerator.");
  368. }
  369. if (!$xpdo->manager->generator->parseSchema("{$schema_path}{$schema_name}", $pkg_path)) {
  370. $xpdo->log(xPDO::LOG_LEVEL_FATAL, "Model regeneration failed! Error parsing schema {$schema_name} for pkg {$pkg} at {$pkg_path}.");
  371. }
  372. $xpdo->log(xPDO::LOG_LEVEL_INFO, "Regeneration of model files for pkg {$pkg} at {$pkg_path} completed successfully.");
  373. }
  374. exit();