VersionManager.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. <?php
  2. namespace Dcat\Admin\Extend;
  3. use Carbon\Carbon;
  4. use Dcat\Admin\Models\Extension;
  5. use Dcat\Admin\Models\ExtensionHistory;
  6. use Dcat\Admin\Support\DatabaseUpdater;
  7. use Illuminate\Support\Arr;
  8. /**
  9. * Class VersionManager.
  10. *
  11. * @see https://github.com/octobercms/october/blob/develop/modules/system/classes/VersionManager.php
  12. */
  13. class VersionManager
  14. {
  15. use Note;
  16. const NO_VERSION_VALUE = 0;
  17. const HISTORY_TYPE_COMMENT = 1;
  18. const HISTORY_TYPE_SCRIPT = 2;
  19. protected $fileVersions;
  20. protected $databaseVersions;
  21. protected $databaseHistory;
  22. protected $updater;
  23. protected $manager;
  24. public function __construct(Manager $manager)
  25. {
  26. $this->manager = $manager;
  27. $this->updater = new DatabaseUpdater();
  28. }
  29. public function update($extension, $stopOnVersion = null)
  30. {
  31. $name = $this->manager->getName($extension);
  32. if (! $this->hasVersionFile($name)) {
  33. return false;
  34. }
  35. $currentVersion = $this->getLatestFileVersion($name);
  36. $databaseVersion = $this->getDatabaseVersion($name);
  37. if ($currentVersion === $databaseVersion) {
  38. $this->note('- <info>Nothing to update.</info>');
  39. return;
  40. }
  41. $this->manager->get($extension)->update($databaseVersion, $stopOnVersion ?: $currentVersion);
  42. $newUpdates = $this->getNewFileVersions($name, $databaseVersion);
  43. foreach ($newUpdates as $version => $details) {
  44. $this->applyExtensionUpdate($name, $version, $details);
  45. if ($stopOnVersion === $version) {
  46. return true;
  47. }
  48. }
  49. return true;
  50. }
  51. public function listNewVersions($extension)
  52. {
  53. $name = $this->manager->getName($extension);
  54. if (! $this->hasVersionFile($name)) {
  55. return [];
  56. }
  57. return $this->getNewFileVersions($name, $this->getDatabaseVersion($name));
  58. }
  59. protected function applyExtensionUpdate($name, $version, $details)
  60. {
  61. [$comments, $scripts] = $this->extractScriptsAndComments($details);
  62. foreach ($scripts as $script) {
  63. if ($this->hasDatabaseHistory($name, $version, $script)) {
  64. continue;
  65. }
  66. $this->applyDatabaseScript($name, $version, $script);
  67. }
  68. if (! $this->hasDatabaseHistory($name, $version)) {
  69. foreach ($comments as $comment) {
  70. $this->applyDatabaseComment($name, $version, $comment);
  71. $this->note(sprintf('- <info>v%s: </info> %s', $version, $comment));
  72. }
  73. }
  74. $this->setDatabaseVersion($name, $version);
  75. }
  76. public function remove($extension, $stopOnVersion = null, $stopCurrentVersion = false)
  77. {
  78. $name = $this->manager->getName($extension);
  79. if (! $this->hasVersionFile($name)) {
  80. return false;
  81. }
  82. $extensionHistory = $this->getDatabaseHistory($name);
  83. $extensionHistory = array_reverse($extensionHistory);
  84. $stopOnNextVersion = false;
  85. $newExtensionVersion = null;
  86. try {
  87. foreach ($extensionHistory as $history) {
  88. if ($stopCurrentVersion && $stopOnVersion === $history->version) {
  89. $newExtensionVersion = $history->version;
  90. break;
  91. }
  92. if ($stopOnNextVersion && $history->version !== $stopOnVersion) {
  93. $newExtensionVersion = $history->version;
  94. break;
  95. }
  96. if ($history->type == static::HISTORY_TYPE_COMMENT) {
  97. $this->removeDatabaseComment($name, $history->version);
  98. } elseif ($history->type == static::HISTORY_TYPE_SCRIPT) {
  99. $this->removeDatabaseScript($name, $history->version, $history->detail);
  100. }
  101. if ($stopOnVersion === $history->version) {
  102. $stopOnNextVersion = true;
  103. }
  104. }
  105. } catch (\Throwable $exception) {
  106. $lastHistory = $this->getLastHistory($name);
  107. if ($lastHistory) {
  108. $this->setDatabaseVersion($name, $lastHistory->version);
  109. }
  110. throw $exception;
  111. }
  112. $this->setDatabaseVersion($name, $newExtensionVersion);
  113. if (isset($this->fileVersions[$name])) {
  114. unset($this->fileVersions[$name]);
  115. }
  116. if (isset($this->databaseVersions[$name])) {
  117. unset($this->databaseVersions[$name]);
  118. }
  119. if (isset($this->databaseHistory[$name])) {
  120. unset($this->databaseHistory[$name]);
  121. }
  122. return true;
  123. }
  124. public function purge($name)
  125. {
  126. $name = $this->manager->getName($name);
  127. $versions = Extension::query()->where('name', $name);
  128. if ($countVersions = $versions->count()) {
  129. $versions->delete();
  130. }
  131. $history = ExtensionHistory::query()->where('name', $name);
  132. if ($countHistory = $history->count()) {
  133. $history->delete();
  134. }
  135. return $countHistory + $countVersions;
  136. }
  137. protected function getLatestFileVersion($name)
  138. {
  139. $versionInfo = $this->getFileVersions($name);
  140. if (! $versionInfo) {
  141. return static::NO_VERSION_VALUE;
  142. }
  143. return trim(key(array_slice($versionInfo, -1, 1)));
  144. }
  145. public function getNewFileVersions($name, $version = null)
  146. {
  147. $name = $this->manager->getName($name);
  148. if ($version === null) {
  149. $version = static::NO_VERSION_VALUE;
  150. }
  151. $versions = $this->getFileVersions($name);
  152. $position = array_search($version, array_keys($versions));
  153. return array_slice($versions, ++$position);
  154. }
  155. public function getFileVersions($name)
  156. {
  157. $name = $this->manager->getName($name);
  158. if ($this->fileVersions !== null && array_key_exists($name, $this->fileVersions)) {
  159. return $this->fileVersions[$name];
  160. }
  161. $versionInfo = (array) $this->parseVersionFile($this->getVersionFile($name));
  162. if ($versionInfo) {
  163. uksort($versionInfo, function ($a, $b) {
  164. return version_compare($a, $b);
  165. });
  166. }
  167. return $this->fileVersions[$name] = $versionInfo;
  168. }
  169. protected function parseVersionFile($file)
  170. {
  171. if ($file && is_file($file)) {
  172. return include $file;
  173. }
  174. }
  175. protected function getVersionFile($name)
  176. {
  177. return $this->manager->path($name, 'version.php');
  178. }
  179. protected function hasVersionFile($name)
  180. {
  181. $versionFile = $this->getVersionFile($name);
  182. return $versionFile && is_file($versionFile);
  183. }
  184. protected function getDatabaseVersion($name)
  185. {
  186. if ($this->databaseVersions === null) {
  187. $this->databaseVersions = Extension::query()->pluck('version', 'name');
  188. }
  189. if (! isset($this->databaseVersions[$name])) {
  190. $this->databaseVersions[$name] =
  191. Extension::query()
  192. ->where('name', $name)
  193. ->value('version');
  194. }
  195. return $this->databaseVersions[$name] ?? static::NO_VERSION_VALUE;
  196. }
  197. protected function setDatabaseVersion($name, $version = null)
  198. {
  199. $currentVersion = $this->getDatabaseVersion($name);
  200. if ($version && ! $currentVersion) {
  201. Extension::query()->create([
  202. 'name' => $name,
  203. 'version' => $version,
  204. ]);
  205. } elseif ($version && $currentVersion) {
  206. Extension::query()->where('name', $name)->update([
  207. 'version' => $version,
  208. 'updated_at' => new Carbon,
  209. ]);
  210. } elseif ($currentVersion) {
  211. Extension::query()->where('name', $name)->delete();
  212. }
  213. $this->databaseVersions[$name] = $version;
  214. }
  215. protected function applyDatabaseComment($name, $version, $comment)
  216. {
  217. ExtensionHistory::query()->create([
  218. 'name' => $name,
  219. 'type' => static::HISTORY_TYPE_COMMENT,
  220. 'version' => $version,
  221. 'detail' => $comment,
  222. ]);
  223. }
  224. protected function removeDatabaseComment($name, $version)
  225. {
  226. ExtensionHistory::query()
  227. ->where('name', $name)
  228. ->where('type', static::HISTORY_TYPE_COMMENT)
  229. ->where('version', $version)
  230. ->delete();
  231. }
  232. protected function applyDatabaseScript($name, $version, $script)
  233. {
  234. $updateFile = $this->manager->path($name, 'updates/'.$script);
  235. if (! is_file($updateFile)) {
  236. $this->note(sprintf('- <error>v%s: Migration file "%s" not found</error>', $version, $script));
  237. return;
  238. }
  239. $this->updater->setUp($this->resolveUpdater($name, $updateFile), function () use ($name, $version, $script) {
  240. ExtensionHistory::query()->create([
  241. 'name' => $name,
  242. 'type' => static::HISTORY_TYPE_SCRIPT,
  243. 'version' => $version,
  244. 'detail' => $script,
  245. ]);
  246. });
  247. $this->note(sprintf('- <info>v%s: Migrated</info> %s', $version, $script));
  248. }
  249. protected function resolveUpdater($name, $updateFile)
  250. {
  251. $updater = $this->updater->resolve($updateFile);
  252. if (method_exists($updater, 'setExtension')) {
  253. $updater->setExtension($this->manager->get($name));
  254. }
  255. return $updater;
  256. }
  257. protected function removeDatabaseScript($name, $version, $script)
  258. {
  259. $updateFile = $this->manager->path($name, 'updates/'.$script);
  260. $this->updater->packDown($this->resolveUpdater($name, $updateFile), function () use ($name, $version, $script) {
  261. ExtensionHistory::query()
  262. ->where('name', $name)
  263. ->where('type', static::HISTORY_TYPE_SCRIPT)
  264. ->where('version', $version)
  265. ->where('detail', $script)
  266. ->delete();
  267. });
  268. }
  269. protected function getDatabaseHistory($name)
  270. {
  271. if ($this->databaseHistory !== null && array_key_exists($name, $this->databaseHistory)) {
  272. return $this->databaseHistory[$name];
  273. }
  274. $historyInfo = ExtensionHistory::query()
  275. ->where('name', $name)
  276. ->orderBy('id')
  277. ->get()
  278. ->all();
  279. return $this->databaseHistory[$name] = $historyInfo;
  280. }
  281. protected function getLastHistory($name)
  282. {
  283. return ExtensionHistory::query()
  284. ->where('name', $name)
  285. ->orderByDesc('id')
  286. ->first();
  287. }
  288. protected function hasDatabaseHistory($name, $version, $script = null)
  289. {
  290. $historyInfo = $this->getDatabaseHistory($name);
  291. if (! $historyInfo) {
  292. return false;
  293. }
  294. foreach ($historyInfo as $history) {
  295. if ($history->version != $version) {
  296. continue;
  297. }
  298. if ($history->type == static::HISTORY_TYPE_COMMENT && ! $script) {
  299. return true;
  300. }
  301. if ($history->type == static::HISTORY_TYPE_SCRIPT && $history->detail == $script) {
  302. return true;
  303. }
  304. }
  305. return false;
  306. }
  307. protected function extractScriptsAndComments($details): array
  308. {
  309. $details = (array) $details;
  310. $fileNamePattern = "/^[a-z0-9\_\-\.\/\\\]+\.php$/i";
  311. $comments = array_values(array_filter($details, function ($detail) use ($fileNamePattern) {
  312. return ! preg_match($fileNamePattern, $detail);
  313. }));
  314. $scripts = array_values(array_filter($details, function ($detail) use ($fileNamePattern) {
  315. return preg_match($fileNamePattern, $detail);
  316. }));
  317. return [$comments, $scripts];
  318. }
  319. public function getCurrentVersion($extension): string
  320. {
  321. return $this->getDatabaseVersion($this->manager->getName($extension));
  322. }
  323. public function hasDatabaseVersion($extension, string $version): bool
  324. {
  325. $name = $this->manager->getName($extension);
  326. $histories = $this->getDatabaseHistory($name);
  327. foreach ($histories as $history) {
  328. if ($history->version === $version) {
  329. return true;
  330. }
  331. }
  332. return false;
  333. }
  334. public function getCurrentVersionNote($extension): string
  335. {
  336. $name = $this->manager->getName($extension);
  337. $histories = $this->getDatabaseHistory($name);
  338. $lastHistory = Arr::last(Arr::where($histories, function ($history) {
  339. return $history->type === static::HISTORY_TYPE_COMMENT;
  340. }));
  341. return $lastHistory ? $lastHistory->detail : '';
  342. }
  343. }