Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
3ed4e82
fix: prevent duplicate rate limit headers in response
barbosa89 May 19, 2026
dc013f8
Merge pull request #131 from phenixphp/bugfix/prevent-duplicate-rate-…
barbosa89 May 20, 2026
14464ae
feat: implement Http Client class with CRUD methods and request handling
barbosa89 May 20, 2026
4955f00
feat: enhance authorization methods in Http Client with Basic, Digest…
barbosa89 May 20, 2026
312a49e
feat: implement retry mechanism for HTTP requests with RetryRequests …
barbosa89 May 20, 2026
aa4e513
refactor: move to dedicate namespace
barbosa89 May 20, 2026
a256f8e
feat: implement Response class to wrap Amp responses and provide data…
barbosa89 May 20, 2026
296c18d
feat: add serverError method and enhance onError handling in Response…
barbosa89 May 20, 2026
8ed50ba
feat: add RequestException class and implement throw methods in Respo…
barbosa89 May 20, 2026
040d778
feat: implement request pooling mechanism in HttpClient with concurre…
barbosa89 May 20, 2026
1e89a48
feat: add StreamResponse class and implement streaming functionality …
barbosa89 May 20, 2026
0166644
feat: enhance StreamResponse with read locking and temporary file han…
barbosa89 May 21, 2026
8aa531f
feat: add move method to File facade and implementation in File class
barbosa89 May 21, 2026
ce6630a
feat: add test for moving files in File class
barbosa89 May 21, 2026
f740c89
feat: add Http facade for simplified HTTP client interactions
barbosa89 May 21, 2026
e57fafb
feat: implement fake response functionality in HttpClient and add cor…
barbosa89 May 21, 2026
12e5d79
chore: move tests to client dir
barbosa89 May 21, 2026
0651e1b
feat: enhance HttpClient with query parameter handling, request metho…
barbosa89 May 21, 2026
9a6b381
feat: add event listener and logging methods to HttpClient for enhanc…
barbosa89 May 21, 2026
545e7ba
feat: add TLS context and certificate configuration methods to HttpCl…
barbosa89 May 21, 2026
8c69771
feat: implement IntersectRequests trait for handling fake responses i…
barbosa89 May 21, 2026
daef50a
chore: rename trait
barbosa89 May 21, 2026
5d7104b
feat: introduce ProtocolVersion enum and update fake response handlin…
barbosa89 May 21, 2026
20d6b81
style: php cs
barbosa89 May 21, 2026
1f01009
feat: implement HasHttpStatus trait for standardized HTTP status hand…
barbosa89 May 21, 2026
0ab8afd
refactor: simplify json method return logic in Response class
barbosa89 May 21, 2026
0f686c2
refactor: simplify retry condition logic in shouldRetry method
barbosa89 May 21, 2026
5a44e27
fix: report throwable in discardTemporaryFile method for better error…
barbosa89 May 21, 2026
ff0872c
refactor: add HasAuthorization trait for handling various authorizati…
barbosa89 May 21, 2026
dcc949a
test: add response state and headers tests for redirect and client er…
barbosa89 May 21, 2026
1af7e1e
test: add request callbacks for all supported HTTP methods in HttpCli…
barbosa89 May 21, 2026
dc78446
test: add additional retry scenarios for RetryRequests including max …
barbosa89 May 21, 2026
773772e
test: add tests for streamed response state and headers handling in S…
barbosa89 May 21, 2026
2270dde
feat: add withClient method to HttpClient for custom client configura…
barbosa89 May 21, 2026
8bb30d2
refactor: simplify StreamResponseTest by using DelegateHttpClient for…
barbosa89 May 21, 2026
30d9641
feat: add stream method to HttpClient for enhanced request handling w…
barbosa89 May 21, 2026
c0fa730
refactor: remove unused event listener function and simplify logging …
barbosa89 May 21, 2026
191982d
test: add created status helper assertion in ResponseTest
barbosa89 May 21, 2026
8890846
test: enhance StreamResponseTest to validate response status and cont…
barbosa89 May 21, 2026
aedf6a1
refactor: move interceptos to Client namespace
barbosa89 May 21, 2026
27bbba9
fix: prevent unnecessary retries in RetryRequests interceptor
barbosa89 May 21, 2026
3a33853
refactor: remove shared setting for HttpClient in container
barbosa89 May 22, 2026
f96a3f1
refactor: remove withDigestAuth method from HttpClient and related tests
barbosa89 May 22, 2026
57917b0
feat: implement HttpClientTestLogger for request logging and faking i…
barbosa89 May 23, 2026
b67ff15
feat: enhance request handling by introducing jsonBody method and pre…
barbosa89 May 23, 2026
0c55a9e
style: php cs
barbosa89 May 23, 2026
f966ec8
fix: ensure jsonBody method returns an empty string on json_encode fa…
barbosa89 May 23, 2026
e13a1f0
refactor: simplify response normalization logic in HttpClientTestLogger
barbosa89 May 23, 2026
94d942a
feat: add HasTls trait for TLS context and certificate handling in Ht…
barbosa89 May 23, 2026
71be070
refactor: replace for loop with do-while for retry attempts in RetryR…
barbosa89 May 23, 2026
f8d27a6
Merge pull request #132 from phenixphp/feature/http-facade
barbosa89 May 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
use Phenix\Exceptions\RuntimeError;
use Phenix\Facades\Config;
use Phenix\Facades\Route;
use Phenix\Http\Client\HttpClient;
use Phenix\Http\Client\HttpClientTestLogger;
use Phenix\Http\Constants\Protocol;
use Phenix\Http\ErrorHandler as AppErrorHandler;
use Phenix\Http\ExceptionHandler as AppExceptionHandler;
Expand Down Expand Up @@ -91,6 +93,9 @@ public function setup(): void
\Phenix\Runtime\Config::build(...)
)->setShared(true);

self::$container->add(HttpClient::class);
self::$container->add(HttpClientTestLogger::class)->setShared(true);

self::$container->add(Phenix::class)->addMethodCall('registerCommands');

/** @var array $providers */
Expand Down
12 changes: 8 additions & 4 deletions src/Cache/RateLimit/Middlewares/RateLimiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,14 @@ public function handleRequest(Request $request, RequestHandler $next): Response
$remaining = max(0, $perMinuteLimit - $current);
$resetTime = time() + $this->rateLimiter->getTtl($clientIp);

$response->addHeader('x-ratelimit-limit', (string) $perMinuteLimit);
$response->addHeader('x-ratelimit-remaining', (string) $remaining);
$response->addHeader('x-ratelimit-reset', (string) $resetTime);
$response->addHeader('x-ratelimit-reset-after', (string) $this->rateLimiter->getTtl($clientIp));
if ($isCustom || ! $response->hasHeader('x-ratelimit-limit')) {
$response->replaceHeaders([
'x-ratelimit-limit' => (string) $perMinuteLimit,
'x-ratelimit-remaining' => (string) $remaining,
'x-ratelimit-reset' => (string) $resetTime,
'x-ratelimit-reset-after' => (string) $this->rateLimiter->getTtl($clientIp),
]);
}

return $response;
}
Expand Down
1 change: 1 addition & 0 deletions src/Facades/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* @method static bool isDirectory(string $path)
* @method static bool isFile(string $path)
* @method static void createDirectory(string $path, int $mode = 0755)
* @method static void move(string $from, string $to)
* @method static \Amp\File\File openFile(string $path, string $mode = 'w')
* @method static int getCreationTime(string $path)
* @method static int getModificationTime(string $path)
Expand Down
55 changes: 55 additions & 0 deletions src/Facades/Http.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Phenix\Facades;

use Mockery\Expectation;
use Mockery\ExpectationInterface;
use Mockery\HigherOrderMessage;
use Phenix\App;
use Phenix\Http\Client\HttpClient;
use Phenix\Runtime\Facade;
use Phenix\Testing\Mockery;

/**
* @method static \Phenix\Http\Client\Response get(\Psr\Http\Message\UriInterface|string $url, array|null $queryParameters = null)
* @method static \Phenix\Http\Client\Response head(\Psr\Http\Message\UriInterface|string $url, array|null $queryParameters = null)
* @method static \Phenix\Http\Client\Response post(\Psr\Http\Message\UriInterface|string $url, \Amp\Http\Client\Form|\Phenix\Contracts\Arrayable|array|string $data = [])
* @method static \Phenix\Http\Client\Response put(\Psr\Http\Message\UriInterface|string $url, \Amp\Http\Client\Form|\Phenix\Contracts\Arrayable|array|string $data = [])
* @method static \Phenix\Http\Client\Response patch(\Psr\Http\Message\UriInterface|string $url, \Amp\Http\Client\Form|\Phenix\Contracts\Arrayable|array|string $data = [])
* @method static \Phenix\Http\Client\Response delete(\Psr\Http\Message\UriInterface|string $url, \Amp\Http\Client\Form|\Phenix\Contracts\Arrayable|array|string $data = [])
* @method static mixed stream(\Psr\Http\Message\UriInterface|string $url, \Closure|null $callback = null, array|null $queryParameters = null, int|null $bodySizeLimit = null, float|null $transferTimeout = null, \Phenix\Http\Constants\HttpMethod $method = \Phenix\Http\Constants\HttpMethod::GET, \Amp\Http\Client\Form|\Phenix\Contracts\Arrayable|array|string|null $data = null)
* @method static \Phenix\Http\Client\HttpClient withHeaders(array $headers)
* @method static \Phenix\Http\Client\HttpClient withClient(\Amp\Http\Client\HttpClient $client)
* @method static \Phenix\Http\Client\HttpClient withBasicAuth(string $username, string $password)
* @method static \Phenix\Http\Client\HttpClient withToken(string $token, string $type = 'Bearer')
* @method static \Phenix\Http\Client\HttpClient retry(int $times, \Closure|int $sleepMilliseconds = 0, callable|null $when = null)
* @method static \Phenix\Http\Client\HttpClient listen(\Amp\Http\Client\EventListener $eventListener)
* @method static \Phenix\Http\Client\HttpClient log(string $path)
* @method static \Phenix\Http\Client\HttpClient withTlsContext(\Amp\Socket\ClientTlsContext $tlsContext)
* @method static \Phenix\Http\Client\HttpClient withCertificate(string $certificate, string|null $key = null, string|null $ca = null, string|null $passphrase = null, string $peerName = '')
* @method static void fake(\Closure|null $response = null)
* @method static void fakeWhen(\Closure $condition, \Closure $response)
* @method static \Phenix\Data\Collection getRequestLog()
* @method static void resetRequestLog()
* @method static void resetFaking()
*
* @see \Phenix\Http\Client\HttpClient
*/
class Http extends Facade
{
public static function getKeyName(): string
{
return HttpClient::class;
}

public static function expect(string $method): Expectation|ExpectationInterface|HigherOrderMessage
{
$mock = Mockery::mock(self::getKeyName())->shouldAllowMockingProtectedMethods()->makePartial();

App::fake(self::getKeyName(), $mock);

return $mock->shouldReceive($method);
}
}
2 changes: 2 additions & 0 deletions src/Filesystem/Contracts/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ public function isDirectory(string $path): bool;
public function isFile(string $path): bool;

public function createDirectory(string $path, int $mode = 0777): void;

public function move(string $from, string $to): void;
}
5 changes: 5 additions & 0 deletions src/Filesystem/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ public function createDirectory(string $path, int $mode = 0755): void
$this->driver->createDirectory($path, $mode);
}

public function move(string $from, string $to): void
{
$this->driver->move($from, $to);
}

public function openFile(string $path, string $mode = 'w'): FileHandler
{
return $this->driver->openFile($path, $mode);
Expand Down
74 changes: 74 additions & 0 deletions src/Http/Client/Concerns/CaptureRequests.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

namespace Phenix\Http\Client\Concerns;

use Amp\Http\Client\Request;
use Closure;
use Phenix\App;
use Phenix\Data\Collection;
use Phenix\Http\Client\HttpClientTestLogger;
use Phenix\Http\Client\Response;
use Phenix\Http\Client\StreamResponse;

trait CaptureRequests
{
protected HttpClientTestLogger|null $requestLogger = null;

public function fake(Closure|null $response = null): void
{
if (App::isProduction()) {
return;
}

$this->getRequestLogger()->fake($response);
}

public function fakeWhen(Closure $condition, Closure $response): void
{
if (App::isProduction()) {
return;
}

$this->getRequestLogger()->fakeWhen($condition, $response);
}

/**
* @return Collection<Request>
*/
public function getRequestLog(): Collection
{
return $this->getRequestLogger()->getRequestLog();
}

public function resetRequestLog(): void
{
$this->getRequestLogger()->resetRequestLog();
}

public function resetFaking(): void
{
$this->getRequestLogger()->resetFaking();
}

protected function getRequestLogger(): HttpClientTestLogger
{
return $this->requestLogger ??= App::make(HttpClientTestLogger::class);
}

protected function recordRequest(Request $request): void
{
$this->getRequestLogger()->record($request);
}

protected function getFakeResponse(Request $request): Response|null
{
return $this->getRequestLogger()->getFakeResponse($request, $this);
}

protected function getFakeStreamResponse(Request $request): StreamResponse|null
{
return $this->getRequestLogger()->getFakeStreamResponse($request, $this);
}
}
27 changes: 27 additions & 0 deletions src/Http/Client/Concerns/HasAuthorization.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Phenix\Http\Client\Concerns;

use SensitiveParameter;

trait HasAuthorization
{
public function withBasicAuth(
string $username,
#[SensitiveParameter]
string $password
): self {
$this->headers['Authorization'] = 'Basic ' . base64_encode("{$username}:{$password}");

return $this;
}

public function withToken(#[SensitiveParameter] string $token, string $type = 'Bearer'): self
{
$this->headers['Authorization'] = "{$type} {$token}";

return $this;
}
}
90 changes: 90 additions & 0 deletions src/Http/Client/Concerns/HasHttpStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace Phenix\Http\Client\Concerns;

use Phenix\Http\Constants\HttpStatus;

trait HasHttpStatus
{
public function ok(): bool
{
return $this->hasStatus(HttpStatus::OK);
}

public function created(): bool
{
return $this->hasStatus(HttpStatus::CREATED);
}

public function accepted(): bool
{
return $this->hasStatus(HttpStatus::ACCEPTED);
}

public function noContent(): bool
{
return $this->hasStatus(HttpStatus::NO_CONTENT) && $this->body === '';
}

public function movedPermanently(): bool
{
return $this->hasStatus(HttpStatus::MOVED_PERMANENTLY);
}

public function found(): bool
{
return $this->hasStatus(HttpStatus::FOUND);
}

public function badRequest(): bool
{
return $this->hasStatus(HttpStatus::BAD_REQUEST);
}

public function unauthorized(): bool
{
return $this->hasStatus(HttpStatus::UNAUTHORIZED);
}

public function paymentRequired(): bool
{
return $this->hasStatus(HttpStatus::PAYMENT_REQUIRED);
}

public function forbidden(): bool
{
return $this->hasStatus(HttpStatus::FORBIDDEN);
}

public function notFound(): bool
{
return $this->hasStatus(HttpStatus::NOT_FOUND);
}

public function requestTimeout(): bool
{
return $this->hasStatus(HttpStatus::REQUEST_TIMEOUT);
}

public function conflict(): bool
{
return $this->hasStatus(HttpStatus::CONFLICT);
}

public function unprocessableEntity(): bool
{
return $this->hasStatus(HttpStatus::UNPROCESSABLE_ENTITY);
}

public function tooManyRequests(): bool
{
return $this->hasStatus(HttpStatus::TOO_MANY_REQUESTS);
}

private function hasStatus(HttpStatus $status): bool
{
return $this->status() === $status->value;
}
}
44 changes: 44 additions & 0 deletions src/Http/Client/Concerns/HasTls.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Phenix\Http\Client\Concerns;

use Amp\Http\Client\Connection\DefaultConnectionFactory;
use Amp\Http\Client\Connection\UnlimitedConnectionPool;
use Amp\Socket\Certificate;
use Amp\Socket\ClientTlsContext;
use Amp\Socket\ConnectContext;
use SensitiveParameter;

trait HasTls
{
public function withTlsContext(ClientTlsContext $tlsContext): self
{
$connectContext = (new ConnectContext())->withTlsContext($tlsContext);
$connectionFactory = new DefaultConnectionFactory(connectContext: $connectContext);

$this->builder = $this->builder->usingPool(new UnlimitedConnectionPool($connectionFactory));
$this->client = $this->builder->build();

return $this;
}

public function withCertificate(
string $certificate,
string|null $key = null,
string|null $ca = null,
#[SensitiveParameter]
string|null $passphrase = null,
string $peerName = ''
): self {
$tlsContext = (new ClientTlsContext($peerName))
->withCertificate(new Certificate($certificate, $key, $passphrase));

if ($ca !== null) {
$tlsContext = $tlsContext->withCaFile($ca);
}

return $this->withTlsContext($tlsContext);
}
}
14 changes: 14 additions & 0 deletions src/Http/Client/Constants/ProtocolVersion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Phenix\Http\Client\Constants;

enum ProtocolVersion: string
{
case V1_0 = '1.0';

case V1_1 = '1.1';

case V2 = '2';
}
Loading
Loading