timacdonald/multiformat-response-objects

一个处理单个控制器内多种响应格式的响应对象

v1.0.0 2019-07-14 05:40 UTC

README

Latest Stable Version Total Downloads License

在某些情况下,您可能希望支持单一端点和控制器多种返回格式(HTML、JSON、CSV、XLSX)。此包为您提供了一个基类,帮助您返回相同数据的不同格式。它支持指定文件扩展名或作为 Accept 头的返回格式。它还允许您拥有共享和格式特定逻辑,同时共享相同的路由和控制器。

安装

您可以使用来自 composer 的方式从 Packagist 安装。

$ composer require timacdonald/multiformat-response-objects

入门

此包旨在帮助您,如果您曾经创建过类似以下的控制器...

class UserController
{
    public function index(Request $request, CsvWriter $csvWriter)
    {
        // some shared logic...

        $query = User::query()
            ->whereActive()
            ->whereStatus($request->query('status'));

        // format check(s) and format specific logic...

        if ($this->wantsCsv($request)) {

            // return a CSV...

            $query->each(function ($user) use ($csvWriter) {
                $csvWriter->addRow($user->only(['name', 'email']));
            });

            return response()->download($csvWriter->file(), "Users.csv", [
                'Content-type' => 'text/csv',
            ]);
       }

       // return a webpage...

        $memberships = Membership::all();

        return view('users.index', [
            'memberships' => $memberships,
            'users' => $this->query->paginate(),
        ]);
    }
}

您可能会注意到上述控制器的一些特点

  1. 所有格式之间有一些初始共享逻辑,即准备查询。
  2. 如果用户请求网页,则不会使用 CsvWriter
  3. 当我们添加更多格式时,无疑会向其他响应类型注入更多不必要的依赖项。
  4. 更多的响应类型也意味着 if 链中需要更多的检查。
  5. 网页也有格式特定逻辑,例如,它需要 $memberships 集合,该集合可能用于在网页上填充下拉列表,但在 CSV 下载中并不需要。

此包清理了这种风格的控制器。让我向您展示如何...

清理控制器

重构控制器第一步是使用响应对象替换格式特定逻辑。您无疑会在最后进行这一步骤,但我认为这样演示更容易。

class UserController
{
    public function index(Request $request, CsvWriter $csvWriter, )
    {
        $query = User::query()
            ->whereActive()
            ->whereStatus($request->query('status'));

        return UserIndexResponse::make(['query' => $query]);
    }
}

您可以通过将数据数组传递给静态 make 方法将值传递给响应对象。这类似于您可能已经发送视图数据的方式 view('users.index', ['some' => 'data'])

响应对象

为了支持特定的响应格式,您需要添加相应的响应方法。如果您想以 mp3 音频格式提供您的博客文章,您会在响应对象中添加一个 toMp3Response 方法。

您可以对这些方法进行类型提示,依赖项将来自容器。在我们的示例中,我们支持 HTML 和 CSV 格式。

use TiMacDonald\MultiFormat\Response;

class UserResponse extends Response
{
    public function toCsvResponse(CsvWriter $writer)
    {
        $this->query->each(function ($user) use ($writer) {
            $writer->addRow($user->only(['name', 'email']));
        });

        return response()->download($writer->file(), "Users.csv", [
            'Content-type' => 'text/csv',
        ]);
    }

    public function toHtmlResponse()
    {
        $memberships = Membership::all();

        return view('users.index', [
            'memberships' => $memberships,
            'users' => $this->query->paginate(),
        ]);
    }
}

您可以看到 toCsvResponse 方法对 CsvWriter 进行了类型提示。这个依赖项只有在请求格式是 CSV 时才会解析。您还可以神奇地访问通过 make 方法传递到任何数据,作为对象上的属性,例如 $this->query

这实际上就是全部内容。下面是一些更详细的文档和功能。

检测响应格式

响应对象将自动通过检查请求 URL 上的文件扩展名来检测请求的响应格式,如果未找到扩展名,将回退到 Accept 头。在底层,我们使用 Symfony 的 MimeTypes 类来检测扩展名。然后回退到 Laravel 的 Request::format() 方法。将使用第一个匹配的 MIME 类型和第一个匹配的扩展名。

您不一定需要支持文件扩展名。这完全由您控制。如果您只想支持 Accept 头,则设置您的路由不支持扩展名。

为什么使用文件扩展名?

API使用Accept头部处理内容协商是很标准的做法。然而,能够通过文件扩展名指定响应格式也很有用。这在Web界面中尤为方便,你可以在相同的URL后添加扩展名来告诉服务器你想要的格式。

<h2>Downloads</h2>
<ul>
    <li><a href="/users.csv">CSV</a></li>
    <li><a href="/users.pdf">PDF</a></li>
</ul>

这种模式在许多地方都有使用。Reddit就是一个很好的例子。在Reddit上的任何URL后添加.json,你将得到JSON格式的响应。

自己试试看

响应格式方法

为了支持某种格式,你需要创建一个名为to{Format}Response的方法,其中{Format}是格式的文件扩展名。例如:

  • CSV: toCsvResponse()
  • JSON: toJsonResponse()
  • HTML: toHtmlResponse()
  • XLSX: toXlsxResponse()

依赖注入

如前所述,容器会调用格式方法,允许你从容器中解决特定格式的依赖。在基本用法示例中,HTML格式没有依赖项,但CSV格式有一个CsvWriter依赖项。

默认响应格式

你可以设置一个默认的响应格式,无论是从调用控制器还是从响应对象内部设置。如果URL和Accept头部没有设置值,或者没有找到现有的Accept类型匹配,将使用此默认格式。

在控制器中

class UserController
{
    public function index()
    {
        //...

        return UserResponse::make(['query' => $query])
            ->withDefaultFormat('csv');
    }
}

在响应对象中

class UserResponse extends Response
{
    protected $defaultFormat = 'csv';

    // ...
}

覆盖格式

如果需要支持的MIME类型没有转换为正确的扩展名,要么因为底层库中没有这个扩展名,要么因为它匹配了第一个扩展名而你想要使用另一个,你可以手动指定覆盖。

audio/mpeg为例。与这个内容类型关联着几个扩展名。

'audio/mpeg' => ['mpga', 'mp2', 'mp2a', 'mp3', 'm2a', 'm3a'],

这个包将解析第一个匹配项,即mpga作为格式类型。如果你想覆盖这个扩展名,可以这样做...

在控制器中

class UserController
{
    public function index()
    {
        //...

        return UserResponse::make(['query' => $query])
            ->withFormatOverrides([
                'audio/mpeg' => 'mp3',
            ]);
    }
}

在响应对象中

class UserResponse extends Response
{
    protected $formatOverrides = [
        'audio/mpeg' => 'mp3',
    ];

    // ...
}

上述方法会在接受头部为audio/mpeg时调用toMp3Response

路由

如果你想要通过文件扩展名来指定响应格式,你应该在你的路由文件中明确指定允许的格式。这个包目前不提供任何路由助手,但以下是一个当前如何做的示例。

Route::get('users{extension?}', [
    'as' => 'users.index',
    'uses' => 'UserController@index',
    // this is what we need to add...
    'where' => [
        'extension' => '^\.(pdf|csv|xlsx)$',
    ],
]);

这个路由将能够响应以下URL和响应对象中的格式...

我讨厌魔法

这很酷。不是每个人都喜欢它。你不必使用make方法。只需添加你自己的构造函数并按照你的喜好设置类属性!

class UserResponse extends Response
{
    /**
     * @var \Illuminate\Database\Eloquent\Builder
     */
    private $query;

    public function __construct(Builder $query)
    {
        $this->query = $query;
    }
}

//...

return new UserResponse($query);

旅程

你已经阅读了readme,你也看到了代码,现在阅读旅程。如果你想看看我是如何得出这个解决方案的,你可以阅读我的博客文章:https://timacdonald.me/versatile-response-objects-laravel/。警告:这有点像发牢骚。

tl;dr; DHH和Adam Wathan都很棒。

谢谢

你可以自由使用这个包,但我要求你联系一个人(不是我自己),这个人曾经或正在维护或为你在项目中使用的开源库做出贡献,并感谢他们的工作。请考虑你的整个技术栈:包、框架、语言、数据库、操作系统、前端、后端等。