Skip to content

Commit

Permalink
feat: switch to Guzzle
Browse files Browse the repository at this point in the history
The previously used library Buzz lacks support of symfony/options-resolver v7. It seems that
the package is not well-maintained anymore. So we switch to Guzzle instead. For the
user of the JobRouter REST Client library nothing has changed.
  • Loading branch information
brotkrueml committed Aug 21, 2024
1 parent b7bcf6a commit a38d16a
Show file tree
Hide file tree
Showing 16 changed files with 495 additions and 253 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed
- Switch to Guzzle

## [3.0.0] - 2024-02-21

### Added
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"php": ">=8.1",
"ext-curl": "*",
"ext-filter": "*",
"kriswallsmith/buzz": "^1.2",
"nyholm/psr7": "^1.8",
"guzzlehttp/guzzle": "^7.7",
"guzzlehttp/psr7": "^2.4",
"psr/http-client": "^1.0",
"psr/http-message": "^1.1 || ^2.0"
},
Expand Down
78 changes: 37 additions & 41 deletions src/Client/RestClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,49 +12,52 @@

namespace JobRouter\AddOn\RestClient\Client;

use Buzz\Browser;
use Buzz\Client\Curl;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Handler\CurlHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Utils;
use JobRouter\AddOn\RestClient\Configuration\ClientConfiguration;
use JobRouter\AddOn\RestClient\Exception\AuthenticationException;
use JobRouter\AddOn\RestClient\Exception\HttpException;
use JobRouter\AddOn\RestClient\Exception\RestClientException;
use JobRouter\AddOn\RestClient\Mapper\MultipartFormDataMapper;
use JobRouter\AddOn\RestClient\Mapper\RouteContentTypeMapper;
use JobRouter\AddOn\RestClient\Middleware\AuthorisationMiddleware;
use JobRouter\AddOn\RestClient\Middleware\UserAgentMiddleware;
use JobRouter\AddOn\RestClient\Resource\FileInterface;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
* RestClient for handling HTTP requests
* RestClient for handling HTTP requests to a JobRouter instance
*/
final class RestClient implements ClientInterface
{
private readonly Psr17Factory $psr17factory;
private readonly Browser $browser;
private readonly AuthorisationMiddleware $authorisationMiddleware;
private readonly Client $client;
private readonly RouteContentTypeMapper $routeContentTypeMapper;
private string $jobRouterVersion = '';
private string $authorisationToken = '';

/**
* Creates a RestClient instance, already authenticated against the JobRouter system
*
* @param ClientConfiguration $configuration The configuration
*
* @throws HttpException
*/
public function __construct(
private readonly ClientConfiguration $configuration,
) {
$this->psr17factory = new Psr17Factory();
$stack = HandlerStack::create();
$stack->setHandler(new CurlHandler());

Check warning on line 48 in src/Client/RestClient.php

View workflow job for this annotation

GitHub Actions / Code Quality

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ public function __construct(private readonly ClientConfiguration $configuration) { $stack = HandlerStack::create(); - $stack->setHandler(new CurlHandler()); + $stack->push((new UserAgentMiddleware())($this->configuration->getUserAgentAddition())); $stack->push((new AuthorisationMiddleware())($this->authorisationToken)); $options = [...$this->configuration->getClientOptions()->toArray(), ...['base_uri' => $configuration->getJobRouterSystem()->getBaseUrl(), 'handler' => $stack, 'synchronous' => true]];
$stack->push((new UserAgentMiddleware())($this->configuration->getUserAgentAddition()));
$stack->push((new AuthorisationMiddleware())($this->authorisationToken));

$client = new Curl($this->psr17factory, $this->configuration->getClientOptions()->toArray());
$this->browser = new Browser($client, $this->psr17factory);
$this->browser->addMiddleware(new UserAgentMiddleware($this->configuration->getUserAgentAddition()));
$this->authorisationMiddleware = new AuthorisationMiddleware();
$this->browser->addMiddleware($this->authorisationMiddleware);
$options = [
...$this->configuration->getClientOptions()->toArray(),
...[

Check warning on line 54 in src/Client/RestClient.php

View workflow job for this annotation

GitHub Actions / Code Quality

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ $stack->setHandler(new CurlHandler()); $stack->push((new UserAgentMiddleware())($this->configuration->getUserAgentAddition())); $stack->push((new AuthorisationMiddleware())($this->authorisationToken)); - $options = [...$this->configuration->getClientOptions()->toArray(), ...['base_uri' => $configuration->getJobRouterSystem()->getBaseUrl(), 'handler' => $stack, 'synchronous' => true]]; + $options = [...$this->configuration->getClientOptions()->toArray(), ...['handler' => $stack, 'synchronous' => true]]; $this->client = new Client($options); $this->routeContentTypeMapper = new RouteContentTypeMapper(); }
'base_uri' => $configuration->getJobRouterSystem()->getBaseUrl(),

Check warning on line 55 in src/Client/RestClient.php

View workflow job for this annotation

GitHub Actions / Code Quality

Escaped Mutant for Mutator "ArrayItem": @@ @@ $stack->setHandler(new CurlHandler()); $stack->push((new UserAgentMiddleware())($this->configuration->getUserAgentAddition())); $stack->push((new AuthorisationMiddleware())($this->authorisationToken)); - $options = [...$this->configuration->getClientOptions()->toArray(), ...['base_uri' => $configuration->getJobRouterSystem()->getBaseUrl(), 'handler' => $stack, 'synchronous' => true]]; + $options = [...$this->configuration->getClientOptions()->toArray(), ...['base_uri' > $configuration->getJobRouterSystem()->getBaseUrl(), 'handler' => $stack, 'synchronous' => true]]; $this->client = new Client($options); $this->routeContentTypeMapper = new RouteContentTypeMapper(); }
'handler' => $stack,
'synchronous' => true,

Check warning on line 57 in src/Client/RestClient.php

View workflow job for this annotation

GitHub Actions / Code Quality

Escaped Mutant for Mutator "TrueValue": @@ @@ $stack->setHandler(new CurlHandler()); $stack->push((new UserAgentMiddleware())($this->configuration->getUserAgentAddition())); $stack->push((new AuthorisationMiddleware())($this->authorisationToken)); - $options = [...$this->configuration->getClientOptions()->toArray(), ...['base_uri' => $configuration->getJobRouterSystem()->getBaseUrl(), 'handler' => $stack, 'synchronous' => true]]; + $options = [...$this->configuration->getClientOptions()->toArray(), ...['base_uri' => $configuration->getJobRouterSystem()->getBaseUrl(), 'handler' => $stack, 'synchronous' => false]]; $this->client = new Client($options); $this->routeContentTypeMapper = new RouteContentTypeMapper(); }
],
];
$this->client = new Client($options);

$this->routeContentTypeMapper = new RouteContentTypeMapper();
}
Expand All @@ -67,7 +70,7 @@ public function __construct(
*/
public function authenticate(): self
{
$this->authorisationMiddleware->resetToken();
$this->authorisationToken = '';

$options = [
'username' => $this->configuration->getUsername(),
Expand Down Expand Up @@ -97,7 +100,7 @@ public function authenticate(): self
throw new AuthenticationException('Token is unavailable', 1570222016);
}

$this->authorisationMiddleware->setToken($content['tokens'][0]);
$this->authorisationToken = $content['tokens'][0];

return $this;
}
Expand Down Expand Up @@ -137,7 +140,7 @@ public function request(string $method, string $resource, $data = []): ResponseI
} elseif ($contentType === 'application/json') {
$response = $this->sendJson($method, $resource, $data);
} else {
$response = $this->browser->sendRequest($this->buildRequest($method, $resource));
$response = $this->client->sendRequest($this->buildRequest($method, $resource));
}
} catch (ClientExceptionInterface $e) {
throw HttpException::fromError(
Expand Down Expand Up @@ -168,21 +171,17 @@ public function request(string $method, string $resource, $data = []): ResponseI
}

/**
* @param array<string, string|int|bool|FileInterface|array<string|int,mixed>> $multipart
* @param array<string, string|int|float|bool|FileInterface|array{path: non-empty-string, filename?: string, contentType?: string}> $data
* @throws GuzzleException
*/
private function sendForm(string $method, string $resource, array $multipart): ResponseInterface
private function sendForm(string $method, string $resource, array $data): ResponseInterface
{
\array_walk($multipart, static function (&$value): void {
if ($value instanceof FileInterface) {
$value = $value->toArray();
}
});
$multipart = (new MultipartFormDataMapper())->map($data);
$request = $this->buildRequest($method, $resource);

return $this->browser->submitForm(
$this->configuration->getJobRouterSystem()->getResourceUrl($resource),
$multipart,
$method,
);
return $this->client->send($request, [
'multipart' => $multipart,
]);
}

/**
Expand All @@ -198,18 +197,15 @@ private function sendJson(string $method, string $resource, string|array $jsonPa
}

if ($jsonPayload !== '') {
$request = $request->withBody($this->psr17factory->createStream($jsonPayload));
$request = $request->withBody(Utils::streamFor($jsonPayload));
}

return $this->browser->sendRequest($request);
return $this->client->sendRequest($request);
}

private function buildRequest(string $method, string $resource): RequestInterface
{
return $this->psr17factory->createRequest(
$method,
$this->configuration->getJobRouterSystem()->getResourceUrl($resource),
);
return new Request($method, $this->configuration->getJobRouterSystem()->getResourceUrl($resource));
}

/**
Expand Down
18 changes: 10 additions & 8 deletions src/Configuration/ClientOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,38 @@

/**
* Value object with available client options which can be defined
* @see \Buzz\Client\AbstractClient->configureOptions()
* @see https://docs.guzzlephp.org/en/stable/request-options.html
*/
final class ClientOptions
{
public function __construct(
private readonly bool $allowRedirects = false,
private readonly int $maxRedirects = 5,

Check warning on line 23 in src/Configuration/ClientOptions.php

View workflow job for this annotation

GitHub Actions / Code Quality

Escaped Mutant for Mutator "DecrementInteger": @@ @@ */ final class ClientOptions { - public function __construct(private readonly bool $allowRedirects = false, private readonly int $maxRedirects = 5, private readonly int|float $timeout = 0, private readonly bool $verify = true, private readonly ?string $proxy = null) + public function __construct(private readonly bool $allowRedirects = false, private readonly int $maxRedirects = 4, private readonly int|float $timeout = 0, private readonly bool $verify = true, private readonly ?string $proxy = null) { } /**

Check warning on line 23 in src/Configuration/ClientOptions.php

View workflow job for this annotation

GitHub Actions / Code Quality

Escaped Mutant for Mutator "IncrementInteger": @@ @@ */ final class ClientOptions { - public function __construct(private readonly bool $allowRedirects = false, private readonly int $maxRedirects = 5, private readonly int|float $timeout = 0, private readonly bool $verify = true, private readonly ?string $proxy = null) + public function __construct(private readonly bool $allowRedirects = false, private readonly int $maxRedirects = 6, private readonly int|float $timeout = 0, private readonly bool $verify = true, private readonly ?string $proxy = null) { } /**
private readonly int $timeout = 0,
private readonly int|float $timeout = 0,
private readonly bool $verify = true,
private readonly ?string $proxy = null,
) {}

/**
* @internal
* @return array{
* allow_redirects: bool,
* max_redirects: int,
* timeout: int,
* allow_redirects?: bool|array{max: int},
* timeout: int|float,
* verify: bool,
* proxy: string|null
* }
*/
public function toArray(): array
{
$allowRedirects = $this->allowRedirects ? [
'max' => $this->maxRedirects,
] : false;

/**
* @phpstan-ignore-next-line
* @phpstan-ignore-next-line Use value object over return of values
*/
return [
'allow_redirects' => $this->allowRedirects,
'max_redirects' => $this->maxRedirects,
'allow_redirects' => $allowRedirects,
'timeout' => $this->timeout,
'verify' => $this->verify,
'proxy' => $this->proxy,
Expand Down
33 changes: 33 additions & 0 deletions src/Exception/FileNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

/*
* This file is part of the JobRouter REST Client.
* https://github.com/jobrouter/php-rest-client
*
* For the full copyright and license information, please view the
* LICENSE.txt file that was distributed with this source code.
*/

namespace JobRouter\AddOn\RestClient\Exception;

final class FileNotFoundException extends \RuntimeException implements ExceptionInterface
{
public static function fromEmptyPath(): self
{
return new self('No file path given', 1724230703);
}

public static function fromPath(string $path, \Throwable $previous): self
{
return new self(
\sprintf(
'File with path "%s" does not exist',
$path,
),
1724230704,
$previous,
);
}
}
64 changes: 64 additions & 0 deletions src/Mapper/MultipartFormDataMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

/*
* This file is part of the JobRouter REST Client.
* https://github.com/jobrouter/php-rest-client
*
* For the full copyright and license information, please view the
* LICENSE.txt file that was distributed with this source code.
*/

namespace JobRouter\AddOn\RestClient\Mapper;

use GuzzleHttp\Psr7\Utils;
use JobRouter\AddOn\RestClient\Exception\FileNotFoundException;
use JobRouter\AddOn\RestClient\Resource\FileInterface;

final class MultipartFormDataMapper
{
/**
* @param array<string, string|int|float|bool|FileInterface|array{path: non-empty-string, filename?: string, contentType?: string}> $data
* @return list<array{name: string, contents: string|resource, filename? :string, headers? :array{Content-Type: string}}>
*/
public function map(array $data): array
{
return \array_map(static function (string $name, string|int|float|bool|FileInterface|array $value): array {
if ($value instanceof FileInterface) {
$value = $value->toArray();
}
if (\is_array($value)) {
// @phpstan-ignore-next-line Offset 'path' always exists and is not nullable.
if (! isset($value['path'])) {
throw FileNotFoundException::fromEmptyPath();
}

try {
$multipart = [
'name' => $name,
'contents' => Utils::tryFopen($value['path'], 'r'),
];
} catch (\RuntimeException $e) {
throw FileNotFoundException::fromPath($value['path'], $e);
}
if (($value['filename'] ?? '') !== '') {
$multipart['filename'] = $value['filename'];
}
if (($value['contentType'] ?? '') !== '') {
$multipart['headers'] = [
'Content-Type' => $value['contentType'],
];
}

return $multipart;
}

// @phpstan-ignore-next-line Use value object over return of values
return [
'name' => $name,
'contents' => (string)$value,
];
}, \array_keys($data), \array_values($data));

Check warning on line 62 in src/Mapper/MultipartFormDataMapper.php

View workflow job for this annotation

GitHub Actions / Code Quality

Escaped Mutant for Mutator "UnwrapArrayValues": @@ @@ } // @phpstan-ignore-next-line Use value object over return of values return ['name' => $name, 'contents' => (string) $value]; - }, \array_keys($data), \array_values($data)); + }, \array_keys($data), $data); } }
}
}
42 changes: 14 additions & 28 deletions src/Middleware/AuthorisationMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,26 @@

namespace JobRouter\AddOn\RestClient\Middleware;

use Buzz\Middleware\MiddlewareInterface;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
* @internal
*/
class AuthorisationMiddleware implements MiddlewareInterface
class AuthorisationMiddleware
{
private ?string $token = null;

public function setToken(
public function __invoke(
#[\SensitiveParameter]
string $token,
): void {
$this->token = $token;
}

public function resetToken(): void
{
$this->token = null;
}

public function handleRequest(RequestInterface $request, callable $next): ?RequestInterface
{
if ($this->token) {
$request = $request->withHeader('X-Jobrouter-Authorization', 'Bearer ' . $this->token);
}

return $next($request);
}

public function handleResponse(RequestInterface $request, ResponseInterface $response, callable $next): ?ResponseInterface
{
return $next($request, $response);
string &$token,
): callable {
return static function (callable $handler) use (&$token): callable {
return static function (RequestInterface $request, array $options) use ($handler, &$token): PromiseInterface {
if ($token !== '') {
$request = $request->withHeader('X-Jobrouter-Authorization', 'Bearer ' . $token);
}

return $handler($request, $options);
};
};
}
}
Loading

0 comments on commit a38d16a

Please sign in to comment.