Dcat Admin 使用 Laravel Octane 时导出功能无法使用的原因及修复方法

问题

在使用 Dcat admin 框架的时候,发现导出功能使用 Octane 时会出现直接打印文件内容的情况,并报 swoole exit 异常,查看源码后才知道 Dcat admin 原生的导出类 的export() 方法是强制发送了响应,返回响应后是由 Octane 再次转发到 Swoole/RoadRunner 服务器的(而不是返回一个响应对象由 Octane 捕获然后转发),所以 Octane 在读取响应内容的时候会直接读到文件内容,并丢失了响应头,故直接进行打印了。另外原生的 export() 方法里面也使用了 exit,也导致了 swoole exit 的异常。

Octane 请求处理响应部分源码:
Laravel\Octane\Worker

...
ob_start();

$response = $gateway->handle($request);

$output = ob_get_contents();

ob_end_clean();

// Here we will actually hand the incoming request to the Laravel application so
// it can generate a response. We'll send this response back to the client so
// it can be returned to a browser. This gateway will also dispatch events.
$this->client->respond(
  $context,
  $octaneResponse = new OctaneResponse($response, $output),
);
...

Dcat Admin 原生 Easy Excel 导出源码
Dcat\Admin\Grid\Exporters\ExcelExporter

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

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 Admin 使用 Laravel Octane 时导出功能无法使用的原因及修复方法

Dcat\Exception\Handler

    /**
     * 显示异常信息.
     *
     * @param  \Throwable  $exception
     * @return array|string|void
     *
     * @throws \Throwable
     */
    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

/*
|--------------------------------------------------------------------------
| The exception handler class
|--------------------------------------------------------------------------
|
*/
'exception_handler' => \App\Admin\Exceptions\Handler::class,

App\Admin\Exception\Handler

<?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 代码如下:

<?php

namespace App\Admin\Exceptions;

use Throwable;
use function admin_route;
use function response;

/**
 * 此异常类用于跳过 Dcat Admin 框架的响应机制,直接返回下载响应
 * 在抛出该异常时,我们编写的 Handler 类会直接将此异常抛由 Laravel 处理
 * @see Handler
 * 然后 Laravel 会自动调用异常类的 render 方法来返回一个 HTTP 响应
 */
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;
    }

    /**
     * @return string
     */
    public function getFilename(): string
    {
        return $this->filename;
    }

    /**
     * @param string $filename
     */
    public function setFilename(string $filename): void
    {
        $this->filename = $filename;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @param string $name
     */
    public function setName(string $name): void
    {
        $this->name = $name;
    }


    public function render($request)
    {
        return response()->redirectTo(admin_route('export', [$this->getFilename(), $this->getName()]));
    }
}

如果帮到了你,不妨点个赞给我一点反馈

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 5

发现直接在 Laravel 的 Handler 中捕获异常不可行,已在原帖中更新了修复方式。

1年前 评论
狒狒达人 7个月前
微波炉 (作者) (楼主) 6个月前

ExporterException 这个类没有use,是从哪里来的

1年前 评论
狒狒达人 7个月前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!