<?php namespace Dcat\Admin\Extend; use Carbon\Carbon; use Dcat\Admin\Models\Extension; use Dcat\Admin\Models\ExtensionHistory; use Dcat\Admin\Support\DatabaseUpdater; use Illuminate\Support\Arr; /** * Class VersionManager. * * @see https://github.com/octobercms/october/blob/develop/modules/system/classes/VersionManager.php */ class VersionManager { use Note; const NO_VERSION_VALUE = 0; const HISTORY_TYPE_COMMENT = 1; const HISTORY_TYPE_SCRIPT = 2; protected $fileVersions; protected $databaseVersions; protected $databaseHistory; protected $updater; protected $manager; public function __construct(Manager $manager) { $this->manager = $manager; $this->updater = new DatabaseUpdater(); } public function update($extension, $stopOnVersion = null) { $name = $this->manager->getName($extension); if (! $this->hasVersionFile($name)) { return false; } $currentVersion = $this->getLatestFileVersion($name); $databaseVersion = $this->getDatabaseVersion($name); if ($currentVersion === $databaseVersion) { $this->note('- <info>Nothing to update.</info>'); return; } $this->manager->get($extension)->update($databaseVersion, $stopOnVersion ?: $currentVersion); $newUpdates = $this->getNewFileVersions($name, $databaseVersion); foreach ($newUpdates as $version => $details) { $this->applyExtensionUpdate($name, $version, $details); if ($stopOnVersion === $version) { return true; } } return true; } public function listNewVersions($extension) { $name = $this->manager->getName($extension); if (! $this->hasVersionFile($name)) { return []; } return $this->getNewFileVersions($name, $this->getDatabaseVersion($name)); } protected function applyExtensionUpdate($name, $version, $details) { [$comments, $scripts] = $this->extractScriptsAndComments($details); foreach ($scripts as $script) { if ($this->hasDatabaseHistory($name, $version, $script)) { continue; } $this->applyDatabaseScript($name, $version, $script); } if (! $this->hasDatabaseHistory($name, $version)) { foreach ($comments as $comment) { $this->applyDatabaseComment($name, $version, $comment); $this->note(sprintf('- <info>v%s: </info> %s', $version, $comment)); } } $this->setDatabaseVersion($name, $version); } public function remove($extension, $stopOnVersion = null, $stopCurrentVersion = false) { $name = $this->manager->getName($extension); if (! $this->hasVersionFile($name)) { return false; } $extensionHistory = $this->getDatabaseHistory($name); $extensionHistory = array_reverse($extensionHistory); $stopOnNextVersion = false; $newExtensionVersion = null; try { foreach ($extensionHistory as $history) { if ($stopCurrentVersion && $stopOnVersion === $history->version) { $newExtensionVersion = $history->version; break; } if ($stopOnNextVersion && $history->version !== $stopOnVersion) { $newExtensionVersion = $history->version; break; } if ($history->type == static::HISTORY_TYPE_COMMENT) { $this->removeDatabaseComment($name, $history->version); } elseif ($history->type == static::HISTORY_TYPE_SCRIPT) { $this->removeDatabaseScript($name, $history->version, $history->detail); } if ($stopOnVersion === $history->version) { $stopOnNextVersion = true; } } } catch (\Throwable $exception) { $lastHistory = $this->getLastHistory($name); if ($lastHistory) { $this->setDatabaseVersion($name, $lastHistory->version); } throw $exception; } $this->setDatabaseVersion($name, $newExtensionVersion); if (isset($this->fileVersions[$name])) { unset($this->fileVersions[$name]); } if (isset($this->databaseVersions[$name])) { unset($this->databaseVersions[$name]); } if (isset($this->databaseHistory[$name])) { unset($this->databaseHistory[$name]); } return true; } public function purge($name) { $name = $this->manager->getName($name); $versions = Extension::query()->where('name', $name); if ($countVersions = $versions->count()) { $versions->delete(); } $history = ExtensionHistory::query()->where('name', $name); if ($countHistory = $history->count()) { $history->delete(); } return $countHistory + $countVersions; } protected function getLatestFileVersion($name) { $versionInfo = $this->getFileVersions($name); if (! $versionInfo) { return static::NO_VERSION_VALUE; } return trim(key(array_slice($versionInfo, -1, 1))); } public function getNewFileVersions($name, $version = null) { $name = $this->manager->getName($name); if ($version === null) { $version = static::NO_VERSION_VALUE; } $versions = $this->getFileVersions($name); $position = array_search($version, array_keys($versions)); return array_slice($versions, ++$position); } public function getFileVersions($name) { $name = $this->manager->getName($name); if ($this->fileVersions !== null && array_key_exists($name, $this->fileVersions)) { return $this->fileVersions[$name]; } $versionInfo = (array) $this->parseVersionFile($this->getVersionFile($name)); if ($versionInfo) { uksort($versionInfo, function ($a, $b) { return version_compare($a, $b); }); } return $this->fileVersions[$name] = $versionInfo; } protected function parseVersionFile($file) { if ($file && is_file($file)) { return include $file; } } protected function getVersionFile($name) { return $this->manager->path($name, 'version.php'); } protected function hasVersionFile($name) { $versionFile = $this->getVersionFile($name); return $versionFile && is_file($versionFile); } protected function getDatabaseVersion($name) { if ($this->databaseVersions === null) { $this->databaseVersions = Extension::query()->pluck('version', 'name'); } if (! isset($this->databaseVersions[$name])) { $this->databaseVersions[$name] = Extension::query() ->where('name', $name) ->value('version'); } return $this->databaseVersions[$name] ?? static::NO_VERSION_VALUE; } protected function setDatabaseVersion($name, $version = null) { $currentVersion = $this->getDatabaseVersion($name); if ($version && ! $currentVersion) { Extension::query()->create([ 'name' => $name, 'version' => $version, ]); } elseif ($version && $currentVersion) { Extension::query()->where('name', $name)->update([ 'version' => $version, 'updated_at' => new Carbon, ]); } elseif ($currentVersion) { Extension::query()->where('name', $name)->delete(); } $this->databaseVersions[$name] = $version; } protected function applyDatabaseComment($name, $version, $comment) { ExtensionHistory::query()->create([ 'name' => $name, 'type' => static::HISTORY_TYPE_COMMENT, 'version' => $version, 'detail' => $comment, ]); } protected function removeDatabaseComment($name, $version) { ExtensionHistory::query() ->where('name', $name) ->where('type', static::HISTORY_TYPE_COMMENT) ->where('version', $version) ->delete(); } protected function applyDatabaseScript($name, $version, $script) { $updateFile = $this->manager->path($name, 'updates/'.$script); if (! is_file($updateFile)) { $this->note(sprintf('- <error>v%s: Migration file "%s" not found</error>', $version, $script)); return; } $this->updater->setUp($this->resolveUpdater($name, $updateFile), function () use ($name, $version, $script) { ExtensionHistory::query()->create([ 'name' => $name, 'type' => static::HISTORY_TYPE_SCRIPT, 'version' => $version, 'detail' => $script, ]); }); $this->note(sprintf('- <info>v%s: Migrated</info> %s', $version, $script)); } protected function resolveUpdater($name, $updateFile) { $updater = $this->updater->resolve($updateFile); if (method_exists($updater, 'setExtension')) { $updater->setExtension($this->manager->get($name)); } return $updater; } protected function removeDatabaseScript($name, $version, $script) { $updateFile = $this->manager->path($name, 'updates/'.$script); $this->updater->packDown($this->resolveUpdater($name, $updateFile), function () use ($name, $version, $script) { ExtensionHistory::query() ->where('name', $name) ->where('type', static::HISTORY_TYPE_SCRIPT) ->where('version', $version) ->where('detail', $script) ->delete(); }); } protected function getDatabaseHistory($name) { if ($this->databaseHistory !== null && array_key_exists($name, $this->databaseHistory)) { return $this->databaseHistory[$name]; } $historyInfo = ExtensionHistory::query() ->where('name', $name) ->orderBy('id') ->get() ->all(); return $this->databaseHistory[$name] = $historyInfo; } protected function getLastHistory($name) { return ExtensionHistory::query() ->where('name', $name) ->orderByDesc('id') ->first(); } protected function hasDatabaseHistory($name, $version, $script = null) { $historyInfo = $this->getDatabaseHistory($name); if (! $historyInfo) { return false; } foreach ($historyInfo as $history) { if ($history->version != $version) { continue; } if ($history->type == static::HISTORY_TYPE_COMMENT && ! $script) { return true; } if ($history->type == static::HISTORY_TYPE_SCRIPT && $history->detail == $script) { return true; } } return false; } protected function extractScriptsAndComments($details): array { $details = (array) $details; $fileNamePattern = "/^[a-z0-9\_\-\.\/\\\]+\.php$/i"; $comments = array_values(array_filter($details, function ($detail) use ($fileNamePattern) { return ! preg_match($fileNamePattern, $detail); })); $scripts = array_values(array_filter($details, function ($detail) use ($fileNamePattern) { return preg_match($fileNamePattern, $detail); })); return [$comments, $scripts]; } public function getCurrentVersion($extension): string { return $this->getDatabaseVersion($this->manager->getName($extension)); } public function hasDatabaseVersion($extension, string $version): bool { $name = $this->manager->getName($extension); $histories = $this->getDatabaseHistory($name); foreach ($histories as $history) { if ($history->version === $version) { return true; } } return false; } public function getCurrentVersionNote($extension): string { $name = $this->manager->getName($extension); $histories = $this->getDatabaseHistory($name); $lastHistory = Arr::last(Arr::where($histories, function ($history) { return $history->type === static::HISTORY_TYPE_COMMENT; })); return $lastHistory ? $lastHistory->detail : ''; } }