feat(api): implement API response transformation middleware
- Added TransformApiV1Response middleware to handle API responses. - Updated Employee and IncoSheets controllers to return ApiResponse. - Refactored API routes to use new middleware for v1.
This commit is contained in:
parent
3c72e50adf
commit
7658483863
19 changed files with 131 additions and 69 deletions
|
|
@ -3,17 +3,18 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Presentation\Api\V1\Http\Middleware\TransformApiV1Response;
|
||||
|
||||
return function (Middleware $middleware): void {
|
||||
$middleware->web(append: [
|
||||
'throttle:web',
|
||||
]);
|
||||
|
||||
$middleware->api(append: [
|
||||
'throttle:api.v1',
|
||||
]);
|
||||
$middleware->api(append: []);
|
||||
|
||||
$middleware->alias([
|
||||
$middleware->group('api.v1', [
|
||||
'throttle:api.v1',
|
||||
TransformApiV1Response::class,
|
||||
]);
|
||||
|
||||
$middleware->preventRequestsDuringMaintenance(except: [
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ Route::get(
|
|||
Route::get('/ping', fn () => response()->json('pong'))->name('api.ping');
|
||||
|
||||
// * V1 API
|
||||
Route::prefix('v1')->name('api.v1.')->group(function () {
|
||||
require __DIR__ . '/apiv1.php';
|
||||
});
|
||||
Route::prefix('v1')
|
||||
->name('api.v1.')
|
||||
->middleware('api.v1')
|
||||
->group(function (): void {
|
||||
require __DIR__ . '/apiv1.php';
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use Shared\DDD\Usecase\Input;
|
|||
use Shared\DDD\Usecase\Presenter;
|
||||
use Shared\DDD\Usecase\Usecase;
|
||||
|
||||
/** @implements UsecaseExecutor<Input,\Shared\DDD\Usecase\Output> */
|
||||
/** @implements UsecaseExecutor<Input, \Shared\DDD\Usecase\Output> */
|
||||
final readonly class LaravelExecutor implements UsecaseExecutor
|
||||
{
|
||||
public function __construct(
|
||||
|
|
@ -21,8 +21,11 @@ final readonly class LaravelExecutor implements UsecaseExecutor
|
|||
}
|
||||
|
||||
/**
|
||||
* @param Usecase<Input,\Shared\DDD\Usecase\Output> $usecase
|
||||
* @param Presenter<\Shared\DDD\Usecase\Output,object> $presenter
|
||||
* @template TView of object
|
||||
*
|
||||
* @param Usecase<Input, \Shared\DDD\Usecase\Output> $usecase
|
||||
* @param Presenter<\Shared\DDD\Usecase\Output, TView> $presenter
|
||||
* @return TView
|
||||
*/
|
||||
#[\Override]
|
||||
public function execute(Usecase $usecase, Input $input, Presenter $presenter): object
|
||||
|
|
@ -36,7 +39,7 @@ final readonly class LaravelExecutor implements UsecaseExecutor
|
|||
$this->logger->debug('Usecase execution started', $context);
|
||||
|
||||
try {
|
||||
/** @var object $result */
|
||||
/** @var TView $result */
|
||||
$result = $this->database->transaction(function () use ($usecase, $input, $presenter): object {
|
||||
$output = $usecase->execute($input);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ declare(strict_types=1);
|
|||
|
||||
namespace Presentation\Api\V1\Contracts;
|
||||
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
interface ApiResponse
|
||||
interface ApiResponse extends Responsable
|
||||
{
|
||||
public function present(): JsonResponse;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ namespace Presentation\Api\V1\Http\Controllers\Employees;
|
|||
|
||||
use Application\Usecase\Employees\Find\FindEmployee;
|
||||
use Application\Usecase\Employees\Find\Input;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Presentation\Api\V1\Contracts\ApiResponse;
|
||||
use Presentation\Api\V1\Http\Requests\Employees\EmployeeShowRequest;
|
||||
use Presentation\Api\V1\Http\Responses\Employees\EmployeeShowResponse;
|
||||
use Presentation\Api\V1\Presenters\Employees\EmployeeShowPresenter;
|
||||
|
|
@ -23,17 +23,15 @@ final readonly class EmployeeShowController
|
|||
) {
|
||||
}
|
||||
|
||||
public function __invoke(EmployeeShowRequest $request): JsonResponse
|
||||
public function __invoke(EmployeeShowRequest $request): ApiResponse
|
||||
{
|
||||
|
||||
/** @var \Presentation\Api\V1\Presenters\Employees\EmployeeShowView $response */
|
||||
$response = UsecaseBuilder::using($this->executor)
|
||||
->do($this->usecase)
|
||||
->from($this->toInput($request))
|
||||
->with($this->presenter)
|
||||
->run();
|
||||
|
||||
return (new EmployeeShowResponse($response))->present();
|
||||
return new EmployeeShowResponse($response);
|
||||
}
|
||||
|
||||
private function toInput(EmployeeShowRequest $request): Input
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ namespace Presentation\Api\V1\Http\Controllers\Employees;
|
|||
|
||||
use Application\Usecase\Employees\FindAll\FindAllEmployees;
|
||||
use Application\Usecase\Employees\FindAll\Input;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Presentation\Api\V1\Contracts\ApiResponse;
|
||||
use Presentation\Api\V1\Http\Requests\Employees\EmployeesIndexRequest;
|
||||
use Presentation\Api\V1\Http\Responses\Employees\EmployeesIndexResponse;
|
||||
use Presentation\Api\V1\Presenters\Employees\EmployeesIndexPresenter;
|
||||
|
|
@ -23,21 +23,25 @@ final readonly class EmployeesIndexController
|
|||
) {
|
||||
}
|
||||
|
||||
public function __invoke(EmployeesIndexRequest $request): JsonResponse
|
||||
public function __invoke(EmployeesIndexRequest $request): ApiResponse
|
||||
{
|
||||
$input = new Input(
|
||||
pagination: $request->pagination(),
|
||||
sorter: $request->sorter(),
|
||||
filter: $request->filter(),
|
||||
);
|
||||
$input = $this->inputFrom($request);
|
||||
|
||||
/** @var \Presentation\Api\V1\Presenters\Employees\EmployeesIndexView $response */
|
||||
$response = UsecaseBuilder::using($this->executor)
|
||||
->do($this->usecase)
|
||||
->from($input)
|
||||
->with($this->presenter)
|
||||
->run();
|
||||
|
||||
return (new EmployeesIndexResponse($response, $request->toLeagueUri()))->present();
|
||||
return new EmployeesIndexResponse($response, $request->toLeagueUri());
|
||||
}
|
||||
|
||||
private function inputFrom(EmployeesIndexRequest $request): Input
|
||||
{
|
||||
return new Input(
|
||||
pagination: $request->pagination(),
|
||||
sorter: $request->sorter(),
|
||||
filter: $request->filter(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ namespace Presentation\Api\V1\Http\Controllers\IncoSheets;
|
|||
|
||||
use Application\Usecase\IncoSheets\FindAll\FindAllIncoSheets;
|
||||
use Application\Usecase\IncoSheets\FindAll\Input;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Presentation\Api\V1\Contracts\ApiResponse;
|
||||
use Presentation\Api\V1\Http\Requests\IncoSheets\IncoSheetsIndexRequest;
|
||||
use Presentation\Api\V1\Http\Responses\IncoSheets\IncoSheetsIndexResponse;
|
||||
use Presentation\Api\V1\Presenters\IncoSheets\IncoSheetsIndexPresenter;
|
||||
|
|
@ -23,7 +23,7 @@ final readonly class IncoSheetsIndexController
|
|||
) {
|
||||
}
|
||||
|
||||
public function __invoke(IncoSheetsIndexRequest $request): JsonResponse
|
||||
public function __invoke(IncoSheetsIndexRequest $request): ApiResponse
|
||||
{
|
||||
$input = new Input(
|
||||
pagination: $request->pagination(),
|
||||
|
|
@ -31,13 +31,12 @@ final readonly class IncoSheetsIndexController
|
|||
filter: $request->filter(),
|
||||
);
|
||||
|
||||
/** @var \Presentation\Api\V1\Presenters\IncoSheets\IncoSheetsIndexView $response */
|
||||
$response = UsecaseBuilder::using($this->executor)
|
||||
->do($this->usecase)
|
||||
->from($input)
|
||||
->with($this->presenter)
|
||||
->run();
|
||||
|
||||
return (new IncoSheetsIndexResponse($response, $request->toLeagueUri(), $request->isShort()))->present();
|
||||
return new IncoSheetsIndexResponse($response, $request->toLeagueUri());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ namespace Presentation\Api\V1\Http\Controllers\IncoSheets;
|
|||
|
||||
use Application\Usecase\IncoSheets\Find\FindIncoSheet;
|
||||
use Application\Usecase\IncoSheets\Find\Input;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Presentation\Api\V1\Contracts\ApiResponse;
|
||||
use Presentation\Api\V1\Http\Requests\IncoSheets\IncoSheetsShowRequest;
|
||||
use Presentation\Api\V1\Http\Responses\IncoSheets\IncoSheetsShowResponse;
|
||||
use Presentation\Api\V1\Presenters\IncoSheets\IncoSheetsShowPresenter;
|
||||
|
|
@ -23,19 +23,18 @@ final readonly class IncoSheetsShowController
|
|||
) {
|
||||
}
|
||||
|
||||
public function __invoke(IncoSheetsShowRequest $request): JsonResponse
|
||||
public function __invoke(IncoSheetsShowRequest $request): ApiResponse
|
||||
{
|
||||
$input = new Input(
|
||||
id: $request->incoId(),
|
||||
);
|
||||
|
||||
/** @var \Presentation\Api\V1\Presenters\IncoSheets\IncoSheetsShowView $response */
|
||||
$response = UsecaseBuilder::using($this->executor)
|
||||
->do($this->usecase)
|
||||
->from($input)
|
||||
->with($this->presenter)
|
||||
->run();
|
||||
|
||||
return (new IncoSheetsShowResponse($response))->present();
|
||||
return new IncoSheetsShowResponse($response);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Api\V1\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Presentation\Api\V1\Contracts\ApiResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class TransformApiV1Response
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
if ($response instanceof ApiResponse) {
|
||||
return $response->present();
|
||||
}
|
||||
|
||||
/** @var Response $response */
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,12 @@ final readonly class EmployeeShowResponse implements ApiResponse
|
|||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function toResponse($request): JsonResponse
|
||||
{
|
||||
return $this->present();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function present(): JsonResponse
|
||||
{
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ final readonly class EmployeesIndexResponse implements ApiResponse
|
|||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function toResponse($request): JsonResponse
|
||||
{
|
||||
return $this->present();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function present(): JsonResponse
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,10 +18,15 @@ final readonly class IncoSheetsIndexResponse implements ApiResponse
|
|||
public function __construct(
|
||||
private IncoSheetsIndexView $view,
|
||||
private Http $baseUri,
|
||||
private bool $isShort = false,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function toResponse($request): JsonResponse
|
||||
{
|
||||
return $this->present();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function present(): JsonResponse
|
||||
{
|
||||
|
|
@ -36,31 +41,15 @@ final readonly class IncoSheetsIndexResponse implements ApiResponse
|
|||
$prevUrl = $page > 1 ? (string) $this->baseUri->withQuery(http_build_query(['page' => $page - 1, 'pageSize' => $pageSize])) : null;
|
||||
$nextUrl = $page < $totalPages ? (string) $this->baseUri->withQuery(http_build_query(['page' => $page + 1, 'pageSize' => $pageSize])) : null;
|
||||
|
||||
$isShort = $this->isShort;
|
||||
|
||||
$incoSheets = array_map(
|
||||
static function (PaginatedIncoSheet | PaginatedShortIncoSheet $sheet) use ($isShort): array {
|
||||
if ($isShort || $sheet instanceof PaginatedShortIncoSheet) {
|
||||
$shortSheet = $sheet instanceof PaginatedShortIncoSheet
|
||||
? $sheet
|
||||
: new PaginatedShortIncoSheet(
|
||||
id: $sheet->id,
|
||||
name: $sheet->name,
|
||||
label: $sheet->label,
|
||||
createdAt: $sheet->createdAt,
|
||||
updatedAt: $sheet->updatedAt,
|
||||
);
|
||||
|
||||
return get_object_vars(IncoShortSheetDto::fromPaginatedShortIncoSheet($shortSheet));
|
||||
}
|
||||
|
||||
return get_object_vars(IncoSheetDto::fromPaginatedIncoSheet($sheet));
|
||||
},
|
||||
$items = array_map(
|
||||
fn (PaginatedIncoSheet|PaginatedShortIncoSheet $sheet): array => $sheet instanceof PaginatedShortIncoSheet
|
||||
? get_object_vars(IncoShortSheetDto::fromPaginatedShortIncoSheet($sheet))
|
||||
: get_object_vars(IncoSheetDto::fromPaginatedIncoSheet($sheet)),
|
||||
$this->view->incoSheets
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'incoSheets' => $incoSheets,
|
||||
'incoSheets' => $items,
|
||||
'pagination' => [
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ final readonly class IncoSheetsShowResponse implements ApiResponse
|
|||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function toResponse($request): JsonResponse
|
||||
{
|
||||
return $this->present();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function present(): JsonResponse
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shared\DDD\Usecase\Builder;
|
||||
|
||||
/** @template-covariant TView of object */
|
||||
interface BuilderStepBuild
|
||||
{
|
||||
/** @return TView */
|
||||
public function run(): object;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ use Shared\DDD\Usecase\Presenter;
|
|||
/** @template TOutput of \Shared\DDD\Usecase\Output */
|
||||
interface BuilderStepPresenter
|
||||
{
|
||||
/** @param Presenter<TOutput,object> $presenter */
|
||||
/**
|
||||
* @template TView of object
|
||||
*
|
||||
* @param Presenter<TOutput, TView> $presenter
|
||||
* @return BuilderStepBuild<TView>
|
||||
*/
|
||||
public function with(Presenter $presenter): BuilderStepBuild;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,14 @@ namespace Shared\DDD\Usecase\Builder;
|
|||
|
||||
use Shared\DDD\Usecase\Usecase;
|
||||
|
||||
/** @template TOutput of \Shared\DDD\Usecase\Output */
|
||||
/**
|
||||
* @template TInput of \Shared\DDD\Usecase\Input
|
||||
* @template TOutput of \Shared\DDD\Usecase\Output
|
||||
*/
|
||||
interface BuilderStepUsecase
|
||||
{
|
||||
/**
|
||||
* @param Usecase<\Shared\DDD\Usecase\Input,TOutput> $usecase
|
||||
* @param Usecase<TInput, TOutput> $usecase
|
||||
* @return BuilderStepInput<TOutput>
|
||||
*/
|
||||
public function do(Usecase $usecase): BuilderStepInput;
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ final class UsecaseBuilder
|
|||
* @template TInput of \Shared\DDD\Usecase\Input
|
||||
* @template TOutput of \Shared\DDD\Usecase\Output
|
||||
*
|
||||
* @param UsecaseExecutor<TInput,TOutput> $executor
|
||||
* @return BuilderStepUsecase<TOutput>
|
||||
* @param UsecaseExecutor<TInput, TOutput> $executor
|
||||
* @return BuilderStepUsecase<TInput, TOutput>
|
||||
*/
|
||||
public static function using(UsecaseExecutor $executor): BuilderStepUsecase
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,28 +12,30 @@ use Shared\DDD\Usecase\Usecase;
|
|||
/**
|
||||
* @template TInput of \Shared\DDD\Usecase\Input
|
||||
* @template TOutput of \Shared\DDD\Usecase\Output
|
||||
* @template TView of object
|
||||
*
|
||||
* @implements BuilderStepInput<TOutput>
|
||||
* @implements BuilderStepPresenter<TOutput>
|
||||
* @implements BuilderStepUsecase<TOutput>
|
||||
* @implements BuilderStepUsecase<TInput, TOutput>
|
||||
* @implements BuilderStepBuild<TView>
|
||||
*/
|
||||
final class UsecaseExecutionBuilder implements BuilderStepBuild, BuilderStepInput, BuilderStepPresenter, BuilderStepUsecase
|
||||
{
|
||||
/** @param UsecaseExecutor<TInput,TOutput> $executor */
|
||||
/** @param UsecaseExecutor<TInput, TOutput> $executor */
|
||||
public function __construct(
|
||||
private readonly UsecaseExecutor $executor,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @var Usecase<TInput,TOutput>|null */
|
||||
/** @var Usecase<TInput, TOutput>|null */
|
||||
private ?Usecase $usecase = null;
|
||||
|
||||
private ?Input $input = null;
|
||||
|
||||
/** @var Presenter<TOutput,object>|null */
|
||||
/** @var Presenter<TOutput, object>|null */
|
||||
private ?Presenter $presenter = null;
|
||||
|
||||
/** @param Usecase<TInput,TOutput> $usecase */
|
||||
/** @param Usecase<TInput, TOutput> $usecase */
|
||||
#[\Override]
|
||||
public function do(Usecase $usecase): BuilderStepInput
|
||||
{
|
||||
|
|
@ -54,7 +56,12 @@ final class UsecaseExecutionBuilder implements BuilderStepBuild, BuilderStepInpu
|
|||
return $this;
|
||||
}
|
||||
|
||||
/** @param Presenter<TOutput,object> $presenter */
|
||||
/**
|
||||
* @template TNextView of object
|
||||
*
|
||||
* @param Presenter<TOutput, TNextView> $presenter
|
||||
* @return BuilderStepBuild<TNextView>
|
||||
*/
|
||||
#[\Override]
|
||||
public function with(Presenter $presenter): BuilderStepBuild
|
||||
{
|
||||
|
|
@ -69,6 +76,7 @@ final class UsecaseExecutionBuilder implements BuilderStepBuild, BuilderStepInpu
|
|||
return $this;
|
||||
}
|
||||
|
||||
/** @return TView */
|
||||
#[\Override]
|
||||
public function run(): object
|
||||
{
|
||||
|
|
@ -76,6 +84,7 @@ final class UsecaseExecutionBuilder implements BuilderStepBuild, BuilderStepInpu
|
|||
throw new LogicException('Usecase, input and presenter are all required.');
|
||||
}
|
||||
|
||||
/** @var TView */
|
||||
return $this->executor->execute(
|
||||
usecase: $this->usecase,
|
||||
input: $this->input,
|
||||
|
|
|
|||
|
|
@ -15,8 +15,11 @@ use Shared\DDD\Usecase\Usecase;
|
|||
interface UsecaseExecutor
|
||||
{
|
||||
/**
|
||||
* @param Usecase<TInput,TOutput> $usecase
|
||||
* @param Presenter<TOutput,object> $presenter
|
||||
* @template TView of object
|
||||
*
|
||||
* @param Usecase<TInput, TOutput> $usecase
|
||||
* @param Presenter<TOutput, TView> $presenter
|
||||
* @return TView
|
||||
*/
|
||||
public function execute(Usecase $usecase, Input $input, Presenter $presenter): object;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue