PageRenderTime 56ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

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

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