Download project

This commit is contained in:
Roman Pyrih
2024-11-20 09:09:44 +01:00
parent 547a138d6a
commit 5ff041757f
40737 changed files with 7766183 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
name-template: v$NEXT_PATCH_VERSION
tag-template: v$NEXT_PATCH_VERSION
categories:
- title: 🔨 Improvements
label: enhancement
- title: 🐛 Bug Fixes
label: bug
- title: 🚀 New Features
label: Feature
change-template: '- #$NUMBER: $TITLE by @$AUTHOR'
template: |
# Changes
$CHANGES

View File

@@ -0,0 +1,117 @@
name: PHP tests
on: [push, pull_request]
jobs:
php-linter:
name: PHP Syntax check 5.6|7.2|7.3|8.0|8.1
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: PHP syntax checker 5.6
uses: prestashop/github-action-php-lint/5.6@master
with:
folder-to-exclude: '! -path "./src/Guzzle7/*"'
- name: PHP syntax checker 7.2
uses: prestashop/github-action-php-lint/7.2@master
- name: PHP syntax checker 8.0
uses: prestashop/github-action-php-lint/8.0@master
- name: PHP syntax checker 8.1
uses: prestashop/github-action-php-lint/8.1@master
php-cs-fixer:
name: PHP-CS-Fixer
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Cache vendor folder
uses: actions/cache@v2
with:
path: vendor
key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
- name: Cache composer folder
uses: actions/cache@v2
with:
path: ~/.composer/cache
key: php-composer-cache
- run: composer install
- name: Run PHP-CS-Fixer
run: vendor/bin/php-cs-fixer fix --config .php_cs.dist.php --diff --dry-run
phpstan:
name: PHPStan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Cache vendor folder
uses: actions/cache@v2
with:
path: vendor
key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
- name: Cache composer folder
uses: actions/cache@v2
with:
path: ~/.composer/cache
key: php-composer-cache
- run: composer install
- run: composer install
working-directory: tests/Guzzle5
- run: composer install
working-directory: tests/Guzzle7
- name: Run PHPStan for Guzzle 5
run: vendor/bin/phpstan analyse -c tests/Guzzle5/phpstan.neon
- name: Run PHPStan for Guzzle 7
run: vendor/bin/phpstan analyse -c tests/Guzzle7/phpstan.neon
phpunit:
name: PHPUnit
strategy:
matrix:
version: [5.6, 7.2, 7.4, 8.1]
runs-on: ubuntu-latest
steps:
- name: Setup PHP with tools
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.version }}
- name: Checkout
uses: actions/checkout@v2
- name: Cache vendor folder
uses: actions/cache@v2
with:
path: vendor
key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
- name: Cache composer folder
uses: actions/cache@v2
with:
path: ~/.composer/cache
key: php-composer-cache
- run: COMPOSER=composer-legacy.json composer install
- name: Run PHPUnit
if: ${{ matrix.version == '5.6' }}
run: vendor/bin/phpunit tests/unit/Guzzle5ConfigTest.php
- name: Run PHPUnit
if: ${{ matrix.version == '7.2' || matrix.version == '7.4' || matrix.version == '8.1' }}
run: vendor/bin/phpunit tests/unit

View File

@@ -0,0 +1,3 @@
/**/vendor/
composer.lock
.history

View File

@@ -0,0 +1,11 @@
<?php
$config = new PrestaShop\CodingStandards\CsFixer\Config();
$config
->setUsingCache(false)
->getFinder()
->in(__DIR__)
->exclude('vendor');
return $config;

View File

@@ -0,0 +1,65 @@
# PrestaShop module library for Guzzle clients
Plug modules to the Guzzle client available on a running shop.
This library is compatible with PHP 5.6.0 and above. Please check [Version Guidance](#version-guidance) for the PHP version compatibility.
[![Latest Stable Version](https://img.shields.io/packagist/v/prestashop/module-lib-guzzle-adapter.svg?style=flat-square)](https://packagist.org/packages/prestashop/module-lib-guzzle-adapter) [![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%205.6.0-8892BF.svg?style=flat-square)](https://php.net/) [![Quality Control Status](https://img.shields.io/github/workflow/status/prestashopcorp/module-lib-guzzle-adapter/PHP%20tests?style=flat-square)](https://github.com/PrestaShopCorp/module-lib-guzzle-adapter/actions/workflows/php.yml)
## Installation
```
composer require prestashop/module-lib-guzzle-adapter
```
## Version Guidance
| Version | Status | Packagist -| Namespace | Repo | Docs | PHP Version |
|---------|----------------|----------------------|--------------|---------------------|---------------------|--------------|
| <=0.5 | Stable | `prestashop/module-lib-guzzle-adapter` | `Prestashop\ModuleLibGuzzleAdapter` | [v0.x][lib-1-repo] | N/A | >=7.2.5 |
| >=0.6 | Latest | `prestashop/module-lib-guzzle-adapter` | `Prestashop\ModuleLibGuzzleAdapter` | [v0.x][lib-php5-repo] | N/A | >=5.6.0 |
| 1.x | Latest | `prestashop/module-lib-guzzle-adapter` | `Prestashop\ModuleLibGuzzleAdapter` | [v1.x][lib-1-repo] | N/A | >=7.2.5 |
[lib-1-repo]: https://github.com/PrestaShopCorp/module-lib-guzzle-adapter/tree/main
[lib-php5-repo]: https://github.com/PrestaShopCorp/module-lib-guzzle-adapter/tree/0.x
## Usage
```php
# Getting a client (Psr\Http\Client\ClientInterface)
$options = ['base_url' => 'http://some-url/'];
$client = (new Prestashop\ModuleLibGuzzleAdapter\ClientFactory())->getClient($options);
# Sending requests and receive response (Psr\Http\Message\ResponseInterface)
$response = $this->client->sendRequest(
new GuzzleHttp\Psr7\Request('POST', 'some-uri')
);
```
In this example, `base_url` is known to be a option for Guzzle 5 that has been replaced for `base_uri` on Guzzle 6+. Any of this two keys can be set, as it will be automatically modified for the other client if needed.
The automatically changed properties are:
| Guzzle 5 property | | Guzzle 7 property |
| ------------- | -- | ------------- |
| base_url | <=> | base_url |
| defaults.authorization | <=> | authorization |
| defaults.exceptions | <=> | http_errors |
| defaults.timeout | <=> | timeout |
## Why this library?
Making HTTP requests in a PrestaShop module can be done in several ways. With `file_get_contents()`, cURL or Guzzle when provided by the core.
Depending on the running version of PrestaShop, the bundled version of Guzzle can be different:
* PrestaShop 1.7: Guzzle 5
* PrestaShop 8: Guzzle 7
Having a module compatible for these two major PrestaShop versions can be tricky. The classes provided by the two Guzzle version are named the same, but their methods are different.
It is not possible for a module contributor to require its own Guzzle dependency either, because PHP cannot load different versions of a same class and he would never know which one would be loaded first.
## Implementation notes
This library reuses the idea behind [PHP-HTTP](https://docs.php-http.org), where the implementation of HTTP requests should be the same (PSR) whatever the client chosen.
The client files from [php-http/guzzle5-adapter](https://github.com/php-http/guzzle5-adapter) and [php-http/guzzle7-adapter](https://github.com/php-http/guzzle7-adapter) have been copied in this repository because these libraries both require a different version Guzzle in their dependencies to work. Requiring them together would conflict, so we duplicated the client adapters to be safe.

View File

@@ -0,0 +1,39 @@
{
"name": "prestashop/module-lib-guzzle-adapter",
"description": "Plug modules to the Guzzle client available on a running shop",
"license": "AFL-3.0",
"autoload": {
"psr-4": {
"Prestashop\\ModuleLibGuzzleAdapter\\": "src/"
}
},
"authors": [
{
"name": "PrestaShop SA",
"email": "contact@prestashop.com"
}
],
"provide": {
"php-http/client-implementation": "1.0",
"psr/http-client-implementation": "1.0"
},
"require": {
"php": ">=5.6.0",
"psr/http-message": "^1.0",
"php-http/message": "^1.7",
"php-http/httplug": "~1.1",
"guzzlehttp/psr7": "~1.9"
},
"require-dev": {
"phpunit/phpunit": "^9.5|^8.5|^5.7",
"prestashop/php-dev-tools": "^4.2|^3.16"
},
"scripts": {
"phpstan-g5": "vendor/bin/phpstan analyze -c tests/Guzzle5/phpstan.neon",
"phpstan-g7": "vendor/bin/phpstan analyze -c tests/Guzzle7/phpstan.neon",
"phpstan": [
"@phpstan-g5",
"@phpstan-g7"
]
}
}

View File

@@ -0,0 +1,40 @@
{
"name": "prestashop/module-lib-guzzle-adapter",
"description": "Plug modules to the Guzzle client available on a running shop",
"license": "AFL-3.0",
"autoload": {
"psr-4": {
"Prestashop\\ModuleLibGuzzleAdapter\\": "src/"
}
},
"authors": [
{
"name": "PrestaShop SA",
"email": "contact@prestashop.com"
}
],
"provide": {
"php-http/client-implementation": "1.0",
"psr/http-client-implementation": "1.0"
},
"require": {
"php": ">=5.6.0",
"psr/http-message": "^1.0",
"php-http/message": "^1.7",
"php-http/httplug": "~1.1",
"guzzlehttp/psr7": "~1.9"
},
"require-dev": {
"phpstan/phpstan": "^1.7",
"phpunit/phpunit": "^9.5|^8.5|^5.7",
"prestashop/php-dev-tools": "^4.2|^3.16"
},
"scripts": {
"phpstan-g5": "vendor/bin/phpstan analyze -c tests/Guzzle5/phpstan.neon",
"phpstan-g7": "vendor/bin/phpstan analyze -c tests/Guzzle7/phpstan.neon",
"phpstan": [
"@phpstan-g5",
"@phpstan-g7"
]
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Prestashop\ModuleLibGuzzleAdapter;
use Prestashop\ModuleLibGuzzleAdapter\Guzzle5\Client as Guzzle5Client;
use Prestashop\ModuleLibGuzzleAdapter\Guzzle5\Config as Guzzle5Config;
use Prestashop\ModuleLibGuzzleAdapter\Guzzle7\Client as Guzzle7Client;
use Prestashop\ModuleLibGuzzleAdapter\Guzzle7\Config as Guzzle7Config;
class ClientFactory
{
/**
* @var VersionDetection
*/
private $versionDetection;
public function __construct(VersionDetection $versionDetection = null)
{
$this->versionDetection = $versionDetection ?: new VersionDetection();
}
/**
* @param array<string, mixed> $config
*
* @return \Prestashop\ModuleLibGuzzleAdapter\Interfaces\HttpClientInterface
*/
public function getClient(array $config = [])
{
return $this->initClient($config);
}
/**
* @param array<string, mixed> $config
*
* @return \Prestashop\ModuleLibGuzzleAdapter\Interfaces\HttpClientInterface
*/
private function initClient(array $config = [])
{
if ($this->versionDetection->getGuzzleMajorVersionNumber() >= 7) {
return Guzzle7Client::createWithConfig(
Guzzle7Config::fixConfig($config)
);
}
return Guzzle5Client::createWithConfig(
Guzzle5Config::fixConfig($config)
);
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Prestashop\ModuleLibGuzzleAdapter\Guzzle5;
use Exception;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception as GuzzleExceptions;
use GuzzleHttp\Message\RequestInterface as GuzzleRequest;
use GuzzleHttp\Message\ResponseInterface as GuzzleResponse;
use GuzzleHttp\Psr7\Response;
use Http\Client\Exception as HttplugException;
use Prestashop\ModuleLibGuzzleAdapter\Interfaces\HttpClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* @author GeLo <geloen.eric@gmail.com>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*
* @see https://github.com/php-http/guzzle5-adapter/blob/master/src/Client.php
*/
class Client implements HttpClientInterface
{
/**
* @var ClientInterface
*/
private $client;
/**
* @param ClientInterface|null $client
*/
public function __construct(ClientInterface $client = null)
{
$this->client = $client ?: new GuzzleClient();
}
/**
* Factory method to create the Guzzle 5 adapter with custom Guzzle configuration.
* Added after duplication of adapter.
*
* @param array<string, mixed> $config
*
* @return self
*/
public static function createWithConfig(array $config)
{
return new self(new GuzzleClient($config));
}
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request)
{
$guzzleRequest = $this->createRequest($request);
try {
$response = $this->client->send($guzzleRequest);
} catch (GuzzleExceptions\TransferException $e) {
throw $this->handleException($e, $request);
}
return $this->createResponse($response);
}
/**
* Converts a PSR request into a Guzzle request.
*
* @param RequestInterface $request
*
* @return GuzzleRequest
*/
private function createRequest(RequestInterface $request)
{
$options = [
'exceptions' => false,
'allow_redirects' => false,
];
$options['version'] = $request->getProtocolVersion();
$options['headers'] = $request->getHeaders();
$body = (string) $request->getBody();
$options['body'] = '' === $body ? null : $body;
return $this->client->createRequest(
$request->getMethod(),
(string) $request->getUri(),
$options
);
}
/**
* Converts a Guzzle response into a PSR response.
*
* @param GuzzleResponse $response
*
* @return ResponseInterface
*/
private function createResponse(GuzzleResponse $response)
{
$body = $response->getBody();
return new Response(
$response->getStatusCode(),
$response->getHeaders(),
isset($body) ? $body->detach() : null,
$response->getProtocolVersion()
);
}
/**
* Converts a Guzzle exception into an Httplug exception.
*
* @param GuzzleExceptions\TransferException $exception
* @param RequestInterface $request
*
* @return \Exception
*/
private function handleException(GuzzleExceptions\TransferException $exception, RequestInterface $request)
{
if ($exception instanceof GuzzleExceptions\ConnectException) {
return new HttplugException\NetworkException($exception->getMessage(), $request, $exception);
}
if ($exception instanceof GuzzleExceptions\RequestException) {
// Make sure we have a response for the HttpException
if ($exception->hasResponse()) {
$psr7Response = $this->createResponse($exception->getResponse());
return new HttplugException\HttpException(
$exception->getMessage(),
$request,
$psr7Response,
$exception
);
}
return new HttplugException\RequestException($exception->getMessage(), $request, $exception);
}
return new HttplugException\TransferException($exception->getMessage(), 0, $exception);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Prestashop\ModuleLibGuzzleAdapter\Guzzle5;
use Prestashop\ModuleLibGuzzleAdapter\Interfaces\ConfigInterface;
class Config implements ConfigInterface
{
/**
* {@inheritdoc}
*/
public static function fixConfig(array $config)
{
if (isset($config['timeout'])) {
$config['defaults']['timeout'] = $config['timeout'];
unset($config['timeout']);
}
if (isset($config['headers'])) {
$config['defaults']['headers'] = $config['headers'];
unset($config['headers']);
}
if (isset($config['http_errors'])) {
$config['defaults']['exceptions'] = $config['http_errors'];
unset($config['http_errors']);
}
if (isset($config['base_uri'])) {
$config['base_url'] = $config['base_uri'];
unset($config['base_uri']);
}
return $config;
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Prestashop\ModuleLibGuzzleAdapter\Guzzle7;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Utils;
use Prestashop\ModuleLibGuzzleAdapter\Interfaces\HttpClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* HTTP Adapter for Guzzle 7.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*
* @see https://github.com/php-http/guzzle7-adapter/blob/master/src/Client.php
*/
class Client implements HttpClientInterface
{
/**
* @var ClientInterface
*/
private $client;
public function __construct(ClientInterface $client = null)
{
if (!$client) {
$client = self::buildClient();
}
$this->client = $client;
}
/**
* Factory method to create the Guzzle 7 adapter with custom Guzzle configuration.
*
* @param array<string, mixed> $config
*
* @return self
*/
public static function createWithConfig(array $config): Client
{
return new self(self::buildClient($config));
}
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request): ResponseInterface
{
return $this->sendAsyncRequest($request)->wait();
}
/**
* {@inheritdoc}
*/
public function sendAsyncRequest(RequestInterface $request): Promise
{
$promise = $this->client->sendAsync($request);
return new Promise($promise, $request);
}
/**
* Build the Guzzle client instance.
*
* @param array<string, mixed> $config
*/
private static function buildClient(array $config = []): GuzzleClient
{
$handlerStack = new HandlerStack(Utils::chooseHandler());
$handlerStack->push(Middleware::prepareBody(), 'prepare_body');
$config = array_merge(['handler' => $handlerStack], $config);
return new GuzzleClient($config);
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Prestashop\ModuleLibGuzzleAdapter\Guzzle7;
use Prestashop\ModuleLibGuzzleAdapter\Interfaces\ConfigInterface;
class Config implements ConfigInterface
{
/**
* {@inheritdoc}
*/
public static function fixConfig(array $config): array
{
if (isset($config['defaults'])) {
if (isset($config['defaults']['timeout'])) {
$config['timeout'] = $config['defaults']['timeout'];
}
if (isset($config['defaults']['exceptions'])) {
$config['http_errors'] = $config['defaults']['exceptions'];
}
if (isset($config['defaults']['headers'])) {
$config['headers'] = $config['defaults']['headers'];
}
unset($config['defaults']);
}
if (isset($config['base_url'])) {
$config['base_uri'] = $config['base_url'];
unset($config['base_url']);
}
return $config;
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Prestashop\ModuleLibGuzzleAdapter\Guzzle7\Exception;
use Http\Client\Exception;
final class UnexpectedValueException extends \UnexpectedValueException implements Exception
{
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Prestashop\ModuleLibGuzzleAdapter\Guzzle7;
use GuzzleHttp\Exception as GuzzleExceptions;
use GuzzleHttp\Promise\PromiseInterface;
use Http\Client\Exception as HttplugException;
use Http\Promise\Promise as HttpPromise;
use Prestashop\ModuleLibGuzzleAdapter\Guzzle7\Exception\UnexpectedValueException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Wrapper around Guzzle promises.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class Promise implements HttpPromise
{
/**
* @var PromiseInterface
*/
private $promise;
/**
* @var string State of the promise
*/
private $state;
/**
* @var ResponseInterface
*/
private $response;
/**
* @var HttplugException
*/
private $exception;
/**
* @var RequestInterface
*/
private $request;
public function __construct(PromiseInterface $promise, RequestInterface $request)
{
$this->request = $request;
$this->state = self::PENDING;
$this->promise = $promise->then(function ($response) {
$this->response = $response;
$this->state = self::FULFILLED;
return $response;
}, function ($reason) use ($request) {
$this->state = self::REJECTED;
if ($reason instanceof HttplugException) {
$this->exception = $reason;
} elseif ($reason instanceof GuzzleExceptions\GuzzleException) {
$this->exception = $this->handleException($reason, $request);
} elseif ($reason instanceof \Throwable) {
$this->exception = new HttplugException\TransferException('Invalid exception returned from Guzzle7', 0, $reason);
} else {
$this->exception = new UnexpectedValueException('Reason returned from Guzzle7 must be an Exception');
}
throw $this->exception;
});
}
/**
* {@inheritdoc}
*/
public function then(callable $onFulfilled = null, callable $onRejected = null)
{
return new static($this->promise->then($onFulfilled, $onRejected), $this->request);
}
/**
* {@inheritdoc}
*/
public function getState()
{
return $this->state;
}
/**
* {@inheritdoc}
*/
public function wait($unwrap = true)
{
$this->promise->wait(false);
if ($unwrap) {
if (self::REJECTED == $this->getState()) {
throw $this->exception;
}
return $this->response;
}
}
/**
* Converts a Guzzle exception into an Httplug exception.
*
* @return HttplugException
*/
private function handleException(GuzzleExceptions\GuzzleException $exception, RequestInterface $request)
{
if ($exception instanceof GuzzleExceptions\ConnectException) {
return new HttplugException\NetworkException($exception->getMessage(), $exception->getRequest(), $exception);
}
if ($exception instanceof GuzzleExceptions\RequestException) {
// Make sure we have a response for the HttpException
if ($exception->hasResponse()) {
return new HttplugException\HttpException(
$exception->getMessage(),
$exception->getRequest(),
$exception->getResponse(),
$exception
);
}
return new HttplugException\RequestException($exception->getMessage(), $exception->getRequest(), $exception);
}
return new HttplugException\TransferException($exception->getMessage(), 0, $exception);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Prestashop\ModuleLibGuzzleAdapter\Interfaces;
/**
* Every HTTP client related exception MUST implement this interface.
*/
interface ClientExceptionInterface extends \Throwable
{
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Prestashop\ModuleLibGuzzleAdapter\Interfaces;
interface ConfigInterface
{
/**
* When a client is created with the config of another version,
* this method makes sure the keys match.
*
* @param array<string, mixed> $config
*
* @return array<string, mixed>
*/
public static function fixConfig(array $config);
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Prestashop\ModuleLibGuzzleAdapter\Interfaces;
use Psr\Http\Message\RequestInterface;
/**
* HTTP Client Interface
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Victor Bocharsky <victor@symfonycasts.com>
*
* @see https://github.com/php-fig/http-client/blob/master/src/ClientInterface.php
*/
interface HttpClientInterface
{
/**
* Sends a PSR-7 request and returns a PSR-7 response.
*
* @param \Psr\Http\Message\RequestInterface $request
*
* @return \Psr\Http\Message\ResponseInterface
*
* @throws ClientExceptionInterface If an error happens while processing the request
*/
public function sendRequest(RequestInterface $request);
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Prestashop\ModuleLibGuzzleAdapter;
class VersionDetection
{
/**
* @return int|null
*/
public function getGuzzleMajorVersionNumber()
{
// Guzzle 7 and above
if (defined('\GuzzleHttp\ClientInterface::MAJOR_VERSION')) {
// @phpstan-ignore-next-line
return (int) \GuzzleHttp\ClientInterface::MAJOR_VERSION;
}
// Before Guzzle 7
if (defined('\GuzzleHttp\ClientInterface::VERSION')) {
// @phpstan-ignore-next-line
return (int) \GuzzleHttp\ClientInterface::VERSION[0];
}
return null;
}
}