Catch plugin's bootstrapper exceptions

This commit is contained in:
Pig Fang 2019-08-31 09:12:51 +08:00
parent 016e11b40b
commit c44ff192ba
11 changed files with 149 additions and 3 deletions

View File

@ -0,0 +1,16 @@
<?php
namespace App\Events;
use App\Services\Plugin;
class PluginBootFailed extends Event
{
/** @var Plugin */
public $plugin;
public function __construct(Plugin $plugin)
{
$this->plugin = $plugin;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Listeners;
use Event;
class NotifyFailedPlugin
{
public function handle($event)
{
$plugin = $event->plugin;
$user = auth()->user();
if ($user && $user->isAdmin()) {
Event::listen(\App\Events\RenderingFooter::class, function ($event) use ($plugin) {
$options = json_encode([
'type' => 'warning',
'title' => trans('errors.plugins.boot.title'),
'message' => trans('errors.plugins.boot.message', ['plugin' => trans($plugin->title)]),
'duration' => 0,
]);
$event->addContent('<script>blessing.ui.notify('.$options.')</script>');
});
}
}
}

View File

@ -27,6 +27,9 @@ class EventServiceProvider extends ServiceProvider
'App\Events\PluginVersionChanged' => [
'App\Listeners\CopyPluginAssets',
],
'App\Events\PluginBootFailed' => [
'App\Listeners\NotifyFailedPlugin',
],
];
/**

View File

@ -233,7 +233,18 @@ class PluginManager
{
$path = $plugin->getPath().'/bootstrap.php';
if ($this->filesystem->exists($path)) {
$this->app->call($this->filesystem->getRequire($path), ['plugin' => $plugin]);
try {
$this->app->call($this->filesystem->getRequire($path), ['plugin' => $plugin]);
} catch (\Throwable $th) {
report($th);
if (is_a($th, \Exception::class)) {
$handler = $this->app->make(\App\Exceptions\Handler::class);
if (! $handler->shouldReport($th)) {
throw $th;
}
}
$this->dispatcher->dispatch(new Events\PluginBootFailed($plugin));
}
}
}

View File

@ -179,8 +179,8 @@ return [
*/
App\Providers\RuntimeCheckServiceProvider::class,
App\Providers\AppServiceProvider::class,
App\Providers\PluginServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\PluginServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\ResponseMacroServiceProvider::class,
App\Providers\TranslationServiceProvider::class,

View File

@ -1,6 +1,6 @@
import Vue from 'vue'
import {
Button, Input, Message, MessageBox, Switch,
Button, Input, Message, MessageBox, Switch, Notification
} from 'element-ui'
Vue.use(Button)
@ -21,4 +21,5 @@ blessing.ui = {
alert: MessageBox.alert,
confirm: MessageBox.confirm,
prompt: MessageBox.prompt,
notify: Notification,
}

View File

@ -2,4 +2,5 @@
@import '~element-theme-chalk/src/input.scss';
@import '~element-theme-chalk/src/message.scss';
@import '~element-theme-chalk/src/message-box.scss';
@import '~element-theme-chalk/src/notification.scss';
@import '~element-theme-chalk/src/switch.scss';

View File

@ -19,6 +19,9 @@ exception:
plugins:
duplicate: The plugin [:dir1] has a duplicated plugin name definition which is same to plugin [:dir2]. Please check your plugins directory, remove one of them or use another name definition.
directory: We can't approach the path for loading plugins specified by the PLUGINS_DIR in .env file. Please check your configuration. Error :msg
boot:
title: Failed to boot plugin.
message: There is something wrong with plugin ":plugin". Please check logs for detail.
cipher:
unsupported: Unsupported password hashing method `:cipher`, please check your `.env` configuration

View File

@ -20,6 +20,9 @@ exception:
plugins:
duplicate: 【插件定义重复】:dir1 目录下的插件与 :dir2 目录下的插件使用了相同的 name 定义并造成了冲突。请检查你的插件目录,移除其中一个插件或者使用不同的 name 属性。
directory: 配置文件 .env 中指定的插件加载目录PLUGINS_DIR不存在或无法打开请检查你的配置。错误信息:msg
boot:
title: 插件加载失败
message: 「:plugin」插件存在错误无法加载。详细信息请见日志。
cipher:
unsupported: 不支持的密码加密方式 `:cipher`,请检查你的 .env 配置文件

View File

@ -0,0 +1,30 @@
<?php
namespace Tests;
use App\Events;
use App\Services\Plugin;
class NotifyFailedPluginTest extends TestCase
{
public function testHandle()
{
$content = [];
$plugin = new Plugin('', ['title' => 'ff']);
event(new Events\PluginBootFailed($plugin));
event(new Events\RenderingFooter($content));
$this->assertCount(0, $content);
$this->actAs('normal');
event(new Events\PluginBootFailed($plugin));
event(new Events\RenderingFooter($content));
$this->assertCount(0, $content);
$this->actAs('admin');
event(new Events\PluginBootFailed($plugin));
event(new Events\RenderingFooter($content));
$this->assertCount(1, $content);
$this->assertStringContainsString('blessing.ui.notify', $content[0]);
}
}

View File

@ -323,6 +323,59 @@ class PluginManagerTest extends TestCase
resolve(PluginManager::class)->boot();
}
public function testHandleBootstrapperExceptions()
{
Event::fake();
$this->mock(Option::class, function ($mock) {
$mock->shouldReceive('get')
->with('plugins_enabled', '[]')
->andReturn(json_encode([['name' => 'mayaka', 'version' => '0.0.0']]));
});
$this->mock(Filesystem::class, function ($mock) {
$mock->shouldReceive('directories')
->with(base_path('plugins'))
->andReturn(collect(['/mayaka']));
$mock->shouldReceive('exists')
->with('/mayaka'.DIRECTORY_SEPARATOR.'package.json')
->andReturn(true);
$mock->shouldReceive('get')
->with('/mayaka'.DIRECTORY_SEPARATOR.'package.json')
->andReturn(json_encode([
'name' => 'mayaka',
'version' => '0.0.0',
]));
$mock->shouldReceive('exists')
->with('/mayaka/vendor/autoload.php')
->andReturn(false);
$mock->shouldReceive('exists')
->with('/mayaka/bootstrap.php')
->andReturn(true);
$mock->shouldReceive('getRequire')
->with('/mayaka/bootstrap.php')
->andReturn(function () {
throw new \Exception();
}, function () {
abort(500);
});
});
app()->forgetInstance(PluginManager::class);
resolve(PluginManager::class)->boot();
Event::assertDispatched(Events\PluginBootFailed::class, function ($event) {
$this->assertEquals('mayaka', $event->plugin->name);
return true;
});
app()->forgetInstance(PluginManager::class);
$this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class);
resolve(PluginManager::class)->boot();
}
public function testLifecycleHooks()
{
$this->mock(Option::class, function ($mock) {