jqh 5 年之前
父节点
当前提交
1e09063a9a
共有 43 个文件被更改,包括 944 次插入51 次删除
  1. 1 1
      config/admin.php
  2. 7 3
      database/migrations/2020_09_22_015815_create_admin_extensions_table.php
  3. 3 2
      src/Admin.php
  4. 8 0
      src/AdminServiceProvider.php
  5. 9 0
      src/Exception/AdminException.php
  6. 1 1
      src/Exception/Handler.php
  7. 7 0
      src/Exception/InvalidArgumentException.php
  8. 7 0
      src/Exception/RuntimeException.php
  9. 70 3
      src/Extend/Manager.php
  10. 27 0
      src/Extend/Note.php
  11. 2 1
      src/Extend/ServiceProvider.php
  12. 109 1
      src/Extend/UpdateManager.php
  13. 423 0
      src/Extend/VersionManager.php
  14. 2 1
      src/Form/Field/CanCascadeFields.php
  15. 3 1
      src/Form/Field/Captcha.php
  16. 2 1
      src/Form/Field/ImageField.php
  17. 2 1
      src/Form/Field/Select.php
  18. 2 1
      src/Grid/Column.php
  19. 2 1
      src/Grid/Column/HasDisplayers.php
  20. 2 2
      src/Grid/Column/HasHeader.php
  21. 2 1
      src/Grid/Concerns/HasComplexHeaders.php
  22. 2 1
      src/Grid/Exporters/ExcelExporter.php
  23. 2 1
      src/Grid/Filter.php
  24. 2 1
      src/Grid/Filter/AbstractFilter.php
  25. 2 1
      src/Grid/Filter/Presenter/Select.php
  26. 2 1
      src/Grid/Model.php
  27. 2 1
      src/Http/Middleware/Permission.php
  28. 4 3
      src/Layout/Content.php
  29. 2 1
      src/Layout/SectionManager.php
  30. 9 0
      src/Models/Extension.php
  31. 9 0
      src/Models/ExtensionHistory.php
  32. 3 2
      src/Models/MenuCache.php
  33. 9 0
      src/Models/Setting.php
  34. 3 2
      src/Repositories/EloquentRepository.php
  35. 9 8
      src/Repositories/QueryBuilderRepository.php
  36. 2 1
      src/Scaffold/ControllerCreator.php
  37. 2 1
      src/Scaffold/MigrationCreator.php
  38. 2 1
      src/Scaffold/ModelCreator.php
  39. 2 1
      src/Show/Field.php
  40. 178 0
      src/Support/DatabaseUpdater.php
  41. 2 1
      src/Traits/ModelTree.php
  42. 3 2
      src/Tree.php
  43. 2 1
      src/Widgets/Form.php

+ 1 - 1
config/admin.php

@@ -296,7 +296,7 @@ return [
     |--------------------------------------------------------------------------
     |
     */
-    'exception_handler' => Dcat\Admin\Http\Exception\Handler::class,
+    'exception_handler' => Dcat\Admin\Exception\Handler::class,
 
     /*
     |--------------------------------------------------------------------------

+ 7 - 3
database/migrations/2020_09_22_015815_create_admin_extensions_table.php

@@ -15,22 +15,26 @@ class CreateAdminExtensionsTable extends Migration
     {
         Schema::create('admin_extensions', function (Blueprint $table) {
             $table->increments('id')->unsigned();
-            $table->string('slug', 100)->unique();
+            $table->string('name', 100)->unique();
             $table->string('version', 20)->default('');
             $table->tinyInteger('is_enabled')->default(0);
             $table->json('options')->nullable();
             $table->timestamps();
+
+            $table->engine = 'InnoDB';
         });
 
         Schema::create('admin_extension_histories', function (Blueprint $table) {
             $table->bigIncrements('id')->unsigned();
-            $table->string('slug', 100);
+            $table->string('name', 100);
             $table->tinyInteger('type')->default(1);
             $table->string('version', 20)->default(0);
             $table->text('description')->nullable();
 
-            $table->index('slug');
+            $table->index('name');
             $table->timestamps();
+
+            $table->engine = 'InnoDB';
         });
     }
 

+ 3 - 2
src/Admin.php

@@ -4,8 +4,9 @@ namespace Dcat\Admin;
 
 use Closure;
 use Dcat\Admin\Contracts\Repository;
+use Dcat\Admin\Exception\InvalidArgumentException;
 use Dcat\Admin\Http\Controllers\AuthController;
-use Dcat\Admin\Http\Exception\Handler;
+use Dcat\Admin\Exception\Handler;
 use Dcat\Admin\Layout\Menu;
 use Dcat\Admin\Layout\Navbar;
 use Dcat\Admin\Layout\SectionManager;
@@ -220,7 +221,7 @@ class Admin
         if (! $repository instanceof Repository) {
             $class = is_object($repository) ? get_class($repository) : $repository;
 
-            throw new \InvalidArgumentException("The class [{$class}] must be a type of [".Repository::class.'].');
+            throw new InvalidArgumentException("The class [{$class}] must be a type of [".Repository::class.'].');
         }
 
         return $repository;

+ 8 - 0
src/AdminServiceProvider.php

@@ -2,6 +2,8 @@
 
 namespace Dcat\Admin;
 
+use Dcat\Admin\Extend\UpdateManager;
+use Dcat\Admin\Extend\VersionManager;
 use Dcat\Admin\Layout\Asset;
 use Dcat\Admin\Layout\Content;
 use Dcat\Admin\Layout\Menu;
@@ -208,6 +210,12 @@ class AdminServiceProvider extends ServiceProvider
         $this->app->singleton('admin.color', Color::class);
         $this->app->singleton('admin.sections', SectionManager::class);
         $this->app->singleton('admin.extend', Manager::class);
+        $this->app->singleton('admin.extend.update', function () {
+            return new UpdateManager(app('admin.extend'));
+        });
+        $this->app->singleton('admin.extend.version', function () {
+            return new VersionManager(app('admin.extend'));
+        });
         $this->app->singleton('admin.navbar', Navbar::class);
         $this->app->singleton('admin.menu', Menu::class);
         $this->app->singleton('admin.context', Context::class);

+ 9 - 0
src/Exception/AdminException.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace Dcat\Admin\Exception;
+
+use Exception;
+
+class AdminException extends Exception
+{
+}

+ 1 - 1
src/Http/Exception/Handler.php → src/Exception/Handler.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace Dcat\Admin\Http\Exception;
+namespace Dcat\Admin\Exception;
 
 use Dcat\Admin\Support\Helper;
 use Illuminate\Support\MessageBag;

+ 7 - 0
src/Exception/InvalidArgumentException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Dcat\Admin\Exception;
+
+class InvalidArgumentException extends AdminException
+{
+}

+ 7 - 0
src/Exception/RuntimeException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Dcat\Admin\Exception;
+
+class RuntimeException extends AdminException
+{
+}

+ 70 - 3
src/Extend/Manager.php

@@ -12,6 +12,8 @@ use RecursiveIteratorIterator;
 
 class Manager
 {
+    use Note;
+
     /**
      * @var Container
      */
@@ -87,6 +89,41 @@ class Manager
         }
     }
 
+    /**
+     * 获取扩展路径.
+     *
+     * @param string|ServiceProvider $name
+     * @param string|null            $path
+     *
+     * @return string|void
+     *
+     * @throws \ReflectionException
+     */
+    public function path($name, $path = null)
+    {
+        if (! $extension = $this->get($name)) {
+            return;
+        }
+
+        return $extension->path($path);
+    }
+
+    /**
+     * 获取扩展对象.
+     *
+     * @param string|ServiceProvider $name
+     *
+     * @return ServiceProvider|null
+     */
+    public function get($name)
+    {
+        if ($name instanceof ServiceProvider) {
+            return $name;
+        }
+
+        return $this->extensions->get($name);
+    }
+
     /**
      * 获取所有扩展.
      *
@@ -104,9 +141,7 @@ class Manager
      */
     public function availableExtensions()
     {
-        return $this->extensions()->filter(function (ServiceProvider $extension) {
-            return $this->enabled($extension->getName());
-        });
+        return $this->extensions()->filter->enabled();
     }
 
     /**
@@ -202,6 +237,22 @@ class Manager
         $this->extensions->put($serviceProvider->getName(), $serviceProvider);
     }
 
+    /**
+     * 获取扩展名称.
+     *
+     * @param $extension
+     *
+     * @return string
+     */
+    public function getName($extension)
+    {
+        if ($extension instanceof ServiceProvider) {
+            return $extension->getName();
+        }
+
+        return $extension;
+    }
+
     /**
      * 获取配置.
      *
@@ -222,6 +273,22 @@ class Manager
         return $this->settings;
     }
 
+    /**
+     * @return UpdateManager
+     */
+    public function updateManager()
+    {
+        return app('admin.extend.update');
+    }
+
+    /**
+     * @return VersionManager
+     */
+    public function versionManager()
+    {
+        return app('admin.extend.version');
+    }
+
     protected function registerPsr4($directory, array $psr4)
     {
         $classLoader = Admin::classLoader();

+ 27 - 0
src/Extend/Note.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace Dcat\Admin\Extend;
+
+use Symfony\Component\Console\Output\OutputInterface;
+
+trait Note
+{
+    /**
+     * @var \Symfony\Component\Console\Output\OutputInterface
+     */
+    public $output;
+
+    /**
+     * @var array
+     */
+    public $notes = [];
+
+    public function note($message)
+    {
+        if ($this->output instanceof OutputInterface) {
+            $this->output->writeln($message);
+        } else {
+            $this->notes[] = $message;
+        }
+    }
+}

+ 2 - 1
src/Extend/ServiceProvider.php

@@ -3,6 +3,7 @@
 namespace Dcat\Admin\Extend;
 
 use Dcat\Admin\Admin;
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Support\ComposerProperty;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\Validator;
@@ -125,7 +126,7 @@ abstract class ServiceProvider extends LaravelServiceProvider
             $this->path = realpath(dirname((new \ReflectionClass(static::class))->getFileName()).'/..');
 
             if (! is_dir($this->path)) {
-                throw new \Exception("The {$this->path} is not a directory.");
+                throw new RuntimeException("The {$this->path} is not a directory.");
             }
         }
 

+ 109 - 1
src/Extend/UpdateManager.php

@@ -1,3 +1,111 @@
 <?php
 
-//namespace
+namespace Dcat\Admin\Extend;
+
+use Dcat\Admin\Exception\AdminException;
+use Illuminate\Support\Facades\Artisan;
+use Illuminate\Support\Facades\Lang;
+
+/**
+ * Class UpdateManager
+ *
+ * @see https://github.com/octobercms/october/blob/develop/modules/system/classes/UpdateManager.php
+ */
+class UpdateManager
+{
+    use Note;
+
+    /**
+     * @var Manager
+     */
+    protected $manager;
+
+    /**
+     * @var VersionManager
+     */
+    protected $versionManager;
+
+    public function __construct(Manager $manager)
+    {
+        $this->manager = $manager;
+        $this->versionManager = $manager->versionManager();
+    }
+
+    public function install($extension)
+    {
+        return $this->update($extension);
+    }
+    
+    public function uninstall($extension)
+    {
+        return $this->rollback($extension);
+    }
+
+    public function rollback($name, ?string $stopOnVersion = null)
+    {
+        /*
+         * Remove the plugin database and version
+         */
+        if (! ($extension = $this->manager->get($name))
+            && $this->versionManager->purge($name)
+        ) {
+            $this->note('<info>Purged from database:</info> ' . $name);
+
+            return $this;
+        }
+
+        if ($stopOnVersion && !$this->versionManager->hasDatabaseVersion($extension, $stopOnVersion)) {
+            throw new AdminException('Extension version not found');
+        }
+
+        if ($this->versionManager->remove($extension, $stopOnVersion, true)) {
+            $this->note('<info>Rolled back:</info> ' . $name);
+
+            if ($currentVersion = $this->versionManager->getCurrentVersion($extension)) {
+                $this->note('<info>Current Version:</info> '.$currentVersion.' ('.$this->versionManager->getCurrentVersionNote($extension).')');
+            }
+
+            return $this;
+        }
+
+        $this->note('<error>Unable to find:</error> '.$name);
+
+        return $this;
+    }
+
+    public function update($name, ?string $stopOnVersion = null)
+    {
+        $name = $this->manager->getName($name);
+
+        if (! ($extension = $this->manager->get($name))) {
+            $this->note('<error>Unable to find:</error> '.$name);
+
+            return;
+        }
+
+        $this->note($name);
+
+        $this->versionUpdate($extension, $stopOnVersion);
+
+        $this->publishAssets($name);
+
+        return $this;
+    }
+
+    protected function publishAssets($name)
+    {
+        Artisan::call('vendor:publish', ['--force' => true, '--tag' => $name]);
+    }
+
+    protected function versionUpdate($extension, $stopOnVersion)
+    {
+        $this->versionManager->notes = [];
+        $this->versionManager->output = $this->output;
+
+        if ($this->versionManager->update($extension, $stopOnVersion) !== false) {
+            foreach ($this->versionManager->notes as $note) {
+                $this->note($note);
+            }
+        }
+    }
+}

+ 423 - 0
src/Extend/VersionManager.php

@@ -2,6 +2,429 @@
 
 namespace Dcat\Admin\Extend;
 
+use Dcat\Admin\Models\Extension;
+use Dcat\Admin\Models\ExtensionHistory;
+use Carbon\Carbon;
+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;
+        }
+
+        $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 = is_string($extension) ? $extension : $this->manager->getIdentifier($extension);
+
+        if (!$this->hasVersionFile($name)) {
+            return false;
+        }
+
+        $extensionHistory = $this->getDatabaseHistory($name);
+        $extensionHistory = array_reverse($extensionHistory);
+
+        $stopOnNextVersion = false;
+        $newPluginVersion = null;
+
+        try {
+            foreach ($extensionHistory as $history) {
+                if ($stopCurrentVersion && $stopOnVersion === $history->version) {
+                    $newPluginVersion = $history->version;
+
+                    break;
+                }
+
+                if ($stopOnNextVersion && $history->version !== $stopOnVersion) {
+                    // Stop if the $stopOnVersion value was found and
+                    // this is a new version. The history could contain
+                    // multiple items for a single version (comments and scripts).
+                    $newPluginVersion = $history->version;
+
+                    break;
+                }
+
+                if ($history->type == self::HISTORY_TYPE_COMMENT) {
+                    $this->removeDatabaseComment($name, $history->version);
+                } elseif ($history->type == self::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, $newPluginVersion);
+
+        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 self::NO_VERSION_VALUE;
+        }
+
+        return trim(key(array_slice($versionInfo, -1, 1)));
+    }
+
+    public function getNewFileVersions($name, $version = null)
+    {
+        if ($version === null) {
+            $version = self::NO_VERSION_VALUE;
+        }
+
+        $versions = $this->getFileVersions($name);
+
+        $position = array_search($version, array_keys($versions));
+
+        return array_slice($versions, ++$position);
+    }
+
+    public function getFileVersions($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] ?? self::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'    => self::HISTORY_TYPE_COMMENT,
+            'version' => $version,
+            'detail'  => $comment,
+        ]);
+    }
+
+    protected function removeDatabaseComment($name, $version)
+    {
+        ExtensionHistory::query()
+            ->where('name', $name)
+            ->where('type', self::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('- <error>v'.$version.':  Migration file "'.$script.'" not found</error>');
+
+            return;
+        }
+
+        $this->updater->setUp($updateFile, function () use ($name, $version, $script) {
+            ExtensionHistory::query()->create([
+                'name'    => $name,
+                'type'    => self::HISTORY_TYPE_SCRIPT,
+                'version' => $version,
+                'detail'  => $script,
+            ]);
+        });
+    }
+
+    protected function removeDatabaseScript($name, $version, $script)
+    {
+        $updateFile = $this->manager->path($name, 'updates/'.$script);
+
+        $this->updater->packDown($updateFile, function () use ($name, $version, $script) {
+            ExtensionHistory::query()
+                ->where('name', $name)
+                ->where('type', self::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 == self::HISTORY_TYPE_COMMENT && ! $script) {
+                return true;
+            }
+
+            if ($history->type == self::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 === self::HISTORY_TYPE_COMMENT;
+        }));
+
+        return $lastHistory ? $lastHistory->detail : '';
+    }
 }

+ 2 - 1
src/Form/Field/CanCascadeFields.php

@@ -3,6 +3,7 @@
 namespace Dcat\Admin\Form\Field;
 
 use Dcat\Admin\Admin;
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Form;
 use Illuminate\Support\Arr;
 
@@ -219,7 +220,7 @@ var checked = $('{$this->getElementClassSelector()}:checked').map(function(){
 }).get();
 JS;
             default:
-                throw new \InvalidArgumentException('Invalid form field type');
+                throw new RuntimeException('Invalid form field type');
         }
     }
 }

+ 3 - 1
src/Form/Field/Captcha.php

@@ -2,6 +2,8 @@
 
 namespace Dcat\Admin\Form\Field;
 
+use Dcat\Admin\Exception\AdminException;
+
 class Captcha extends Text
 {
     protected $rules = ['required', 'captcha'];
@@ -11,7 +13,7 @@ class Captcha extends Text
     public function __construct()
     {
         if (! class_exists(\Mews\Captcha\Captcha::class)) {
-            throw new \Exception('To use captcha field, please install [mews/captcha] first.');
+            throw new AdminException('To use captcha field, please install [mews/captcha] first.');
         }
 
         $this->column = '__captcha__';

+ 2 - 1
src/Form/Field/ImageField.php

@@ -2,6 +2,7 @@
 
 namespace Dcat\Admin\Form\Field;
 
+use Dcat\Admin\Exception\AdminException;
 use Illuminate\Support\Str;
 use Intervention\Image\Constraint;
 use Intervention\Image\Facades\Image as InterventionImage;
@@ -81,7 +82,7 @@ trait ImageField
         }
 
         if (! class_exists(ImageManagerStatic::class)) {
-            throw new \Exception('To use image handling and manipulation, please install [intervention/image] first.');
+            throw new AdminException('To use image handling and manipulation, please install [intervention/image] first.');
         }
 
         $this->interventionCalls[] = [

+ 2 - 1
src/Form/Field/Select.php

@@ -3,6 +3,7 @@
 namespace Dcat\Admin\Form\Field;
 
 use Dcat\Admin\Admin;
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Form\Field;
 use Dcat\Admin\Support\Helper;
 use Illuminate\Database\Eloquent\Model;
@@ -152,7 +153,7 @@ class Select extends Field
         if (! class_exists($model)
             || ! in_array(Model::class, class_parents($model))
         ) {
-            throw new \InvalidArgumentException("[$model] must be a valid model class");
+            throw new RuntimeException("[$model] must be a valid model class");
         }
 
         $this->options = function ($value) use ($model, $idField, $textField) {

+ 2 - 1
src/Grid/Column.php

@@ -3,6 +3,7 @@
 namespace Dcat\Admin\Grid;
 
 use Closure;
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Grid;
 use Dcat\Admin\Grid\Displayers\AbstractDisplayer;
 use Dcat\Admin\Support\Helper;
@@ -621,7 +622,7 @@ class Column
         }
 
         if (! class_exists($class) || ! is_subclass_of($class, AbstractDisplayer::class)) {
-            throw new \Exception("Invalid column definition [$class]");
+            throw new RuntimeException("Invalid column definition [$class]");
         }
 
         $this->displayUsing($class);

+ 2 - 1
src/Grid/Column/HasDisplayers.php

@@ -3,6 +3,7 @@
 namespace Dcat\Admin\Grid\Column;
 
 use Dcat\Admin\Admin;
+use Dcat\Admin\Exception\InvalidArgumentException;
 use Dcat\Admin\Grid;
 use Dcat\Admin\Grid\Column;
 use Dcat\Admin\Grid\Displayers\AbstractDisplayer;
@@ -262,7 +263,7 @@ trait HasDisplayers
     public function action($action)
     {
         if (! is_subclass_of($action, RowAction::class)) {
-            throw new \InvalidArgumentException("Action class [$action] must be sub-class of [Dcat\Admin\Grid\RowAction]");
+            throw new InvalidArgumentException("Action class [$action] must be sub-class of [Dcat\Admin\Grid\RowAction]");
         }
 
         $grid = $this->grid;

+ 2 - 2
src/Grid/Column/HasHeader.php

@@ -2,8 +2,8 @@
 
 namespace Dcat\Admin\Grid\Column;
 
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Grid;
-use Dcat\Admin\Grid\Column;
 use Dcat\Admin\Grid\Model;
 use Dcat\Admin\Support\Helper;
 use Illuminate\Contracts\Support\Htmlable;
@@ -92,7 +92,7 @@ trait HasHeader
         }
 
         if (! $filter instanceof Grid\Column\Filter) {
-            throw new \InvalidArgumentException('The "$filter" must be a type of '.Grid\Column\Filter::class.'.');
+            throw new RuntimeException('The "$filter" must be a type of '.Grid\Column\Filter::class.'.');
         }
 
         return $this->addHeader($filter);

+ 2 - 1
src/Grid/Concerns/HasComplexHeaders.php

@@ -2,6 +2,7 @@
 
 namespace Dcat\Admin\Grid\Concerns;
 
+use Dcat\Admin\Exception\InvalidArgumentException;
 use Dcat\Admin\Grid\Column;
 use Dcat\Admin\Grid\ComplexHeader;
 use Illuminate\Support\Collection;
@@ -24,7 +25,7 @@ trait HasComplexHeaders
     public function combine(string $label, array $columnNames)
     {
         if (count($columnNames) < 2) {
-            throw new \InvalidArgumentException('Invalid column names.');
+            throw new InvalidArgumentException('Invalid column names.');
         }
 
         if (! $this->complexHeaders) {

+ 2 - 1
src/Grid/Exporters/ExcelExporter.php

@@ -2,6 +2,7 @@
 
 namespace Dcat\Admin\Grid\Exporters;
 
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Grid;
 use Dcat\EasyExcel\Excel;
 
@@ -12,7 +13,7 @@ class ExcelExporter extends AbstractExporter
         parent::__construct($titles);
 
         if (! class_exists(Excel::class)) {
-            throw new \Exception('To use exporter, please install [dcat/easy-excel] first.');
+            throw new RuntimeException('To use exporter, please install [dcat/easy-excel] first.');
         }
     }
 

+ 2 - 1
src/Grid/Filter.php

@@ -3,6 +3,7 @@
 namespace Dcat\Admin\Grid;
 
 use Dcat\Admin\Admin;
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Grid\Filter\AbstractFilter;
 use Dcat\Admin\Grid\Filter\Between;
 use Dcat\Admin\Grid\Filter\Date;
@@ -776,7 +777,7 @@ class Filter implements Renderable
         if (! empty(static::$supports[$method])) {
             $class = static::$supports[$method];
             if (! is_subclass_of($class, AbstractFilter::class)) {
-                throw new \InvalidArgumentException("The class [{$class}] must be a type of ".AbstractFilter::class.'.');
+                throw new RuntimeException("The class [{$class}] must be a type of ".AbstractFilter::class.'.');
             }
 
             return $this->addFilter(new $class(...$arguments));

+ 2 - 1
src/Grid/Filter/AbstractFilter.php

@@ -3,6 +3,7 @@
 namespace Dcat\Admin\Grid\Filter;
 
 use Dcat\Admin\Admin;
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Grid\Filter;
 use Dcat\Admin\Grid\Filter\Presenter\Checkbox;
 use Dcat\Admin\Grid\Filter\Presenter\DateTime;
@@ -654,7 +655,7 @@ abstract class AbstractFilter
             return $this->presenter()->{$method}(...$params);
         }
 
-        throw new \BadMethodCallException(sprintf(
+        throw new RuntimeException(sprintf(
             'Call to undefined method %s::%s()', static::class, $method
         ));
     }

+ 2 - 1
src/Grid/Filter/Presenter/Select.php

@@ -2,6 +2,7 @@
 
 namespace Dcat\Admin\Grid\Filter\Presenter;
 
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Support\Helper;
 use Illuminate\Contracts\Support\Arrayable;
 use Illuminate\Database\Eloquent\Model;
@@ -122,7 +123,7 @@ class Select extends Presenter
         if (! class_exists($model)
             || ! in_array(Model::class, class_parents($model))
         ) {
-            throw new \InvalidArgumentException("[$model] must be a valid model class");
+            throw new RuntimeException("[$model] must be a valid model class");
         }
 
         $this->options = function ($value) use ($model, $idField, $textField) {

+ 2 - 1
src/Grid/Model.php

@@ -3,6 +3,7 @@
 namespace Dcat\Admin\Grid;
 
 use Dcat\Admin\Admin;
+use Dcat\Admin\Exception\AdminException;
 use Dcat\Admin\Grid;
 use Dcat\Admin\Middleware\Pjax;
 use Dcat\Admin\Repositories\Repository;
@@ -485,7 +486,7 @@ class Model
             return $results->getCollection();
         }
 
-        throw new \Exception('Grid query error');
+        throw new AdminException('Grid query error');
     }
 
     /**

+ 2 - 1
src/Http/Middleware/Permission.php

@@ -4,6 +4,7 @@ namespace Dcat\Admin\Http\Middleware;
 
 use Dcat\Admin\Admin;
 use Dcat\Admin\Auth\Permission as Checker;
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Support\Helper;
 use Illuminate\Http\Request;
 use Illuminate\Support\Str;
@@ -69,7 +70,7 @@ class Permission
         $method = array_shift($args);
 
         if (! method_exists(Checker::class, $method)) {
-            throw new \InvalidArgumentException("Invalid permission method [$method].");
+            throw new RuntimeException("Invalid permission method [$method].");
         }
 
         call_user_func_array([Checker::class, $method], [$args]);

+ 4 - 3
src/Layout/Content.php

@@ -4,6 +4,7 @@ namespace Dcat\Admin\Layout;
 
 use Closure;
 use Dcat\Admin\Admin;
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Traits\HasBuilderEvents;
 use Illuminate\Contracts\Support\Renderable;
 use Illuminate\Support\Str;
@@ -161,21 +162,21 @@ class Content implements Renderable
     protected function formatBreadcrumb(array &$breadcrumb)
     {
         if (! $breadcrumb) {
-            throw new \Exception('Breadcrumb format error!');
+            throw new RuntimeException('Breadcrumb format error!');
         }
 
         $notArray = false;
         foreach ($breadcrumb as &$item) {
             $isArray = is_array($item);
             if ($isArray && ! isset($item['text'])) {
-                throw new \Exception('Breadcrumb format error!');
+                throw new RuntimeException('Breadcrumb format error!');
             }
             if (! $isArray && $item) {
                 $notArray = true;
             }
         }
         if (! $breadcrumb) {
-            throw new \Exception('Breadcrumb format error!');
+            throw new RuntimeException('Breadcrumb format error!');
         }
         if ($notArray) {
             $breadcrumb = [

+ 2 - 1
src/Layout/SectionManager.php

@@ -2,6 +2,7 @@
 
 namespace Dcat\Admin\Layout;
 
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Support\Helper;
 use Illuminate\Contracts\Support\Htmlable;
 use Illuminate\Contracts\Support\Renderable;
@@ -64,7 +65,7 @@ class SectionManager
     protected function put(string $section, $content, bool $append = false, int $priority = 10)
     {
         if (! $section) {
-            throw new \InvalidArgumentException('Section name is required.');
+            throw new RuntimeException('Section name is required.');
         }
 
         if (! isset($this->sections[$section])) {

+ 9 - 0
src/Models/Extension.php

@@ -7,4 +7,13 @@ use Illuminate\Database\Eloquent\Model;
 class Extension extends Model
 {
     protected $table = 'admin_extensions';
+
+    public function __construct(array $attributes = [])
+    {
+        $connection = config('admin.database.connection') ?: config('database.default');
+
+        $this->setConnection($connection);
+
+        parent::__construct($attributes);
+    }
 }

+ 9 - 0
src/Models/ExtensionHistory.php

@@ -7,4 +7,13 @@ use Illuminate\Database\Eloquent\Model;
 class ExtensionHistory extends Model
 {
     protected $table = 'admin_extension_histories';
+
+    public function __construct(array $attributes = [])
+    {
+        $connection = config('admin.database.connection') ?: config('database.default');
+
+        $this->setConnection($connection);
+
+        parent::__construct($attributes);
+    }
 }

+ 3 - 2
src/Models/MenuCache.php

@@ -2,11 +2,12 @@
 
 namespace Dcat\Admin\Models;
 
+use Dcat\Admin\Admin;
 use Illuminate\Support\Facades\Cache;
 
 trait MenuCache
 {
-    protected $cacheKey = 'dcat-admin-menus-%d';
+    protected $cacheKey = 'dcat-admin-menus-%d-%s';
 
     /**
      * Get an item from the cache, or execute the given Closure and store the result.
@@ -41,7 +42,7 @@ trait MenuCache
      */
     protected function getCacheKey()
     {
-        return sprintf($this->cacheKey, (int) static::withPermission());
+        return sprintf($this->cacheKey, (int) static::withPermission(), Admin::app()->getName());
     }
 
     /**

+ 9 - 0
src/Models/Setting.php

@@ -10,4 +10,13 @@ class Setting extends Model
     protected $primaryKey = 'slug';
     public $incrementing = false;
     protected $fillable = ['slug', 'value'];
+
+    public function __construct(array $attributes = [])
+    {
+        $connection = config('admin.database.connection') ?: config('database.default');
+
+        $this->setConnection($connection);
+
+        parent::__construct($attributes);
+    }
 }

+ 3 - 2
src/Repositories/EloquentRepository.php

@@ -3,6 +3,7 @@
 namespace Dcat\Admin\Repositories;
 
 use Dcat\Admin\Contracts\TreeRepository;
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Form;
 use Dcat\Admin\Grid;
 use Dcat\Admin\Show;
@@ -410,7 +411,7 @@ class EloquentRepository extends Repository implements TreeRepository
         $model = $this->model();
 
         if (! $model instanceof Sortable) {
-            throw new \RuntimeException(
+            throw new RuntimeException(
                 sprintf(
                     'The model "%s" must be a type of %s.',
                     get_class($model),
@@ -432,7 +433,7 @@ class EloquentRepository extends Repository implements TreeRepository
         $model = $this->model();
 
         if (! $model instanceof Sortable) {
-            throw new \RuntimeException(
+            throw new RuntimeException(
                 sprintf(
                     'The model "%s" must be a type of %s.',
                     get_class($model),

+ 9 - 8
src/Repositories/QueryBuilderRepository.php

@@ -3,6 +3,7 @@
 namespace Dcat\Admin\Repositories;
 
 use Dcat\Admin\Contracts\TreeRepository;
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Form;
 use Dcat\Admin\Grid;
 use Dcat\Admin\Show;
@@ -333,7 +334,7 @@ class QueryBuilderRepository extends Repository implements TreeRepository
      */
     public function moveOrderUp()
     {
-        throw new \RuntimeException('Not support.');
+        throw new RuntimeException('Not support.');
     }
 
     /**
@@ -343,7 +344,7 @@ class QueryBuilderRepository extends Repository implements TreeRepository
      */
     public function moveOrderDown()
     {
-        throw new \RuntimeException('Not support.');
+        throw new RuntimeException('Not support.');
     }
 
     /**
@@ -409,7 +410,7 @@ class QueryBuilderRepository extends Repository implements TreeRepository
      */
     public function getParentColumn()
     {
-        throw new \RuntimeException('Not support.');
+        throw new RuntimeException('Not support.');
     }
 
     /**
@@ -419,7 +420,7 @@ class QueryBuilderRepository extends Repository implements TreeRepository
      */
     public function getTitleColumn()
     {
-        throw new \RuntimeException('Not support.');
+        throw new RuntimeException('Not support.');
     }
 
     /**
@@ -429,7 +430,7 @@ class QueryBuilderRepository extends Repository implements TreeRepository
      */
     public function getOrderColumn()
     {
-        throw new \RuntimeException('Not support.');
+        throw new RuntimeException('Not support.');
     }
 
     /**
@@ -440,7 +441,7 @@ class QueryBuilderRepository extends Repository implements TreeRepository
      */
     public function saveOrder($tree = [], $parentId = 0)
     {
-        throw new \RuntimeException('Not support.');
+        throw new RuntimeException('Not support.');
     }
 
     /**
@@ -452,7 +453,7 @@ class QueryBuilderRepository extends Repository implements TreeRepository
      */
     public function withQuery($queryCallback)
     {
-        throw new \RuntimeException('Not support.');
+        throw new RuntimeException('Not support.');
     }
 
     /**
@@ -462,7 +463,7 @@ class QueryBuilderRepository extends Repository implements TreeRepository
      */
     public function toTree()
     {
-        throw new \RuntimeException('Not support.');
+        throw new RuntimeException('Not support.');
     }
 
     /**

+ 2 - 1
src/Scaffold/ControllerCreator.php

@@ -2,6 +2,7 @@
 
 namespace Dcat\Admin\Scaffold;
 
+use Dcat\Admin\Exception\AdminException;
 use Dcat\Admin\Support\Helper;
 
 class ControllerCreator
@@ -54,7 +55,7 @@ class ControllerCreator
         }
 
         if ($this->files->exists($path)) {
-            throw new \Exception("Controller [$this->name] already exists!");
+            throw new AdminException("Controller [$this->name] already exists!");
         }
 
         $stub = $this->files->get($this->getStub());

+ 2 - 1
src/Scaffold/MigrationCreator.php

@@ -2,6 +2,7 @@
 
 namespace Dcat\Admin\Scaffold;
 
+use Dcat\Admin\Exception\AdminException;
 use Illuminate\Database\Migrations\MigrationCreator as BaseMigrationCreator;
 use Illuminate\Filesystem\Filesystem;
 use Illuminate\Support\Arr;
@@ -88,7 +89,7 @@ class MigrationCreator extends BaseMigrationCreator
         });
 
         if (empty($fields)) {
-            throw new \Exception('Table fields can\'t be empty');
+            throw new AdminException('Table fields can\'t be empty');
         }
 
         $rows[] = "\$table->increments('$keyName');\n";

+ 2 - 1
src/Scaffold/ModelCreator.php

@@ -2,6 +2,7 @@
 
 namespace Dcat\Admin\Scaffold;
 
+use Dcat\Admin\Exception\AdminException;
 use Dcat\Admin\Support\Helper;
 use Illuminate\Support\Str;
 
@@ -65,7 +66,7 @@ class ModelCreator
         }
 
         if ($this->files->exists($path)) {
-            throw new \Exception("Model [$this->name] already exists!");
+            throw new AdminException("Model [$this->name] already exists!");
         }
 
         $stub = $this->files->get($this->getStub());

+ 2 - 1
src/Show/Field.php

@@ -3,6 +3,7 @@
 namespace Dcat\Admin\Show;
 
 use Dcat\Admin\Admin;
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Show;
 use Dcat\Admin\Support\Helper;
 use Dcat\Admin\Traits\HasBuilderEvents;
@@ -711,7 +712,7 @@ HTML;
         $class = static::$definitions[$this->name];
 
         if (! $class instanceof \Closure) {
-            throw new \Exception("Invalid column definition [$class]");
+            throw new RuntimeException("Invalid column definition [$class]");
         }
 
         $this->as($class);

+ 178 - 0
src/Support/DatabaseUpdater.php

@@ -0,0 +1,178 @@
+<?php
+
+namespace Dcat\Admin\Support;
+
+use Dcat\Admin\Exception\AdminException;
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Seeder;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\DB;
+
+/**
+ * Database updater
+ *
+ * Executes database migration and seed scripts based on their filename.
+ *
+ * @package october\database
+ * @author Alexey Bobkov, Samuel Georges
+ */
+class DatabaseUpdater
+{
+    /**
+     * Sets up a migration or seed file.
+     */
+    public function setUp($file, \Closure $callback = null)
+    {
+        $object = $this->resolve($file);
+
+        if ($object === null) {
+            return false;
+        }
+
+        $this->isValidScript($object);
+
+        Model::unguard();
+
+        Db::connection($this->connection())->transaction(function () use ($object, $callback) {
+            if ($object instanceof Migration) {
+                $object->up();
+            }
+            elseif ($object instanceof Seeder) {
+                $object->run();
+            }
+
+            $callback && $callback();
+        });
+
+        Model::reguard();
+
+        return true;
+    }
+
+    /**
+     * Packs down a migration or seed file.
+     */
+    public function packDown($file, \Closure $callback = null)
+    {
+        $object = $this->resolve($file);
+
+        if ($object === null) {
+            return false;
+        }
+
+        $this->isValidScript($object);
+
+        Model::unguard();
+
+        Db::connection($this->connection())->transaction(function () use ($object, $callback) {
+            if ($object instanceof Migration) {
+                $object->down();
+            }
+
+            $callback && $callback();
+        });
+
+        Model::reguard();
+
+        return true;
+    }
+
+    /**
+     * Resolve a migration instance from a file.
+     * @param  string  $file
+     * @return object
+     */
+    public function resolve($file)
+    {
+        if (! is_file($file)) {
+            return;
+        }
+
+        require_once $file;
+
+        if ($class = $this->getClassFromFile($file)) {
+            return new $class;
+        }
+    }
+
+    /**
+     * Checks if the object is a valid update script.
+     */
+    protected function isValidScript($object)
+    {
+        if ($object instanceof Migration) {
+            return true;
+        }
+        elseif ($object instanceof Seeder) {
+            return true;
+        }
+
+        throw new AdminException(sprintf(
+            'Database script [%s] must inherit %s or %s classes',
+            get_class($object),
+            Migration::class,
+            Seeder::class
+        ));
+    }
+
+    /**
+     * Extracts the namespace and class name from a file.
+     * @param string $file
+     * @return string
+     */
+    public function getClassFromFile($file)
+    {
+        $fileParser = fopen($file, 'r');
+        $class = $namespace = $buffer = '';
+        $i = 0;
+
+        while (! $class) {
+            if (feof($fileParser)) {
+                break;
+            }
+
+            $buffer .= fread($fileParser, 512);
+
+            // Prefix and suffix string to prevent unterminated comment warning
+            $tokens = token_get_all('/**/' . $buffer . '/**/');
+
+            if (strpos($buffer, '{') === false) {
+                continue;
+            }
+
+            for (; $i < count($tokens); $i++) {
+                /*
+                 * Namespace opening
+                 */
+                if ($tokens[$i][0] === T_NAMESPACE) {
+                    for ($j = $i + 1; $j < count($tokens); $j++) {
+                        if ($tokens[$j] === ';') {
+                            break;
+                        }
+
+                        $namespace .= is_array($tokens[$j]) ? $tokens[$j][1] : $tokens[$j];
+                    }
+                }
+
+                /*
+                 * Class opening
+                 */
+                if ($tokens[$i][0] === T_CLASS && $tokens[$i-1][1] !== '::') {
+                    $class = $tokens[$i+2][1];
+                    break;
+                }
+            }
+        }
+
+        if (!strlen(trim($namespace)) && !strlen(trim($class))) {
+            return false;
+        }
+
+        return trim($namespace) . '\\' . trim($class);
+    }
+
+    public function connection()
+    {
+        return config('admin.database.connection') ?: config('database.default');
+    }
+}

+ 2 - 1
src/Traits/ModelTree.php

@@ -2,6 +2,7 @@
 
 namespace Dcat\Admin\Traits;
 
+use Dcat\Admin\Exception\AdminException;
 use Dcat\Admin\Support\Helper;
 use Dcat\Admin\Tree;
 use Illuminate\Database\Eloquent\Builder;
@@ -340,7 +341,7 @@ trait ModelTree
                 && Request::has($parentColumn)
                 && Request::input($parentColumn) == $branch->getKey()
             ) {
-                throw new \Exception(trans('admin.parent_select_error'));
+                throw new AdminException(trans('admin.parent_select_error'));
             }
 
             if (Request::has('_order')) {

+ 3 - 2
src/Tree.php

@@ -4,6 +4,7 @@ namespace Dcat\Admin;
 
 use Closure;
 use Dcat\Admin\Contracts\TreeRepository;
+use Dcat\Admin\Exception\InvalidArgumentException;
 use Dcat\Admin\Repositories\EloquentRepository;
 use Dcat\Admin\Traits\HasBuilderEvents;
 use Dcat\Admin\Tree\AbstractTool;
@@ -176,7 +177,7 @@ class Tree implements Renderable
         if (! $repository instanceof TreeRepository) {
             $class = get_class($repository);
 
-            throw new \InvalidArgumentException("The class [{$class}] must be a type of [".TreeRepository::class.'].');
+            throw new InvalidArgumentException("The class [{$class}] must be a type of [".TreeRepository::class.'].');
         }
 
         return $repository;
@@ -342,7 +343,7 @@ class Tree implements Renderable
         $tree = json_decode($serialize, true);
 
         if (json_last_error() != JSON_ERROR_NONE) {
-            throw new \InvalidArgumentException(json_last_error_msg());
+            throw new InvalidArgumentException(json_last_error_msg());
         }
 
         $this->repository->saveOrder($tree);

+ 2 - 1
src/Widgets/Form.php

@@ -5,6 +5,7 @@ namespace Dcat\Admin\Widgets;
 use Closure;
 use Dcat\Admin\Admin;
 use Dcat\Admin\Contracts\LazyRenderable;
+use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Form\Concerns\HandleCascadeFields;
 use Dcat\Admin\Form\Concerns\HasRows;
 use Dcat\Admin\Form\Concerns\HasTabs;
@@ -838,7 +839,7 @@ HTML;
             return $this->macroCall($method, $arguments);
         }
 
-        throw new \BadMethodCallException("Field [{$method}] does not exist.");
+        throw new RuntimeException("Field [{$method}] does not exist.");
     }
 
     /**