timacdonald / multiformat-response-objects
一个处理单个控制器内多种响应格式的响应对象
Requires
- php: ^7.2
- illuminate/http: 5.8.*
- illuminate/support: 5.8.*
- symfony/mime: ^4.3
Requires (Dev)
- orchestra/testbench: ^3.5
- phpunit/phpunit: ^8.0
This package is auto-updated.
Last update: 2024-09-10 07:50:30 UTC
README
在某些情况下,您可能希望支持单一端点和控制器多种返回格式(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(), ]); } }
您可能会注意到上述控制器的一些特点
- 所有格式之间有一些初始共享逻辑,即准备查询。
- 如果用户请求网页,则不会使用
CsvWriter
。 - 当我们添加更多格式时,无疑会向其他响应类型注入更多不必要的依赖项。
- 更多的响应类型也意味着
if
链中需要更多的检查。 - 网页也有格式特定逻辑,例如,它需要
$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和响应对象中的格式...
- http://example.com/users [HTML]
- http://example.com/users.pdf [PDF]
- http://example.com/users.csv [CSV]
- http://example.com/users.xlsx [XLSX]
我讨厌魔法
这很酷。不是每个人都喜欢它。你不必使用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都很棒。
谢谢
你可以自由使用这个包,但我要求你联系一个人(不是我自己),这个人曾经或正在维护或为你在项目中使用的开源库做出贡献,并感谢他们的工作。请考虑你的整个技术栈:包、框架、语言、数据库、操作系统、前端、后端等。