Migrate.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2013-present http://www.thinkcmf.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
  8. // +---------------------------------------------------------------------
  9. // | Author: Dean <zxxjjforever@163.com>
  10. // +----------------------------------------------------------------------
  11. namespace think\migration;
  12. use InvalidArgumentException;
  13. use Phinx\Db\Adapter\AdapterFactory;
  14. use Phinx\Db\Adapter\ProxyAdapter;
  15. use Phinx\Migration\AbstractMigration;
  16. use Phinx\Migration\MigrationInterface;
  17. use Phinx\Util\Util;
  18. use think\migration\Migrator;
  19. class Migrate
  20. {
  21. /**
  22. * @var array
  23. */
  24. protected $migrations;
  25. private $output;
  26. private $input;
  27. private $appName;
  28. private $pluginName;
  29. /**
  30. * @param string $appName
  31. * @param string $pluginName
  32. */
  33. public function __construct(string $appName = '', string $pluginName = '')
  34. {
  35. $this->appName = $appName;
  36. $this->pluginName = $pluginName;
  37. }
  38. public function setOutput($output)
  39. {
  40. $this->output = $output;
  41. }
  42. public function getAdapter()
  43. {
  44. if (isset($this->adapter)) {
  45. return $this->adapter;
  46. }
  47. $options = $this->getDbConfig();
  48. $adapter = AdapterFactory::instance()->getAdapter($options['adapter'], $options);
  49. if ($adapter->hasOption('table_prefix') || $adapter->hasOption('table_suffix')) {
  50. $adapter = AdapterFactory::instance()->getWrapper('prefix', $adapter);
  51. }
  52. $this->adapter = $adapter;
  53. return $adapter;
  54. }
  55. /**
  56. * 获取数据库配置
  57. * @return array
  58. */
  59. protected function getDbConfig(): array
  60. {
  61. $default = $this->app->config->get('database.default');
  62. $config = $this->app->config->get("database.connections.{$default}");
  63. if (0 == $config['deploy']) {
  64. $dbConfig = [
  65. 'adapter' => $config['type'],
  66. 'host' => $config['hostname'],
  67. 'name' => $config['database'],
  68. 'user' => $config['username'],
  69. 'pass' => $config['password'],
  70. 'port' => $config['hostport'],
  71. 'charset' => $config['charset'],
  72. 'table_prefix' => $config['prefix'],
  73. 'version_order' => $config['version_order'] ?? 'creation',
  74. ];
  75. } else {
  76. $dbConfig = [
  77. 'adapter' => explode(',', $config['type'])[0],
  78. 'host' => explode(',', $config['hostname'])[0],
  79. 'name' => explode(',', $config['database'])[0],
  80. 'user' => explode(',', $config['username'])[0],
  81. 'pass' => explode(',', $config['password'])[0],
  82. 'port' => explode(',', $config['hostport'])[0],
  83. 'charset' => explode(',', $config['charset'])[0],
  84. 'table_prefix' => explode(',', $config['prefix'])[0],
  85. 'version_order' => explode(',', $config['version_order'])[0] ?? 'creation',
  86. ];
  87. }
  88. if ($this->appName) {
  89. $table = "{$this->appName}_migration";
  90. } elseif ($this->pluginName) {
  91. $table = 'plugin_' . cmf_parse_name($this->pluginName) . '_migration';
  92. } else {
  93. $table = 'migration';
  94. }
  95. $dbConfig['default_migration_table'] = $dbConfig['table_prefix'] . $table;
  96. return $dbConfig;
  97. }
  98. public function verifyMigrationDirectory(string $path)
  99. {
  100. if (!is_dir($path)) {
  101. throw new InvalidArgumentException(sprintf('Migration directory "%s" does not exist', $path));
  102. }
  103. if (!is_writable($path)) {
  104. throw new InvalidArgumentException(sprintf('Migration directory "%s" is not writable', $path));
  105. }
  106. }
  107. public function getPath()
  108. {
  109. $this->app = app();
  110. if ($this->appName) {
  111. $path = $this->app->getAppPath() . $this->appName . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'migrations';
  112. } elseif ($this->pluginName) {
  113. $path = WEB_ROOT . 'plugins' . DIRECTORY_SEPARATOR . cmf_parse_name($this->pluginName) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'migrations';
  114. } else {
  115. $path = $this->app->getRootPath() . 'vendor/thinkcmf/cmf/src/data/migrations';
  116. }
  117. return $path;
  118. }
  119. public function executeMigration(MigrationInterface $migration, $direction = MigrationInterface::UP)
  120. {
  121. $this->writeln(' ==' . ' <info>' . $migration->getVersion() . ' ' . $migration->getName() . ':</info>' . ' <comment>' . (MigrationInterface::UP === $direction ? 'migrating' : 'reverting') . '</comment>');
  122. // Execute the migration and log the time elapsed.
  123. $start = microtime(true);
  124. $startTime = time();
  125. $direction = (MigrationInterface::UP === $direction) ? MigrationInterface::UP : MigrationInterface::DOWN;
  126. $migration->setAdapter($this->getAdapter());
  127. // begin the transaction if the adapter supports it
  128. if ($this->getAdapter()->hasTransactions()) {
  129. $this->getAdapter()->beginTransaction();
  130. }
  131. // Run the migration
  132. if (method_exists($migration, MigrationInterface::CHANGE)) {
  133. if (MigrationInterface::DOWN === $direction) {
  134. // Create an instance of the ProxyAdapter so we can record all
  135. // of the migration commands for reverse playback
  136. /** @var ProxyAdapter $proxyAdapter */
  137. $proxyAdapter = AdapterFactory::instance()->getWrapper('proxy', $this->getAdapter());
  138. $migration->setAdapter($proxyAdapter);
  139. /** @noinspection PhpUndefinedMethodInspection */
  140. $migration->change();
  141. $proxyAdapter->executeInvertedCommands();
  142. $migration->setAdapter($this->getAdapter());
  143. } else {
  144. /** @noinspection PhpUndefinedMethodInspection */
  145. $migration->change();
  146. }
  147. } else {
  148. $migration->{$direction}();
  149. }
  150. // commit the transaction if the adapter supports it
  151. if ($this->getAdapter()->hasTransactions()) {
  152. $this->getAdapter()->commitTransaction();
  153. }
  154. // Record it in the database
  155. $this->getAdapter()
  156. ->migrated($migration, $direction, date('Y-m-d H:i:s', $startTime), date('Y-m-d H:i:s', time()));
  157. $end = microtime(true);
  158. $this->writeln(' ==' . ' <info>' . $migration->getVersion() . ' ' . $migration->getName() . ':</info>' . ' <comment>' . (MigrationInterface::UP === $direction ? 'migrated' : 'reverted') . ' ' . sprintf('%.4fs', $end - $start) . '</comment>');
  159. }
  160. public function getVersionLog()
  161. {
  162. return $this->getAdapter()->getVersionLog();
  163. }
  164. public function getVersions()
  165. {
  166. return $this->getAdapter()->getVersions();
  167. }
  168. public function getMigrations()
  169. {
  170. if (null === $this->migrations) {
  171. $phpFiles = glob($this->getPath() . DIRECTORY_SEPARATOR . '*.php', defined('GLOB_BRACE') ? GLOB_BRACE : 0);
  172. // filter the files to only get the ones that match our naming scheme
  173. $fileNames = [];
  174. /** @var Migrator[] $versions */
  175. $versions = [];
  176. foreach ($phpFiles as $filePath) {
  177. if (Util::isValidMigrationFileName(basename($filePath))) {
  178. $version = Util::getVersionFromFileName(basename($filePath));
  179. if (isset($versions[$version])) {
  180. throw new InvalidArgumentException(sprintf('Duplicate migration - "%s" has the same version as "%s"', $filePath, $versions[$version]->getVersion()));
  181. }
  182. // convert the filename to a class name
  183. $class = Util::mapFileNameToClassName(basename($filePath));
  184. if (isset($fileNames[$class])) {
  185. throw new InvalidArgumentException(sprintf('Migration "%s" has the same name as "%s"', basename($filePath), $fileNames[$class]));
  186. }
  187. $fileNames[$class] = basename($filePath);
  188. // load the migration file
  189. /** @noinspection PhpIncludeInspection */
  190. require_once $filePath;
  191. if (!class_exists($class)) {
  192. throw new InvalidArgumentException(sprintf('Could not find class "%s" in file "%s"', $class, $filePath));
  193. }
  194. // instantiate it
  195. //$this->input = new Input();
  196. //$this->output = new Output();
  197. $migration = new $class('production', $version, $this->input, $this->output);
  198. if (!($migration instanceof AbstractMigration)) {
  199. throw new InvalidArgumentException(sprintf('The class "%s" in file "%s" must extend \Phinx\Migration\AbstractMigration', $class, $filePath));
  200. }
  201. $versions[$version] = $migration;
  202. }
  203. }
  204. ksort($versions);
  205. $this->migrations = $versions;
  206. }
  207. return $this->migrations;
  208. }
  209. public function getCurrentVersion()
  210. {
  211. $versions = $this->getVersions();
  212. $version = 0;
  213. if (!empty($versions)) {
  214. $version = end($versions);
  215. }
  216. return $version;
  217. }
  218. public function migrateToDateTime(DateTime $dateTime)
  219. {
  220. $versions = array_keys($this->getMigrations());
  221. $dateString = $dateTime->format('YmdHis');
  222. $outstandingMigrations = array_filter($versions, function ($version) use ($dateString) {
  223. return $version <= $dateString;
  224. });
  225. if (count($outstandingMigrations) > 0) {
  226. $migration = max($outstandingMigrations);
  227. $this->writeln('Migrating to version ' . $migration);
  228. $this->migrate($migration);
  229. }
  230. }
  231. protected function writeln($content)
  232. {
  233. if ($this->output) {
  234. $this->output->writeln($content);
  235. }
  236. }
  237. public function migrate($version = null)
  238. {
  239. $path = $this->getPath();
  240. if (!is_dir($path)) {
  241. $this->writeln("$path not exists --> ignore");
  242. return false;
  243. }
  244. $migrations = $this->getMigrations();
  245. $versions = $this->getVersions();
  246. $current = $this->getCurrentVersion();
  247. if (empty($versions) && empty($migrations)) {
  248. return;
  249. }
  250. if (null === $version) {
  251. $version = max(array_merge($versions, array_keys($migrations)));
  252. } else if (0 != $version && !isset($migrations[$version])) {
  253. $this->writeln(sprintf('<comment>warning</comment> %s is not a valid version', $version));
  254. return;
  255. }
  256. // are we migrating up or down?
  257. $direction = $version > $current ? MigrationInterface::UP : MigrationInterface::DOWN;
  258. if ($direction === MigrationInterface::DOWN) {
  259. // run downs first
  260. krsort($migrations);
  261. foreach ($migrations as $migration) {
  262. if ($migration->getVersion() <= $version) {
  263. break;
  264. }
  265. if (in_array($migration->getVersion(), $versions)) {
  266. $this->executeMigration($migration, MigrationInterface::DOWN);
  267. }
  268. }
  269. }
  270. ksort($migrations);
  271. foreach ($migrations as $migration) {
  272. if ($migration->getVersion() > $version) {
  273. break;
  274. }
  275. if (!in_array($migration->getVersion(), $versions)) {
  276. $this->executeMigration($migration);
  277. }
  278. }
  279. }
  280. }