DbalExecutor.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\Migrations\Version;
  4. use DateTimeImmutable;
  5. use Doctrine\DBAL\Connection;
  6. use Doctrine\DBAL\Schema\Schema;
  7. use Doctrine\Migrations\AbstractMigration;
  8. use Doctrine\Migrations\EventDispatcher;
  9. use Doctrine\Migrations\Events;
  10. use Doctrine\Migrations\Exception\SkipMigration;
  11. use Doctrine\Migrations\Metadata\MigrationPlan;
  12. use Doctrine\Migrations\Metadata\Storage\MetadataStorage;
  13. use Doctrine\Migrations\MigratorConfiguration;
  14. use Doctrine\Migrations\ParameterFormatter;
  15. use Doctrine\Migrations\Provider\SchemaDiffProvider;
  16. use Doctrine\Migrations\Query\Query;
  17. use Doctrine\Migrations\Tools\BytesFormatter;
  18. use Doctrine\Migrations\Tools\TransactionHelper;
  19. use Psr\Log\LoggerInterface;
  20. use Psr\Log\LogLevel;
  21. use Symfony\Component\Stopwatch\Stopwatch;
  22. use Throwable;
  23. use function count;
  24. use function ucfirst;
  25. /**
  26. * The DbalExecutor class is responsible for executing a single migration version.
  27. *
  28. * @internal
  29. */
  30. final class DbalExecutor implements Executor
  31. {
  32. /** @var Connection */
  33. private $connection;
  34. /** @var SchemaDiffProvider */
  35. private $schemaProvider;
  36. /** @var ParameterFormatter */
  37. private $parameterFormatter;
  38. /** @var Stopwatch */
  39. private $stopwatch;
  40. /** @var Query[] */
  41. private $sql = [];
  42. /** @var MetadataStorage */
  43. private $metadataStorage;
  44. /** @var LoggerInterface */
  45. private $logger;
  46. /** @var EventDispatcher */
  47. private $dispatcher;
  48. public function __construct(
  49. MetadataStorage $metadataStorage,
  50. EventDispatcher $dispatcher,
  51. Connection $connection,
  52. SchemaDiffProvider $schemaProvider,
  53. LoggerInterface $logger,
  54. ParameterFormatter $parameterFormatter,
  55. Stopwatch $stopwatch
  56. ) {
  57. $this->connection = $connection;
  58. $this->schemaProvider = $schemaProvider;
  59. $this->parameterFormatter = $parameterFormatter;
  60. $this->stopwatch = $stopwatch;
  61. $this->metadataStorage = $metadataStorage;
  62. $this->logger = $logger;
  63. $this->dispatcher = $dispatcher;
  64. }
  65. /**
  66. * @return Query[]
  67. */
  68. public function getSql(): array
  69. {
  70. return $this->sql;
  71. }
  72. public function addSql(Query $sqlQuery): void
  73. {
  74. $this->sql[] = $sqlQuery;
  75. }
  76. public function execute(
  77. MigrationPlan $plan,
  78. MigratorConfiguration $configuration
  79. ): ExecutionResult {
  80. $result = new ExecutionResult($plan->getVersion(), $plan->getDirection(), new DateTimeImmutable());
  81. $this->startMigration($plan, $configuration);
  82. try {
  83. $this->executeMigration(
  84. $plan,
  85. $result,
  86. $configuration
  87. );
  88. $result->setSql($this->sql);
  89. } catch (SkipMigration $e) {
  90. $result->setSkipped(true);
  91. $this->migrationEnd($e, $plan, $result, $configuration);
  92. } catch (Throwable $e) {
  93. $result->setError(true, $e);
  94. $this->migrationEnd($e, $plan, $result, $configuration);
  95. throw $e;
  96. }
  97. return $result;
  98. }
  99. private function startMigration(
  100. MigrationPlan $plan,
  101. MigratorConfiguration $configuration
  102. ): void {
  103. $this->sql = [];
  104. $this->dispatcher->dispatchVersionEvent(
  105. Events::onMigrationsVersionExecuting,
  106. $plan,
  107. $configuration
  108. );
  109. if (! $plan->getMigration()->isTransactional()) {
  110. return;
  111. }
  112. // only start transaction if in transactional mode
  113. $this->connection->beginTransaction();
  114. }
  115. private function executeMigration(
  116. MigrationPlan $plan,
  117. ExecutionResult $result,
  118. MigratorConfiguration $configuration
  119. ): ExecutionResult {
  120. $stopwatchEvent = $this->stopwatch->start('execute');
  121. $migration = $plan->getMigration();
  122. $direction = $plan->getDirection();
  123. $result->setState(State::PRE);
  124. $fromSchema = $this->getFromSchema($configuration);
  125. $migration->{'pre' . ucfirst($direction)}($fromSchema);
  126. $this->logger->info(...$this->getMigrationHeader($plan, $migration, $direction));
  127. $result->setState(State::EXEC);
  128. $toSchema = $this->schemaProvider->createToSchema($fromSchema);
  129. $result->setToSchema($toSchema);
  130. $migration->$direction($toSchema);
  131. foreach ($migration->getSql() as $sqlQuery) {
  132. $this->addSql($sqlQuery);
  133. }
  134. foreach ($this->schemaProvider->getSqlDiffToMigrate($fromSchema, $toSchema) as $sql) {
  135. $this->addSql(new Query($sql));
  136. }
  137. if (count($this->sql) !== 0) {
  138. if (! $configuration->isDryRun()) {
  139. $this->executeResult($configuration);
  140. } else {
  141. foreach ($this->sql as $query) {
  142. $this->outputSqlQuery($query, $configuration);
  143. }
  144. }
  145. } else {
  146. $this->logger->warning('Migration {version} was executed but did not result in any SQL statements.', [
  147. 'version' => (string) $plan->getVersion(),
  148. ]);
  149. }
  150. $result->setState(State::POST);
  151. $migration->{'post' . ucfirst($direction)}($toSchema);
  152. $stopwatchEvent->stop();
  153. $periods = $stopwatchEvent->getPeriods();
  154. $lastPeriod = $periods[count($periods) - 1];
  155. $result->setTime((float) $lastPeriod->getDuration() / 1000);
  156. $result->setMemory($lastPeriod->getMemory());
  157. $params = [
  158. 'version' => (string) $plan->getVersion(),
  159. 'time' => $lastPeriod->getDuration(),
  160. 'memory' => BytesFormatter::formatBytes($lastPeriod->getMemory()),
  161. 'direction' => $direction === Direction::UP ? 'migrated' : 'reverted',
  162. ];
  163. $this->logger->info('Migration {version} {direction} (took {time}ms, used {memory} memory)', $params);
  164. if (! $configuration->isDryRun()) {
  165. $this->metadataStorage->complete($result);
  166. }
  167. if ($migration->isTransactional()) {
  168. TransactionHelper::commitIfInTransaction($this->connection);
  169. }
  170. $plan->markAsExecuted($result);
  171. $result->setState(State::NONE);
  172. $this->dispatcher->dispatchVersionEvent(
  173. Events::onMigrationsVersionExecuted,
  174. $plan,
  175. $configuration
  176. );
  177. return $result;
  178. }
  179. /**
  180. * @return mixed[]
  181. */
  182. private function getMigrationHeader(MigrationPlan $planItem, AbstractMigration $migration, string $direction): array
  183. {
  184. $versionInfo = (string) $planItem->getVersion();
  185. $description = $migration->getDescription();
  186. if ($description !== '') {
  187. $versionInfo .= ' (' . $description . ')';
  188. }
  189. $params = ['version_name' => $versionInfo];
  190. if ($direction === Direction::UP) {
  191. return ['++ migrating {version_name}', $params];
  192. }
  193. return ['++ reverting {version_name}', $params];
  194. }
  195. private function migrationEnd(Throwable $e, MigrationPlan $plan, ExecutionResult $result, MigratorConfiguration $configuration): void
  196. {
  197. $migration = $plan->getMigration();
  198. if ($migration->isTransactional()) {
  199. //only rollback transaction if in transactional mode
  200. $this->connection->rollBack();
  201. }
  202. $plan->markAsExecuted($result);
  203. $this->logResult($e, $result, $plan);
  204. $this->dispatcher->dispatchVersionEvent(
  205. Events::onMigrationsVersionSkipped,
  206. $plan,
  207. $configuration
  208. );
  209. }
  210. private function logResult(Throwable $e, ExecutionResult $result, MigrationPlan $plan): void
  211. {
  212. if ($result->isSkipped()) {
  213. $this->logger->error(
  214. 'Migration {version} skipped during {state}. Reason: "{reason}"',
  215. [
  216. 'version' => (string) $plan->getVersion(),
  217. 'reason' => $e->getMessage(),
  218. 'state' => $this->getExecutionStateAsString($result->getState()),
  219. ]
  220. );
  221. } elseif ($result->hasError()) {
  222. $this->logger->error(
  223. 'Migration {version} failed during {state}. Error: "{error}"',
  224. [
  225. 'version' => (string) $plan->getVersion(),
  226. 'error' => $e->getMessage(),
  227. 'state' => $this->getExecutionStateAsString($result->getState()),
  228. ]
  229. );
  230. }
  231. }
  232. private function executeResult(MigratorConfiguration $configuration): void
  233. {
  234. foreach ($this->sql as $key => $query) {
  235. $this->outputSqlQuery($query, $configuration);
  236. $stopwatchEvent = $this->stopwatch->start('query');
  237. // executeQuery() must be used here because $query might return a result set, for instance REPAIR does
  238. $this->connection->executeQuery($query->getStatement(), $query->getParameters(), $query->getTypes());
  239. $stopwatchEvent->stop();
  240. if (! $configuration->getTimeAllQueries()) {
  241. continue;
  242. }
  243. $this->logger->notice('Query took {duration}ms', [
  244. 'duration' => $stopwatchEvent->getDuration(),
  245. ]);
  246. }
  247. }
  248. private function outputSqlQuery(Query $query, MigratorConfiguration $configuration): void
  249. {
  250. $params = $this->parameterFormatter->formatParameters(
  251. $query->getParameters(),
  252. $query->getTypes()
  253. );
  254. $this->logger->log(
  255. $configuration->getTimeAllQueries() ? LogLevel::NOTICE : LogLevel::DEBUG,
  256. '{query} {params}',
  257. [
  258. 'query' => $query->getStatement(),
  259. 'params' => $params,
  260. ]
  261. );
  262. }
  263. private function getFromSchema(MigratorConfiguration $configuration): Schema
  264. {
  265. // if we're in a dry run, use the from Schema instead of reading the schema from the database
  266. if ($configuration->isDryRun() && $configuration->getFromSchema() !== null) {
  267. return $configuration->getFromSchema();
  268. }
  269. return $this->schemaProvider->createFromSchema();
  270. }
  271. private function getExecutionStateAsString(int $state): string
  272. {
  273. switch ($state) {
  274. case State::PRE:
  275. return 'Pre-Checks';
  276. case State::POST:
  277. return 'Post-Checks';
  278. case State::EXEC:
  279. return 'Execution';
  280. default:
  281. return 'No State';
  282. }
  283. }
  284. }