PageRenderTime 71ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/src_8/core/lib/Drupal/Core/Command/DbDumpCommand.php

https://bitbucket.org/razum-io/ns-hub
PHP | 425 lines | 195 code | 43 blank | 187 comment | 33 complexity | 14a8c487616765530bd401f51549b61f MD5 | raw file
Possible License(s): LGPL-2.1, GPL-2.0, CC-BY-SA-3.0, MIT, BSD-3-Clause
  1. <?php
  2. namespace Drupal\Core\Command;
  3. use Drupal\Component\Utility\Variable;
  4. use Drupal\Core\Database\Connection;
  5. use Symfony\Component\Console\Input\InputInterface;
  6. use Symfony\Component\Console\Input\InputOption;
  7. use Symfony\Component\Console\Output\OutputInterface;
  8. /**
  9. * Provides a command to dump the current database to a script.
  10. *
  11. * This script exports all tables in the given database, and all data (except
  12. * for tables denoted as schema-only). The resulting script creates the tables
  13. * and populates them with the exported data.
  14. *
  15. * @todo This command is currently only compatible with MySQL. Making it
  16. * backend-agnostic will require \Drupal\Core\Database\Schema support the
  17. * ability to retrieve table schema information. Note that using a raw
  18. * SQL dump file here (eg, generated from mysqldump or pg_dump) is not an
  19. * option since these tend to still be database-backend specific.
  20. * @see https://www.drupal.org/node/301038
  21. *
  22. * @see \Drupal\Core\Command\DbDumpApplication
  23. */
  24. class DbDumpCommand extends DbCommandBase {
  25. /**
  26. * An array of table patterns to exclude completely.
  27. *
  28. * This excludes any lingering simpletest tables generated during test runs.
  29. *
  30. * @var array
  31. */
  32. protected $excludeTables = ['test[0-9]+'];
  33. /**
  34. * {@inheritdoc}
  35. */
  36. protected function configure() {
  37. $this->setName('dump-database-d8-mysql')
  38. ->setDescription('Dump the current database to a generation script')
  39. ->addOption('schema-only', NULL, InputOption::VALUE_OPTIONAL, 'A comma separated list of tables to only export the schema without data.', 'cache.*,sessions,watchdog');
  40. parent::configure();
  41. }
  42. /**
  43. * {@inheritdoc}
  44. */
  45. protected function execute(InputInterface $input, OutputInterface $output) {
  46. $connection = $this->getDatabaseConnection($input);
  47. // If not explicitly set, disable ANSI which will break generated php.
  48. if ($input->hasParameterOption(['--ansi']) !== TRUE) {
  49. $output->setDecorated(FALSE);
  50. }
  51. $schema_tables = $input->getOption('schema-only');
  52. $schema_tables = explode(',', $schema_tables);
  53. $output->writeln($this->generateScript($connection, $schema_tables), OutputInterface::OUTPUT_RAW);
  54. }
  55. /**
  56. * Generates the database script.
  57. *
  58. * @param \Drupal\Core\Database\Connection $connection
  59. * The database connection to use.
  60. * @param array $schema_only
  61. * Table patterns for which to only dump the schema, no data.
  62. * @return string
  63. * The PHP script.
  64. */
  65. protected function generateScript(Connection $connection, array $schema_only = []) {
  66. $tables = '';
  67. $schema_only_patterns = [];
  68. foreach ($schema_only as $match) {
  69. $schema_only_patterns[] = '/^' . $match . '$/';
  70. }
  71. foreach ($this->getTables($connection) as $table) {
  72. $schema = $this->getTableSchema($connection, $table);
  73. // Check for schema only.
  74. if (empty($schema_only_patterns) || preg_replace($schema_only_patterns, '', $table)) {
  75. $data = $this->getTableData($connection, $table);
  76. }
  77. else {
  78. $data = [];
  79. }
  80. $tables .= $this->getTableScript($table, $schema, $data);
  81. }
  82. $script = $this->getTemplate();
  83. // Substitute in the tables.
  84. $script = str_replace('{{TABLES}}', trim($tables), $script);
  85. return trim($script);
  86. }
  87. /**
  88. * Returns a list of tables, not including those set to be excluded.
  89. *
  90. * @param \Drupal\Core\Database\Connection $connection
  91. * The database connection to use.
  92. * @return array
  93. * An array of table names.
  94. */
  95. protected function getTables(Connection $connection) {
  96. $tables = array_values($connection->schema()->findTables('%'));
  97. foreach ($tables as $key => $table) {
  98. // Remove any explicitly excluded tables.
  99. foreach ($this->excludeTables as $pattern) {
  100. if (preg_match('/^' . $pattern . '$/', $table)) {
  101. unset($tables[$key]);
  102. }
  103. }
  104. }
  105. return $tables;
  106. }
  107. /**
  108. * Returns a schema array for a given table.
  109. *
  110. * @param \Drupal\Core\Database\Connection $connection
  111. * The database connection to use.
  112. * @param string $table
  113. * The table name.
  114. *
  115. * @return array
  116. * A schema array (as defined by hook_schema()).
  117. *
  118. * @todo This implementation is hard-coded for MySQL.
  119. */
  120. protected function getTableSchema(Connection $connection, $table) {
  121. // Check this is MySQL.
  122. if ($connection->databaseType() !== 'mysql') {
  123. throw new \RuntimeException('This script can only be used with MySQL database backends.');
  124. }
  125. $query = $connection->query("SHOW FULL COLUMNS FROM {" . $table . "}");
  126. $definition = [];
  127. while (($row = $query->fetchAssoc()) !== FALSE) {
  128. $name = $row['Field'];
  129. // Parse out the field type and meta information.
  130. preg_match('@([a-z]+)(?:\((\d+)(?:,(\d+))?\))?\s*(unsigned)?@', $row['Type'], $matches);
  131. $type = $this->fieldTypeMap($connection, $matches[1]);
  132. if ($row['Extra'] === 'auto_increment') {
  133. // If this is an auto increment, then the type is 'serial'.
  134. $type = 'serial';
  135. }
  136. $definition['fields'][$name] = [
  137. 'type' => $type,
  138. 'not null' => $row['Null'] === 'NO',
  139. ];
  140. if ($size = $this->fieldSizeMap($connection, $matches[1])) {
  141. $definition['fields'][$name]['size'] = $size;
  142. }
  143. if (isset($matches[2]) && $type === 'numeric') {
  144. // Add precision and scale.
  145. $definition['fields'][$name]['precision'] = $matches[2];
  146. $definition['fields'][$name]['scale'] = $matches[3];
  147. }
  148. elseif ($type === 'time' || $type === 'datetime') {
  149. // @todo Core doesn't support these, but copied from `migrate-db.sh` for now.
  150. // Convert to varchar.
  151. $definition['fields'][$name]['type'] = 'varchar';
  152. $definition['fields'][$name]['length'] = '100';
  153. }
  154. elseif (!isset($definition['fields'][$name]['size'])) {
  155. // Try use the provided length, if it doesn't exist default to 100. It's
  156. // not great but good enough for our dumps at this point.
  157. $definition['fields'][$name]['length'] = isset($matches[2]) ? $matches[2] : 100;
  158. }
  159. if (isset($row['Default'])) {
  160. $definition['fields'][$name]['default'] = $row['Default'];
  161. }
  162. if (isset($matches[4])) {
  163. $definition['fields'][$name]['unsigned'] = TRUE;
  164. }
  165. // Check for the 'varchar_ascii' type that should be 'binary'.
  166. if (isset($row['Collation']) && $row['Collation'] == 'ascii_bin') {
  167. $definition['fields'][$name]['type'] = 'varchar_ascii';
  168. $definition['fields'][$name]['binary'] = TRUE;
  169. }
  170. // Check for the non-binary 'varchar_ascii'.
  171. if (isset($row['Collation']) && $row['Collation'] == 'ascii_general_ci') {
  172. $definition['fields'][$name]['type'] = 'varchar_ascii';
  173. }
  174. // Check for the 'utf8_bin' collation.
  175. if (isset($row['Collation']) && $row['Collation'] == 'utf8_bin') {
  176. $definition['fields'][$name]['binary'] = TRUE;
  177. }
  178. }
  179. // Set primary key, unique keys, and indexes.
  180. $this->getTableIndexes($connection, $table, $definition);
  181. // Set table collation.
  182. $this->getTableCollation($connection, $table, $definition);
  183. return $definition;
  184. }
  185. /**
  186. * Adds primary key, unique keys, and index information to the schema.
  187. *
  188. * @param \Drupal\Core\Database\Connection $connection
  189. * The database connection to use.
  190. * @param string $table
  191. * The table to find indexes for.
  192. * @param array &$definition
  193. * The schema definition to modify.
  194. */
  195. protected function getTableIndexes(Connection $connection, $table, &$definition) {
  196. // Note, this query doesn't support ordering, so that is worked around
  197. // below by keying the array on Seq_in_index.
  198. $query = $connection->query("SHOW INDEX FROM {" . $table . "}");
  199. while (($row = $query->fetchAssoc()) !== FALSE) {
  200. $index_name = $row['Key_name'];
  201. $column = $row['Column_name'];
  202. // Key the arrays by the index sequence for proper ordering (start at 0).
  203. $order = $row['Seq_in_index'] - 1;
  204. // If specified, add length to the index.
  205. if ($row['Sub_part']) {
  206. $column = [$column, $row['Sub_part']];
  207. }
  208. if ($index_name === 'PRIMARY') {
  209. $definition['primary key'][$order] = $column;
  210. }
  211. elseif ($row['Non_unique'] == 0) {
  212. $definition['unique keys'][$index_name][$order] = $column;
  213. }
  214. else {
  215. $definition['indexes'][$index_name][$order] = $column;
  216. }
  217. }
  218. }
  219. /**
  220. * Set the table collation.
  221. *
  222. * @param \Drupal\Core\Database\Connection $connection
  223. * The database connection to use.
  224. * @param string $table
  225. * The table to find indexes for.
  226. * @param array &$definition
  227. * The schema definition to modify.
  228. */
  229. protected function getTableCollation(Connection $connection, $table, &$definition) {
  230. $query = $connection->query("SHOW TABLE STATUS LIKE '{" . $table . "}'");
  231. $data = $query->fetchAssoc();
  232. // Set `mysql_character_set`. This will be ignored by other backends.
  233. $definition['mysql_character_set'] = str_replace('_general_ci', '', $data['Collation']);
  234. }
  235. /**
  236. * Gets all data from a given table.
  237. *
  238. * If a table is set to be schema only, and empty array is returned.
  239. *
  240. * @param \Drupal\Core\Database\Connection $connection
  241. * The database connection to use.
  242. * @param string $table
  243. * The table to query.
  244. *
  245. * @return array
  246. * The data from the table as an array.
  247. */
  248. protected function getTableData(Connection $connection, $table) {
  249. $order = $this->getFieldOrder($connection, $table);
  250. $query = $connection->query("SELECT * FROM {" . $table . "} " . $order);
  251. $results = [];
  252. while (($row = $query->fetchAssoc()) !== FALSE) {
  253. $results[] = $row;
  254. }
  255. return $results;
  256. }
  257. /**
  258. * Given a database field type, return a Drupal type.
  259. *
  260. * @param \Drupal\Core\Database\Connection $connection
  261. * The database connection to use.
  262. * @param string $type
  263. * The MySQL field type.
  264. *
  265. * @return string
  266. * The Drupal schema field type. If there is no mapping, the original field
  267. * type is returned.
  268. */
  269. protected function fieldTypeMap(Connection $connection, $type) {
  270. // Convert everything to lowercase.
  271. $map = array_map('strtolower', $connection->schema()->getFieldTypeMap());
  272. $map = array_flip($map);
  273. // The MySql map contains type:size. Remove the size part.
  274. return isset($map[$type]) ? explode(':', $map[$type])[0] : $type;
  275. }
  276. /**
  277. * Given a database field type, return a Drupal size.
  278. *
  279. * @param \Drupal\Core\Database\Connection $connection
  280. * The database connection to use.
  281. * @param string $type
  282. * The MySQL field type.
  283. *
  284. * @return string
  285. * The Drupal schema field size.
  286. */
  287. protected function fieldSizeMap(Connection $connection, $type) {
  288. // Convert everything to lowercase.
  289. $map = array_map('strtolower', $connection->schema()->getFieldTypeMap());
  290. $map = array_flip($map);
  291. $schema_type = explode(':', $map[$type])[0];
  292. // Only specify size on these types.
  293. if (in_array($schema_type, ['blob', 'float', 'int', 'text'])) {
  294. // The MySql map contains type:size. Remove the type part.
  295. return explode(':', $map[$type])[1];
  296. }
  297. }
  298. /**
  299. * Gets field ordering for a given table.
  300. *
  301. * @param \Drupal\Core\Database\Connection $connection
  302. * The database connection to use.
  303. * @param string $table
  304. * The table name.
  305. *
  306. * @return string
  307. * The order string to append to the query.
  308. */
  309. protected function getFieldOrder(Connection $connection, $table) {
  310. // @todo this is MySQL only since there are no Database API functions for
  311. // table column data.
  312. // @todo this code is duplicated in `core/scripts/migrate-db.sh`.
  313. $connection_info = $connection->getConnectionOptions();
  314. // Order by primary keys.
  315. $order = '';
  316. $query = "SELECT `COLUMN_NAME` FROM `information_schema`.`COLUMNS`
  317. WHERE (`TABLE_SCHEMA` = '" . $connection_info['database'] . "')
  318. AND (`TABLE_NAME` = '{" . $table . "}') AND (`COLUMN_KEY` = 'PRI')
  319. ORDER BY COLUMN_NAME";
  320. $results = $connection->query($query);
  321. while (($row = $results->fetchAssoc()) !== FALSE) {
  322. $order .= $row['COLUMN_NAME'] . ', ';
  323. }
  324. if (!empty($order)) {
  325. $order = ' ORDER BY ' . rtrim($order, ', ');
  326. }
  327. return $order;
  328. }
  329. /**
  330. * The script template.
  331. *
  332. * @return string
  333. * The template for the generated PHP script.
  334. */
  335. protected function getTemplate() {
  336. // The template contains an instruction for the file to be ignored by PHPCS.
  337. // This is because the files can be huge and coding standards are
  338. // irrelevant.
  339. $script = <<<'ENDOFSCRIPT'
  340. <?php
  341. // @codingStandardsIgnoreFile
  342. /**
  343. * @file
  344. * A database agnostic dump for testing purposes.
  345. *
  346. * This file was generated by the Drupal 8.0 db-tools.php script.
  347. */
  348. use Drupal\Core\Database\Database;
  349. $connection = Database::getConnection();
  350. {{TABLES}}
  351. ENDOFSCRIPT;
  352. return $script;
  353. }
  354. /**
  355. * The part of the script for each table.
  356. *
  357. * @param string $table
  358. * Table name.
  359. * @param array $schema
  360. * Drupal schema definition.
  361. * @param array $data
  362. * Data for the table.
  363. *
  364. * @return string
  365. * The table create statement, and if there is data, the insert command.
  366. */
  367. protected function getTableScript($table, array $schema, array $data) {
  368. $output = '';
  369. $output .= "\$connection->schema()->createTable('" . $table . "', " . Variable::export($schema) . ");\n\n";
  370. if (!empty($data)) {
  371. $insert = '';
  372. foreach ($data as $record) {
  373. $insert .= "->values(" . Variable::export($record) . ")\n";
  374. }
  375. $output .= "\$connection->insert('" . $table . "')\n"
  376. . "->fields(" . Variable::export(array_keys($schema['fields'])) . ")\n"
  377. . $insert
  378. . "->execute();\n\n";
  379. }
  380. return $output;
  381. }
  382. }