SchemaDumper.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\Migrations;
  4. use Doctrine\DBAL\Platforms\AbstractPlatform;
  5. use Doctrine\DBAL\Schema\AbstractSchemaManager;
  6. use Doctrine\DBAL\Schema\Table;
  7. use Doctrine\Migrations\Exception\NoTablesFound;
  8. use Doctrine\Migrations\Generator\Generator;
  9. use Doctrine\Migrations\Generator\SqlGenerator;
  10. use InvalidArgumentException;
  11. use function array_merge;
  12. use function count;
  13. use function implode;
  14. use function preg_last_error;
  15. use function preg_match;
  16. use function restore_error_handler;
  17. use function set_error_handler;
  18. use function sprintf;
  19. use const PREG_BACKTRACK_LIMIT_ERROR;
  20. use const PREG_BAD_UTF8_ERROR;
  21. use const PREG_BAD_UTF8_OFFSET_ERROR;
  22. use const PREG_INTERNAL_ERROR;
  23. use const PREG_RECURSION_LIMIT_ERROR;
  24. /**
  25. * The SchemaDumper class is responsible for dumping the current state of your database schema to a migration. This
  26. * is to be used in conjunction with the Rollup class.
  27. *
  28. * @internal
  29. *
  30. * @see Doctrine\Migrations\Rollup
  31. */
  32. class SchemaDumper
  33. {
  34. /** @var AbstractPlatform */
  35. private $platform;
  36. /** @var AbstractSchemaManager */
  37. private $schemaManager;
  38. /** @var Generator */
  39. private $migrationGenerator;
  40. /** @var SqlGenerator */
  41. private $migrationSqlGenerator;
  42. /** @var string[] */
  43. private $excludedTablesRegexes;
  44. /**
  45. * @param string[] $excludedTablesRegexes
  46. */
  47. public function __construct(
  48. AbstractPlatform $platform,
  49. AbstractSchemaManager $schemaManager,
  50. Generator $migrationGenerator,
  51. SqlGenerator $migrationSqlGenerator,
  52. array $excludedTablesRegexes = []
  53. ) {
  54. $this->platform = $platform;
  55. $this->schemaManager = $schemaManager;
  56. $this->migrationGenerator = $migrationGenerator;
  57. $this->migrationSqlGenerator = $migrationSqlGenerator;
  58. $this->excludedTablesRegexes = $excludedTablesRegexes;
  59. }
  60. /**
  61. * @param string[] $excludedTablesRegexes
  62. *
  63. * @throws NoTablesFound
  64. */
  65. public function dump(
  66. string $fqcn,
  67. array $excludedTablesRegexes = [],
  68. bool $formatted = false,
  69. int $lineLength = 120
  70. ): string {
  71. $schema = $this->schemaManager->createSchema();
  72. $up = [];
  73. $down = [];
  74. foreach ($schema->getTables() as $table) {
  75. if ($this->shouldSkipTable($table, $excludedTablesRegexes)) {
  76. continue;
  77. }
  78. $upSql = $this->platform->getCreateTableSQL($table);
  79. $upCode = $this->migrationSqlGenerator->generate(
  80. $upSql,
  81. $formatted,
  82. $lineLength
  83. );
  84. if ($upCode !== '') {
  85. $up[] = $upCode;
  86. }
  87. $downSql = [$this->platform->getDropTableSQL($table)];
  88. $downCode = $this->migrationSqlGenerator->generate(
  89. $downSql,
  90. $formatted,
  91. $lineLength
  92. );
  93. if ($downCode === '') {
  94. continue;
  95. }
  96. $down[] = $downCode;
  97. }
  98. if (count($up) === 0) {
  99. throw NoTablesFound::new();
  100. }
  101. $up = implode("\n", $up);
  102. $down = implode("\n", $down);
  103. return $this->migrationGenerator->generateMigration(
  104. $fqcn,
  105. $up,
  106. $down
  107. );
  108. }
  109. /**
  110. * @param string[] $excludedTablesRegexes
  111. */
  112. private function shouldSkipTable(Table $table, array $excludedTablesRegexes): bool
  113. {
  114. foreach (array_merge($excludedTablesRegexes, $this->excludedTablesRegexes) as $regex) {
  115. if (self::pregMatch($regex, $table->getName()) !== 0) {
  116. return true;
  117. }
  118. }
  119. return false;
  120. }
  121. /**
  122. * A local wrapper for "preg_match" which will throw a InvalidArgumentException if there
  123. * is an internal error in the PCRE engine.
  124. * Copied from https://github.com/symfony/symfony/blob/62216ea67762b18982ca3db73c391b0748a49d49/src/Symfony/Component/Yaml/Parser.php#L1072-L1090
  125. *
  126. * @internal
  127. *
  128. * @param mixed[] $matches
  129. */
  130. private static function pregMatch(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int
  131. {
  132. try {
  133. $errorMessages = [];
  134. set_error_handler(static function (int $severity, string $message, string $file, int $line) use (&$errorMessages): bool {
  135. $errorMessages[] = $message;
  136. return true;
  137. });
  138. $ret = preg_match($pattern, $subject, $matches, $flags, $offset);
  139. } finally {
  140. restore_error_handler();
  141. }
  142. if ($ret === false) {
  143. switch (preg_last_error()) {
  144. case PREG_INTERNAL_ERROR:
  145. $error = sprintf('Internal PCRE error, please check your Regex. Reported errors: %s.', implode(', ', $errorMessages));
  146. break;
  147. case PREG_BACKTRACK_LIMIT_ERROR:
  148. $error = 'pcre.backtrack_limit reached.';
  149. break;
  150. case PREG_RECURSION_LIMIT_ERROR:
  151. $error = 'pcre.recursion_limit reached.';
  152. break;
  153. case PREG_BAD_UTF8_ERROR:
  154. $error = 'Malformed UTF-8 data.';
  155. break;
  156. case PREG_BAD_UTF8_OFFSET_ERROR:
  157. $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
  158. break;
  159. default:
  160. $error = 'Error.';
  161. }
  162. throw new InvalidArgumentException($error);
  163. }
  164. return $ret;
  165. }
  166. }