Skip to content

Instantly share code, notes, and snippets.

@kohenkatz
Created February 14, 2025 02:51
Show Gist options
  • Save kohenkatz/18885b4a783fe91faf10e6613eb2b93a to your computer and use it in GitHub Desktop.
Save kohenkatz/18885b4a783fe91faf10e6613eb2b93a to your computer and use it in GitHub Desktop.
A Laravel class for content negotiation in JSON or flat-file (CSV/XLS(X)/etc)
<?php
namespace App\Http\Responses;
use Closure;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder;
use Illuminate\Http\Resources\Json\JsonResource;
use InvalidArgumentException;
use Maatwebsite\Excel\Concerns\FromQuery;
use Spatie\QueryBuilder\QueryBuilder;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
/**
* Implements an HTTP-compliant Content Negotiation response.
*
* Given a Laravel "JsonResource" transformer and a Laravel-Excel "Exporter",
* the incoming request's `Accept` header is used to choose the appropriate response
* format.
*
* In the event the incoming request indicates that any response type is accepted
* (using `*\/*`), JSON is used as the default. If the requested type is not supported,
* a spec-compliant 406 response is returned.
*
* This class is partially inspired by the Ruby on Rails `ActionController#respond_to` method.
*
* @package App\Http\Responses
*/
class Negotiable implements Responsable
{
protected const SUPPORTED_DOWNLOAD_TYPES = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => \Maatwebsite\Excel\Excel::XLSX,
'application/vnd.ms-excel' => \Maatwebsite\Excel\Excel::XLS,
'application/vnd.oasis.opendocument.spreadsheet' => \Maatwebsite\Excel\Excel::ODS,
'text/csv' => \Maatwebsite\Excel\Excel::CSV,
];
public function __construct(
protected Builder|EloquentBuilder|Relation|QueryBuilder $query,
protected string $jsonResource,
protected string $exportClass,
protected null|array|Closure $additionalData = null,
protected string $name = 'data',
)
{
if (!is_a($jsonResource, JsonResource::class, allow_string: true)) {
throw new InvalidArgumentException("$jsonResource must be a valid JsonResource");
}
if (!is_a($exportClass, FromQuery::class, allow_string: true)) {
throw new InvalidArgumentException("$exportClass must be a valid Laravel-Excel Exporter");
}
}
/**
* Create an HTTP response that represents the object.
*
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function toResponse($request): Response
{
// Check for JSON
if ($request->accepts('application/json')) {
return tap(($this->jsonResource)::collection($this->query->get()), function (JsonResource $r) {
if ($this->additionalData) {
$r->additional(value($this->additionalData));
}
})->toResponse($request);
}
// Check for any supported flat-file type
if (($mimeType = $request->prefers(array_keys(static::SUPPORTED_DOWNLOAD_TYPES)))) {
$type = static::SUPPORTED_DOWNLOAD_TYPES[$mimeType];
$name = $this->name . '.' . strtolower($type);
return (new ($this->exportClass)($this->query))
->download(
$name,
$type,
[
'Content-Type' => $mimeType,
],
);
}
throw new NotAcceptableHttpException('Unrecognized content type');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment