From ac552a4530a590ee3496ab81a842ccb6e25f36e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Mon, 17 Aug 2026 09:30:21 +0200 Subject: [PATCH] refactor: adopt native PHP 8.5 \Uri\Rfc3986\Uri for request URIs and pagination links --- src/Infra/Http/Helpers/RequestHelper.php | 11 ++++++++ src/Infra/Providers/AppServiceProvider.php | 11 ++++++++ src/Presentation/Api/V1/Dto/PaginationDto.php | 14 +++++------ .../V1/Exceptions/ApiExceptionRenderer.php | 16 ++++++------ .../Employees/EmployeesIndexController.php | 2 +- .../IncoSheets/IncoSheetsIndexController.php | 2 +- .../IncoSheets/IncoSheetsSearchController.php | 2 +- .../IncoSheets/IncoSheetsIndexRequest.php | 2 +- .../IncoSheets/IncoSheetsSearchRequest.php | 2 +- .../Employees/EmployeesIndexResponse.php | 4 +-- .../IncoSheets/IncoSheetsIndexResponse.php | 4 +-- .../IncoSheets/IncoSheetsSearchResponse.php | 4 +-- .../Infra/Http/Helpers/RequestHelperTest.php | 25 +++++++++++++++++++ .../Api/V1/Dto/PaginationDtoTest.php | 6 ++--- 14 files changed, 76 insertions(+), 29 deletions(-) diff --git a/src/Infra/Http/Helpers/RequestHelper.php b/src/Infra/Http/Helpers/RequestHelper.php index 5668b7a..2c9a637 100644 --- a/src/Infra/Http/Helpers/RequestHelper.php +++ b/src/Infra/Http/Helpers/RequestHelper.php @@ -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()); diff --git a/src/Infra/Providers/AppServiceProvider.php b/src/Infra/Providers/AppServiceProvider.php index 6e7eaba..9e37f3f 100644 --- a/src/Infra/Providers/AppServiceProvider.php +++ b/src/Infra/Providers/AppServiceProvider.php @@ -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); diff --git a/src/Presentation/Api/V1/Dto/PaginationDto.php b/src/Presentation/Api/V1/Dto/PaginationDto.php index 87571f5..a15750b 100644 --- a/src/Presentation/Api/V1/Dto/PaginationDto.php +++ b/src/Presentation/Api/V1/Dto/PaginationDto.php @@ -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, diff --git a/src/Presentation/Api/V1/Exceptions/ApiExceptionRenderer.php b/src/Presentation/Api/V1/Exceptions/ApiExceptionRenderer.php index e2c77ad..cdab0ac 100644 --- a/src/Presentation/Api/V1/Exceptions/ApiExceptionRenderer.php +++ b/src/Presentation/Api/V1/Exceptions/ApiExceptionRenderer.php @@ -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, diff --git a/src/Presentation/Api/V1/Http/Controllers/Employees/EmployeesIndexController.php b/src/Presentation/Api/V1/Http/Controllers/Employees/EmployeesIndexController.php index d594fdc..4b84c0e 100644 --- a/src/Presentation/Api/V1/Http/Controllers/Employees/EmployeesIndexController.php +++ b/src/Presentation/Api/V1/Http/Controllers/Employees/EmployeesIndexController.php @@ -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 diff --git a/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsIndexController.php b/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsIndexController.php index 333a4ec..81b97ed 100644 --- a/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsIndexController.php +++ b/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsIndexController.php @@ -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 diff --git a/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsSearchController.php b/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsSearchController.php index 7a7898d..d2fda71 100644 --- a/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsSearchController.php +++ b/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsSearchController.php @@ -34,7 +34,7 @@ final readonly class IncoSheetsSearchController return new IncoSheetsSearchResponse( view: $view, - baseUri: $request->toLeagueUri(), + baseUri: $request->toUri(), ); } diff --git a/src/Presentation/Api/V1/Http/Requests/IncoSheets/IncoSheetsIndexRequest.php b/src/Presentation/Api/V1/Http/Requests/IncoSheets/IncoSheetsIndexRequest.php index 1ea2a84..4e36c92 100644 --- a/src/Presentation/Api/V1/Http/Requests/IncoSheets/IncoSheetsIndexRequest.php +++ b/src/Presentation/Api/V1/Http/Requests/IncoSheets/IncoSheetsIndexRequest.php @@ -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 { diff --git a/src/Presentation/Api/V1/Http/Requests/IncoSheets/IncoSheetsSearchRequest.php b/src/Presentation/Api/V1/Http/Requests/IncoSheets/IncoSheetsSearchRequest.php index 2e0511d..633b800 100644 --- a/src/Presentation/Api/V1/Http/Requests/IncoSheets/IncoSheetsSearchRequest.php +++ b/src/Presentation/Api/V1/Http/Requests/IncoSheets/IncoSheetsSearchRequest.php @@ -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 { diff --git a/src/Presentation/Api/V1/Http/Responses/Employees/EmployeesIndexResponse.php b/src/Presentation/Api/V1/Http/Responses/Employees/EmployeesIndexResponse.php index 0b3e1c8..218c624 100644 --- a/src/Presentation/Api/V1/Http/Responses/Employees/EmployeesIndexResponse.php +++ b/src/Presentation/Api/V1/Http/Responses/Employees/EmployeesIndexResponse.php @@ -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, ) { } diff --git a/src/Presentation/Api/V1/Http/Responses/IncoSheets/IncoSheetsIndexResponse.php b/src/Presentation/Api/V1/Http/Responses/IncoSheets/IncoSheetsIndexResponse.php index 174761d..7f4f9d6 100644 --- a/src/Presentation/Api/V1/Http/Responses/IncoSheets/IncoSheetsIndexResponse.php +++ b/src/Presentation/Api/V1/Http/Responses/IncoSheets/IncoSheetsIndexResponse.php @@ -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, ) { } diff --git a/src/Presentation/Api/V1/Http/Responses/IncoSheets/IncoSheetsSearchResponse.php b/src/Presentation/Api/V1/Http/Responses/IncoSheets/IncoSheetsSearchResponse.php index 32b3297..49650eb 100644 --- a/src/Presentation/Api/V1/Http/Responses/IncoSheets/IncoSheetsSearchResponse.php +++ b/src/Presentation/Api/V1/Http/Responses/IncoSheets/IncoSheetsSearchResponse.php @@ -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, ) { } diff --git a/tests/Unit/Infra/Http/Helpers/RequestHelperTest.php b/tests/Unit/Infra/Http/Helpers/RequestHelperTest.php index 9c80e39..b675b7a 100644 --- a/tests/Unit/Infra/Http/Helpers/RequestHelperTest.php +++ b/tests/Unit/Infra/Http/Helpers/RequestHelperTest.php @@ -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'); diff --git a/tests/Unit/Presentation/Api/V1/Dto/PaginationDtoTest.php b/tests/Unit/Presentation/Api/V1/Dto/PaginationDtoTest.php index 078af24..7430251 100644 --- a/tests/Unit/Presentation/Api/V1/Dto/PaginationDtoTest.php +++ b/tests/Unit/Presentation/Api/V1/Dto/PaginationDtoTest.php @@ -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(