问题
在使用 Dcat admin 框架的时候,发现导出功能使用 Octane 时会出现直接打印文件内容的情况,并报 swoole exit
异常,查看源码后才知道 Dcat admin 原生的导出类 的export()
方法是强制发送了响应,返回响应后是由 Octane 再次转发到 Swoole/RoadRunner 服务器的(而不是返回一个响应对象由 Octane 捕获然后转发),所以 Octane 在读取响应内容的时候会直接读到文件内容,并丢失了响应头,故直接进行打印了。另外原生的 export()
方法里面也使用了 exit
,也导致了 swoole exit
的异常。
Octane 请求处理响应部分源码:
Laravel\Octane\Worker
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ... ob_start ();$response = $gateway ->handle ($request );$output = ob_get_contents ();ob_end_clean ();$this ->client->respond ( $context , $octaneResponse = new OctaneResponse ($response , $output ), ); ...
Dcat Admin 原生 Easy Excel 导出源码
Dcat\Admin\Grid\Exporters\ExcelExporter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public function export ( ) { $filename = $this ->getFilename ().'.' .$this ->extension; $exporter = Excel ::export (); if ($this ->scope === Grid\Exporter ::SCOPE_ALL ) { $exporter ->chunk (function (int $times ) { return $this ->buildData ($times ); }); } else { $exporter ->data ($this ->buildData () ?: [[]]); } return $exporter ->headings ($this ->titles ())->download ($filename ); }
解决方法
在不修改源码的前提下,可以使用异常抛出与捕获的方法解决
新建一个 Exporter
类继承 \Dcat\Admin\Grid\Exporters\AbstractExporter
,重写 export()
方法,在 export()
方法中抛出一个 ExporterException
异常,然后由 App\Exceptions\Handler
捕获并返回下载响应即可。
因为使用了 Xlswriter ,所以我这里是跳转到了下载文件的地址,也可以使用流式传输来下载 EasyExcel
的导出。
App\Exception\Handler
1 2 3 4 5 6 7 public function render ($request , Throwable $e ) { if ($e instanceof ExporterException) { return response ()->redirectTo (admin_route ('export' , [$e ->getMessage ()])); } return parent ::render ($request , $e ); }
仍然存在问题
本以为已经解决了该问题,但是当进行生产环境的测试时,发现异常直接由 Dcat Admin 捕获了,导致没有执行我们定义在 Handler
中的方法。
通过查看源码发现,当 .env
文件中 APP_DEBUG
设置为 false
的时候,Dcat Admin 就会捕获异常,并自己返回。响应
Dcat\Exception\Handler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public function render (\Throwable $exception ) { if (config ('app.debug' )) { throw $exception ; } if (Helper ::isAjaxRequest ()) { return ; } $error = new MessageBag ([ 'type' => get_class ($exception ), 'message' => $exception ->getMessage (), 'file' => $exception ->getFile (), 'line' => $exception ->getLine (), 'trace' => $this ->replaceBasePath ($exception ->getTraceAsString ()), ]); $errors = new ViewErrorBag (); $errors ->put ('exception' , $error ); return view ('admin::partials.exception' , compact ('errors' ))->render (); }
幸好 Dcat Admin 提供了异常处理类的配置,我们只需要继承该类重写 render()
逻辑,然后再在 config/admin.php
文件中修改异常捕获类就可以了。
config/admin.php
1 2 3 4 5 6 7 'exception_handler' => \App\Admin\Exceptions\Handler ::class ,
App\Admin\Exception\Handler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php namespace App \Admin \Exceptions ;class Handler extends \Dcat \Admin \Exception \Handler { public function render (\Throwable $exception ) { if ($exception instanceof ExporterException) { throw $exception ; } return parent ::render ($exception ); } }
改进一点
我们不必在 App\Exceptions\Handler
中用 if
判断异常,这使得我们的代码过于分散。其实 Laravel 可以在捕获异常时调用异常的 render
方法,于是我们最终的 ExportException
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 <?php namespace App \Admin \Exceptions ;use Throwable ;use function admin_route ;use function response ;class ExporterException extends \Exception { public string $name ; public string $filename ; public function __construct (string $filename = "" , string $name = "" , int $code = 0 , ?Throwable $previous = null ) { parent ::__construct ($filename , $code , $previous ); $this ->filename = $filename ; $this ->name = $name ; } public function getFilename ( ): string { return $this ->filename; } public function setFilename (string $filename ): void { $this ->filename = $filename ; } public function getName ( ): string { return $this ->name; } public function setName (string $name ): void { $this ->name = $name ; } public function render ($request ) { return response ()->redirectTo (admin_route ('export' , [$this ->getFilename (), $this ->getName ()])); } }