feat(RequestHelper): add pagination and URI conversion methods

Implement pagination logic and methods to convert request URLs to League URI instances.
This commit is contained in:
Jérémy Munsch 2026-08-13 14:52:51 +02:00
parent 8855f74bf0
commit 05ececd879
Signed by: forge-joniko
GPG key ID: D7EFE2F5CB6CC0E4
3 changed files with 175 additions and 76 deletions

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Infra\Http\Helpers;
use Illuminate\Http\Request as HttpRequest;
use League\Uri\Http;
use Shared\Pagination;
final class RequestHelper
{
private function __construct()
{
// Static class
}
public static function pagination(HttpRequest $request, int $defaultPageSize = 25): Pagination
{
if ($request->has('offset') || $request->has('limit')) {
return Pagination::ofOffsetAndLimit(
$request->integer('offset', 0),
$request->integer('limit', $defaultPageSize)
);
}
$pageSize = $request->integer(
'pageSize',
$request->integer('per_page', $request->integer('pagination', $defaultPageSize))
);
return Pagination::ofPage(
$request->integer('page', 1),
$pageSize
);
}
public static function toLeagueUri(HttpRequest $request): Http
{
return Http::new($request->url());
}
public static function toLeagueFullUri(HttpRequest $request): Http
{
return Http::new($request->fullUrl());
}
}

View file

@ -4,10 +4,8 @@ declare(strict_types=1);
namespace Infra\Providers;
use Carbon\CarbonInterface;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request as HttpRequest;
use Illuminate\Http\Request as IlluminateRequest;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Date;
@ -15,6 +13,7 @@ use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Infra\Http\Helpers\RequestHelper;
use League\Uri\Http;
use Shared\Pagination;
@ -56,93 +55,30 @@ class AppServiceProvider extends ServiceProvider
Date::setLocale(Config::string('app.locale'));
Date::setFallbackLocale(Config::string('app.fallback_locale'));
$provider = $this;
Date::macro('toLocalInput', function (CarbonInterface | \DateTimeInterface | int | float | string | null $date) use ($provider): ?string {
return $provider->toLocalInput($date);
Request::macro('pagination', function (int $defaultPageSize = 25): Pagination {
/** @var \Illuminate\Http\Request $this */
return RequestHelper::pagination($this, $defaultPageSize);
});
Date::macro('forHumans', function (CarbonInterface | \DateTimeInterface | int | float | string | null $date) use ($provider): ?string {
return $provider->forHumans($date);
Request::macro('toLeagueUri', function (): Http {
/** @var \Illuminate\Http\Request $this */
return RequestHelper::toLeagueUri($this);
});
Request::macro('pagination', function (int $defaultPageSize = 25) use ($provider): Pagination {
return $provider->pagination(request(), $defaultPageSize);
});
Request::macro('toLeagueUri', function () use ($provider): Http {
return $provider->toLeagueUri(request());
});
Request::macro('toLeagueFullUri', function () use ($provider): Http {
return $provider->toLeagueFullUri(request());
Request::macro('toLeagueFullUri', function (): Http {
/** @var \Illuminate\Http\Request $this */
return RequestHelper::toLeagueFullUri($this);
});
}
protected function configureRateLimiting(): void
{
RateLimiter::for('web', function (IlluminateRequest $request) {
RateLimiter::for('web', function (IlluminateRequest $_) {
return Limit::perMinute(Config::integer('throttles.web'))->by(resolve('clientIp'));
});
RateLimiter::for('api.v1', function (IlluminateRequest $request) {
RateLimiter::for('api.v1', function (IlluminateRequest $_) {
return Limit::perMinute(Config::integer('throttles.api.v1'))->by(resolve('clientIp'));
});
}
public function toLocalInput(CarbonInterface | \DateTimeInterface | int | float | string | null $date): ?string
{
if ($date === null) {
return null;
}
$timezone = request()->string('client_timezone', 'Europe/Paris')->value();
return Date::parse($date)
->setTimezone($timezone)
->format('Y-m-d\TH:i');
}
public function forHumans(CarbonInterface | \DateTimeInterface | int | float | string | null $date): ?string
{
if ($date === null) {
return null;
}
$timezone = request()->string('client_timezone', 'Europe/Paris')->value();
return Date::parse($date)
->setTimezone($timezone)
->isoFormat('ddd DD/MM/Y \à HH:mm');
}
public function pagination(HttpRequest $request, int $defaultPageSize): Pagination
{
if ($request->has('offset') || $request->has('limit')) {
return Pagination::ofOffsetAndLimit(
$request->integer('offset', 0),
$request->integer('limit', $defaultPageSize)
);
}
$pageSize = $request->integer(
'pageSize',
$request->integer('per_page', $request->integer('pagination', $defaultPageSize))
);
return Pagination::ofPage(
$request->integer('page', 1),
$pageSize
);
}
public function toLeagueUri(HttpRequest $request): Http
{
return Http::new($request->url());
}
public function toLeagueFullUri(HttpRequest $request): Http
{
return Http::new($request->fullUrl());
}
}

View file

@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Infra\Http\Helpers;
use Illuminate\Http\Request as HttpRequest;
use Infra\Http\Helpers\RequestHelper;
use League\Uri\Http;
use Shared\Pagination;
covers(RequestHelper::class);
describe('RequestHelper Unit Tests', function (): void {
it('prevents direct instantiation but covers constructor', function (): void {
// GIVEN
$reflection = new \ReflectionClass(RequestHelper::class);
// WHEN
$constructor = $reflection->getConstructor();
$constructor?->invoke($reflection->newInstanceWithoutConstructor());
// THEN
expect(fn () => $reflection->newInstance())
->toThrow(\ReflectionException::class);
});
it('creates offset and limit pagination when offset or limit query param is present', function (): void {
// GIVEN
$request = HttpRequest::create('/api/v1/employees?offset=10&limit=50', 'GET');
// WHEN
$pagination = RequestHelper::pagination($request, 25);
// THEN
expect($pagination)->toBeInstanceOf(Pagination::class)
->and($pagination->from())->toBe(10)
->and($pagination->to())->toBe(59);
});
it('creates page and page size pagination using pageSize query param', function (): void {
// GIVEN
$request = HttpRequest::create('/api/v1/employees?page=2&pageSize=15', 'GET');
// WHEN
$pagination = RequestHelper::pagination($request, 25);
// THEN
expect($pagination)->toBeInstanceOf(Pagination::class)
->and($pagination->from())->toBe(15)
->and($pagination->to())->toBe(29);
});
it('creates page and page size pagination using per_page query param fallback', function (): void {
// GIVEN
$request = HttpRequest::create('/api/v1/employees?page=3&per_page=20', 'GET');
// WHEN
$pagination = RequestHelper::pagination($request, 25);
// THEN
expect($pagination)->toBeInstanceOf(Pagination::class)
->and($pagination->from())->toBe(40)
->and($pagination->to())->toBe(59);
});
it('creates page and page size pagination using pagination query param fallback', function (): void {
// GIVEN
$request = HttpRequest::create('/api/v1/employees?page=1&pagination=30', 'GET');
// WHEN
$pagination = RequestHelper::pagination($request, 25);
// THEN
expect($pagination)->toBeInstanceOf(Pagination::class)
->and($pagination->from())->toBe(0)
->and($pagination->to())->toBe(29);
});
it('uses default page size when no pagination parameters are supplied', function (): void {
// GIVEN
$request = HttpRequest::create('/api/v1/employees', 'GET');
// WHEN
$pagination = RequestHelper::pagination($request, 25);
// THEN
expect($pagination)->toBeInstanceOf(Pagination::class)
->and($pagination->from())->toBe(0)
->and($pagination->to())->toBe(24);
});
it('converts request URL to League Uri instance', function (): void {
// GIVEN
$request = HttpRequest::create('http://localhost/api/v1/employees?foo=bar', 'GET');
// WHEN
$uri = RequestHelper::toLeagueUri($request);
// THEN
expect($uri)->toBeInstanceOf(Http::class)
->and((string) $uri)->toBe('http://localhost/api/v1/employees');
});
it('converts full request URL with query parameters to League Uri instance', function (): void {
// GIVEN
$request = HttpRequest::create('http://localhost/api/v1/employees?page=2&pageSize=10', 'GET');
// WHEN
$uri = RequestHelper::toLeagueFullUri($request);
// THEN
expect($uri)->toBeInstanceOf(Http::class)
->and((string) $uri)->toBe('http://localhost/api/v1/employees?page=2&pageSize=10');
});
});