refactor: adopt native PHP 8.5 \Uri\Rfc3986\Uri for request URIs and pagination links
This commit is contained in:
parent
7c0edb9507
commit
ac552a4530
14 changed files with 76 additions and 29 deletions
|
|
@ -7,6 +7,7 @@ namespace Infra\Http\Helpers;
|
|||
use Illuminate\Http\Request as HttpRequest;
|
||||
use League\Uri\Http;
|
||||
use Shared\Pagination;
|
||||
use Uri\Rfc3986\Uri;
|
||||
|
||||
final class RequestHelper
|
||||
{
|
||||
|
|
@ -35,6 +36,16 @@ final class RequestHelper
|
|||
);
|
||||
}
|
||||
|
||||
public static function toUri(HttpRequest $request): Uri
|
||||
{
|
||||
return new Uri($request->url());
|
||||
}
|
||||
|
||||
public static function toFullUri(HttpRequest $request): Uri
|
||||
{
|
||||
return new Uri($request->fullUrl());
|
||||
}
|
||||
|
||||
public static function toLeagueUri(HttpRequest $request): Http
|
||||
{
|
||||
return Http::new($request->url());
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use Illuminate\Support\ServiceProvider;
|
|||
use Infra\Http\Helpers\RequestHelper;
|
||||
use League\Uri\Http;
|
||||
use Shared\Pagination;
|
||||
use Uri\Rfc3986\Uri;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
@ -56,6 +57,16 @@ class AppServiceProvider extends ServiceProvider
|
|||
return RequestHelper::pagination($this, $defaultPageSize);
|
||||
});
|
||||
|
||||
Request::macro('toUri', function (): Uri {
|
||||
/** @var \Illuminate\Http\Request $this */
|
||||
return RequestHelper::toUri($this);
|
||||
});
|
||||
|
||||
Request::macro('toFullUri', function (): Uri {
|
||||
/** @var \Illuminate\Http\Request $this */
|
||||
return RequestHelper::toFullUri($this);
|
||||
});
|
||||
|
||||
Request::macro('toLeagueUri', function (): Http {
|
||||
/** @var \Illuminate\Http\Request $this */
|
||||
return RequestHelper::toLeagueUri($this);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace Presentation\Api\V1\Dto;
|
||||
|
||||
use League\Uri\Http;
|
||||
use Presentation\Api\V1\Contracts\ApiResponseDto;
|
||||
use Uri\Rfc3986\Uri;
|
||||
|
||||
final readonly class PaginationDto implements ApiResponseDto
|
||||
{
|
||||
|
|
@ -21,15 +21,15 @@ final readonly class PaginationDto implements ApiResponseDto
|
|||
) {
|
||||
}
|
||||
|
||||
public static function create(int $page, int $pageSize, int $totalItems, Http $baseUri): self
|
||||
public static function create(int $page, int $pageSize, int $totalItems, Uri $baseUri): self
|
||||
{
|
||||
$totalPages = $pageSize > 0 ? (int) ceil($totalItems / $pageSize) : 1;
|
||||
|
||||
$firstUrl = (string) $baseUri->withQuery(http_build_query(['page' => 1, 'pageSize' => $pageSize]));
|
||||
$lastUrl = (string) $baseUri->withQuery(http_build_query(['page' => $totalPages, 'pageSize' => $pageSize]));
|
||||
$selfUrl = (string) $baseUri->withQuery(http_build_query(['page' => $page, 'pageSize' => $pageSize]));
|
||||
$prevUrl = $page > 1 ? (string) $baseUri->withQuery(http_build_query(['page' => $page - 1, 'pageSize' => $pageSize])) : null;
|
||||
$nextUrl = $page < $totalPages ? (string) $baseUri->withQuery(http_build_query(['page' => $page + 1, 'pageSize' => $pageSize])) : null;
|
||||
$firstUrl = $baseUri->withQuery(http_build_query(['page' => 1, 'pageSize' => $pageSize]))->toString();
|
||||
$lastUrl = $baseUri->withQuery(http_build_query(['page' => $totalPages, 'pageSize' => $pageSize]))->toString();
|
||||
$selfUrl = $baseUri->withQuery(http_build_query(['page' => $page, 'pageSize' => $pageSize]))->toString();
|
||||
$prevUrl = $page > 1 ? $baseUri->withQuery(http_build_query(['page' => $page - 1, 'pageSize' => $pageSize]))->toString() : null;
|
||||
$nextUrl = $page < $totalPages ? $baseUri->withQuery(http_build_query(['page' => $page + 1, 'pageSize' => $pageSize]))->toString() : null;
|
||||
|
||||
return new self(
|
||||
page: $page,
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ use Illuminate\Http\JsonResponse;
|
|||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use League\Uri\Http;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
use Throwable;
|
||||
use Uri\Rfc3986\Uri;
|
||||
|
||||
final readonly class ApiExceptionRenderer
|
||||
{
|
||||
|
|
@ -21,11 +21,11 @@ final readonly class ApiExceptionRenderer
|
|||
|
||||
public function render(Throwable $e, Request $request): JsonResponse
|
||||
{
|
||||
$baseUri = $request->toLeagueUri();
|
||||
$baseUri = $request->toUri();
|
||||
|
||||
return match (true) {
|
||||
$e instanceof NotFoundException => response()->json([
|
||||
'type' => (string) $baseUri->withPath('/errors/404'),
|
||||
'type' => $baseUri->withPath('/errors/404')->toString(),
|
||||
'title' => 'Resource Not Found',
|
||||
'status' => 404,
|
||||
'detail' => $e->getMessage(),
|
||||
|
|
@ -33,7 +33,7 @@ final readonly class ApiExceptionRenderer
|
|||
], 404, ['Content-Type' => self::CONTENT_TYPE]),
|
||||
|
||||
$e instanceof ValidationException => response()->json([
|
||||
'type' => (string) $baseUri->withPath('/errors/422'),
|
||||
'type' => $baseUri->withPath('/errors/422')->toString(),
|
||||
'title' => 'Unprocessable Entity',
|
||||
'status' => 422,
|
||||
'detail' => $e->getMessage(),
|
||||
|
|
@ -59,7 +59,7 @@ final readonly class ApiExceptionRenderer
|
|||
], 422, ['Content-Type' => self::CONTENT_TYPE]),
|
||||
|
||||
$e instanceof \DomainException => response()->json([
|
||||
'type' => (string) $baseUri->withPath('/errors/422'),
|
||||
'type' => $baseUri->withPath('/errors/422')->toString(),
|
||||
'title' => 'Unprocessable Entity',
|
||||
'status' => 422,
|
||||
'detail' => $e->getMessage(),
|
||||
|
|
@ -73,7 +73,7 @@ final readonly class ApiExceptionRenderer
|
|||
], 422, ['Content-Type' => self::CONTENT_TYPE]),
|
||||
|
||||
$e instanceof ThrottleRequestsException => response()->json([
|
||||
'type' => (string) $baseUri->withPath('/errors/429'),
|
||||
'type' => $baseUri->withPath('/errors/429')->toString(),
|
||||
'title' => 'Too Many Requests',
|
||||
'status' => 429,
|
||||
'detail' => $e->getMessage(),
|
||||
|
|
@ -84,7 +84,7 @@ final readonly class ApiExceptionRenderer
|
|||
};
|
||||
}
|
||||
|
||||
private function renderDefault(Throwable $e, Request $request, Http $baseUri): JsonResponse
|
||||
private function renderDefault(Throwable $e, Request $request, Uri $baseUri): JsonResponse
|
||||
{
|
||||
$status = $e instanceof HttpExceptionInterface ? $e->getStatusCode() : 500;
|
||||
$title = Response::$statusTexts[$status] ?? 'Internal Server Error';
|
||||
|
|
@ -100,7 +100,7 @@ final readonly class ApiExceptionRenderer
|
|||
$headers['Content-Type'] = self::CONTENT_TYPE;
|
||||
|
||||
return response()->json([
|
||||
'type' => (string) $baseUri->withPath('/errors/' . $status),
|
||||
'type' => $baseUri->withPath('/errors/' . $status)->toString(),
|
||||
'title' => $title,
|
||||
'status' => $status,
|
||||
'detail' => $detail,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ final readonly class EmployeesIndexController
|
|||
->with($this->presenter)
|
||||
->run();
|
||||
|
||||
return new EmployeesIndexResponse($response, $request->toLeagueUri());
|
||||
return new EmployeesIndexResponse($response, $request->toUri());
|
||||
}
|
||||
|
||||
private function inputFrom(EmployeesIndexRequest $request): Input
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ final readonly class IncoSheetsIndexController
|
|||
->with($this->presenter)
|
||||
->run();
|
||||
|
||||
return new IncoSheetsIndexResponse($response, $request->toLeagueUri());
|
||||
return new IncoSheetsIndexResponse($response, $request->toUri());
|
||||
}
|
||||
|
||||
private function inputFrom(IncoSheetsIndexRequest $request): Input
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ final readonly class IncoSheetsSearchController
|
|||
|
||||
return new IncoSheetsSearchResponse(
|
||||
view: $view,
|
||||
baseUri: $request->toLeagueUri(),
|
||||
baseUri: $request->toUri(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use Shared\Pagination;
|
|||
|
||||
/**
|
||||
* @method \Shared\Pagination pagination(int $defaultPageSize = 25)
|
||||
* @method \League\Uri\Http toLeagueUri()
|
||||
* @method \Uri\Rfc3986\Uri toUri()
|
||||
*/
|
||||
final class IncoSheetsIndexRequest extends FormRequest
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use Illuminate\Foundation\Http\FormRequest;
|
|||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* @method \League\Uri\Http toLeagueUri()
|
||||
* @method \Uri\Rfc3986\Uri toUri()
|
||||
*/
|
||||
final class IncoSheetsSearchRequest extends FormRequest
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,17 +6,17 @@ namespace Presentation\Api\V1\Http\Responses\Employees;
|
|||
|
||||
use Application\Usecase\Employees\FindAll\PaginatedEmployee;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use League\Uri\Http;
|
||||
use Presentation\Api\V1\Contracts\ApiResponse;
|
||||
use Presentation\Api\V1\Dto\Employees\PaginatedEmployee as PaginatedEmployeeDto;
|
||||
use Presentation\Api\V1\Dto\PaginationDto;
|
||||
use Presentation\Api\V1\Presenters\Employees\EmployeesIndexView;
|
||||
use Uri\Rfc3986\Uri;
|
||||
|
||||
final readonly class EmployeesIndexResponse implements ApiResponse
|
||||
{
|
||||
public function __construct(
|
||||
private EmployeesIndexView $view,
|
||||
private Http $baseUri,
|
||||
private Uri $baseUri,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ use Application\Usecase\IncoSheets\FindAll\PaginatedShortIncoSheet;
|
|||
use Domain\Models\ValueObjects\Inco\Additive;
|
||||
use Domain\Models\ValueObjects\Inco\Allergen;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use League\Uri\Http;
|
||||
use Presentation\Api\V1\Contracts\ApiResponse;
|
||||
use Presentation\Api\V1\Dto\IncoSheets\IncoSheetDto;
|
||||
use Presentation\Api\V1\Dto\IncoSheets\IncoShortSheetDto;
|
||||
|
|
@ -23,12 +22,13 @@ use Presentation\Api\V1\Dto\IncoSheets\Parts\StorageConditionDto;
|
|||
use Presentation\Api\V1\Dto\IncoSheets\Parts\WeightDto;
|
||||
use Presentation\Api\V1\Dto\PaginationDto;
|
||||
use Presentation\Api\V1\Presenters\IncoSheets\IncoSheetsIndexView;
|
||||
use Uri\Rfc3986\Uri;
|
||||
|
||||
final readonly class IncoSheetsIndexResponse implements ApiResponse
|
||||
{
|
||||
public function __construct(
|
||||
private IncoSheetsIndexView $view,
|
||||
private Http $baseUri,
|
||||
private Uri $baseUri,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ use Application\Usecase\IncoSheets\Search\PaginatedShortIncoSheet;
|
|||
use Domain\Models\ValueObjects\Inco\Additive;
|
||||
use Domain\Models\ValueObjects\Inco\Allergen;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use League\Uri\Http;
|
||||
use Presentation\Api\V1\Contracts\ApiResponse;
|
||||
use Presentation\Api\V1\Dto\IncoSheets\IncoSheetDto;
|
||||
use Presentation\Api\V1\Dto\IncoSheets\IncoShortSheetDto;
|
||||
|
|
@ -23,12 +22,13 @@ use Presentation\Api\V1\Dto\IncoSheets\Parts\StorageConditionDto;
|
|||
use Presentation\Api\V1\Dto\IncoSheets\Parts\WeightDto;
|
||||
use Presentation\Api\V1\Dto\PaginationDto;
|
||||
use Presentation\Api\V1\Presenters\IncoSheets\IncoSheetsSearchView;
|
||||
use Uri\Rfc3986\Uri;
|
||||
|
||||
final readonly class IncoSheetsSearchResponse implements ApiResponse
|
||||
{
|
||||
public function __construct(
|
||||
private IncoSheetsSearchView $view,
|
||||
private Http $baseUri,
|
||||
private Uri $baseUri,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use Illuminate\Http\Request as HttpRequest;
|
|||
use Infra\Http\Helpers\RequestHelper;
|
||||
use League\Uri\Http;
|
||||
use Shared\Pagination;
|
||||
use Uri\Rfc3986\Uri;
|
||||
|
||||
covers(RequestHelper::class);
|
||||
|
||||
|
|
@ -90,6 +91,30 @@ describe('RequestHelper Unit Tests', function (): void {
|
|||
->and($pagination->to())->toBe(24);
|
||||
});
|
||||
|
||||
it('converts request URL to native RFC 3986 Uri instance', function (): void {
|
||||
// GIVEN
|
||||
$request = HttpRequest::create('http://localhost/api/v1/employees?foo=bar', 'GET');
|
||||
|
||||
// WHEN
|
||||
$uri = RequestHelper::toUri($request);
|
||||
|
||||
// THEN
|
||||
expect($uri)->toBeInstanceOf(Uri::class)
|
||||
->and($uri->toString())->toBe('http://localhost/api/v1/employees');
|
||||
});
|
||||
|
||||
it('converts full request URL with query parameters to native RFC 3986 Uri instance', function (): void {
|
||||
// GIVEN
|
||||
$request = HttpRequest::create('http://localhost/api/v1/employees?page=2&pageSize=10', 'GET');
|
||||
|
||||
// WHEN
|
||||
$uri = RequestHelper::toFullUri($request);
|
||||
|
||||
// THEN
|
||||
expect($uri)->toBeInstanceOf(Uri::class)
|
||||
->and($uri->toString())->toBe('http://localhost/api/v1/employees?page=2&pageSize=10');
|
||||
});
|
||||
|
||||
it('converts request URL to League Uri instance', function (): void {
|
||||
// GIVEN
|
||||
$request = HttpRequest::create('http://localhost/api/v1/employees?foo=bar', 'GET');
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@ declare(strict_types=1);
|
|||
|
||||
namespace Tests\Unit\Presentation\Api\V1\Dto;
|
||||
|
||||
use League\Uri\Http;
|
||||
use Presentation\Api\V1\Dto\PaginationDto;
|
||||
use Uri\Rfc3986\Uri;
|
||||
|
||||
covers(PaginationDto::class);
|
||||
|
||||
describe('PaginationDto Unit Tests', function (): void {
|
||||
it('creates pagination metadata and links correctly', function (): void {
|
||||
// GIVEN
|
||||
$baseUri = Http::new('http://localhost/api/v1/employees');
|
||||
$baseUri = new Uri('http://localhost/api/v1/employees');
|
||||
|
||||
// WHEN
|
||||
$dto = PaginationDto::create(
|
||||
|
|
@ -36,7 +36,7 @@ describe('PaginationDto Unit Tests', function (): void {
|
|||
|
||||
it('handles first page correctly with null prev link', function (): void {
|
||||
// GIVEN
|
||||
$baseUri = Http::new('http://localhost/api/v1/employees');
|
||||
$baseUri = new Uri('http://localhost/api/v1/employees');
|
||||
|
||||
// WHEN
|
||||
$dto = PaginationDto::create(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue