first commit

This commit is contained in:
2024-11-05 12:22:50 +01:00
commit e5682a3912
19641 changed files with 2948548 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
<?php
$finder = PhpCsFixer\Finder::create()->in([
__DIR__.'/src',
__DIR__.'/tests',
]);
return PhpCsFixer\Config::create()
->setRiskyAllowed(true)
->setRules([
'@Symfony' => true,
'concat_space' => [
'spacing' => 'one',
],
'cast_spaces' => [
'space' => 'single',
],
'error_suppression' => [
'mute_deprecation_error' => false,
'noise_remaining_usages' => false,
'noise_remaining_usages_exclude' => [],
],
'function_to_constant' => false,
'no_alias_functions' => false,
'non_printable_character' => false,
'phpdoc_summary' => false,
'phpdoc_align' => [
'align' => 'left',
],
'protected_to_private' => false,
'psr4' => false,
'self_accessor' => false,
'yoda_style' => null,
'non_printable_character' => true,
'phpdoc_no_empty_return' => false,
])
->setFinder($finder)
->setCacheFile(__DIR__.'/.php_cs.cache')
;

View File

@@ -0,0 +1,30 @@
language: php
notifications:
email:
on_success: never
php:
- 7.2
- 7.3
- 5.6
matrix:
fast_finish: true
allow_failures:
- php: 7.3
cache:
directories:
- $HOME/.composer/cache
before_script:
- composer install -n
script:
- composer test -- --coverage-clover=coverage.xml
- composer phpcs
- composer psalm
after_success:
- bash <(curl -s https://codecov.io/bash)

View File

@@ -0,0 +1,19 @@
Copyright (c) 2018-present PrestaShop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,157 @@
# Circuit Breaker, an implementation for resilient PHP applications
[![codecov](https://codecov.io/gh/PrestaShop/circuit-breaker/branch/master/graph/badge.svg)](https://codecov.io/gh/PrestaShop/circuit-breaker) [![PHPStan](https://img.shields.io/badge/PHPStan-Level%207-brightgreen.svg?style=flat&logo=php)](https://shields.io/#/) [![Psalm](https://img.shields.io/badge/Psalm-Level%20Max-brightgreen.svg?style=flat&logo=php)](https://shields.io/#/) [![Build Status](https://travis-ci.com/PrestaShop/circuit-breaker.svg?branch=master)](https://travis-ci.com/PrestaShop/circuit-breaker)
## Main principles
![circuit breaker](https://user-images.githubusercontent.com/1247388/49721725-438bd700-fc63-11e8-8498-82ca681b15fb.png)
This library is compatible with PHP 5.6+.
## Installation
```
composer require prestashop/circuit-breaker
```
## Use
### Simple Circuit Breaker
You can use the factory to create a simple circuit breaker.
By default, you need to define 3 parameters for the circuit breaker:
* the **failures**: define how many times we try to access the service;
* the **timeout**: define how much time we wait before consider the service unreachable;
* the **threshold**: define how much time we wait before trying to access again the service (once it is considered unreachable);
The **fallback** callback will be used if the distant service is unreachable when the Circuit Breaker is Open (means "is used" if the service is unreachable).
> You'd better return the same type of response expected from your distant call.
```php
use PrestaShop\CircuitBreaker\SimpleCircuitBreakerFactory;
use PrestaShop\CircuitBreaker\FactorySettings;
$circuitBreakerFactory = new SimpleCircuitBreakerFactory();
$circuitBreaker = $circuitBreakerFactory->create(new FactorySettings(2, 0.1, 10));
$fallbackResponse = function () {
return '{}';
};
$response = $circuitBreaker->call('https://api.domain.com', [], $fallbackResponse);
```
If you don't specify any fallback, by default the circuit breaker will return an empty string.
```php
use PrestaShop\CircuitBreaker\SimpleCircuitBreakerFactory;
use PrestaShop\CircuitBreaker\FactorySettings;
$circuitBreakerFactory = new SimpleCircuitBreakerFactory();
$circuitBreaker = $circuitBreakerFactory->create(new FactorySettings(2, 0.1, 10));
$response = $circuitBreaker->call('https://unreacheable.api.domain.com', []); // $response == ''
```
You can also define the client options (or even set your own client if you prefer).
```php
use PrestaShop\CircuitBreaker\SimpleCircuitBreakerFactory;
use PrestaShop\CircuitBreaker\FactorySettings;
$circuitBreakerFactory = new SimpleCircuitBreakerFactory();
$settings = new FactorySettings(2, 0.1, 10);
$settings->setClientOptions(['method' => 'POST']);
$circuitBreaker = $circuitBreakerFactory->create($settings);
$response = $circuitBreaker->call('https://api.domain.com/create/user', ['body' => ['firstname' => 'John', 'lastname' => 'Doe']]);
```
> For the Guzzle implementation, the Client options are described
> in the [HttpGuzzle documentation](http://docs.guzzlephp.org/en/stable/index.html).
### Advanced Circuit Breaker
If you need more control on your circuit breaker, you should use the `AdvancedCircuitBreaker` which manages more features:
* the **stripped failures**: define how many times we try to access the service when the circuit breaker is Half Open (when it retires to reach the service after it was unreachable);
* the **stripped timeout**: define how much time we wait before consider the service unreachable (again in Half open state);
* the **storage**: used to store the circuit breaker states and transitions. By default it's an `SimpleArray` so if you want to "cache" the fact that your service is unreachable you should use a persistent storage;
* the **transition dispatcher**: used if you need to subscribe to transition events (ex: a dispatcher based on Symfony EventDispatcher is available)
#### Storage
```php
use Doctrine\Common\Cache\FilesystemCache;
use PrestaShop\CircuitBreaker\AdvancedCircuitBreakerFactory;
use PrestaShop\CircuitBreaker\FactorySettings;
use PrestaShop\CircuitBreaker\Storage\DoctrineCache;
$circuitBreakerFactory = new AdvancedCircuitBreakerFactory();
$settings = new FactorySettings(2, 0.1, 60); //60 seconds threshold
//Once the circuit breaker is open, the fallback response will be returned instantly during the next 60 seconds
//Since the state is persisted even other requests/processes will be aware that the circuit breaker is open
$doctrineCache = new FilesystemCache(_PS_CACHE_DIR_ . '/addons_category');
$storage = new DoctrineCache($doctrineCache);
$settings->setStorage($storage);
$circuitBreaker = $circuitBreakerFactory->create($settings);
$response = $circuitBreaker->call('https://unreachable.api.domain.com/create/user', []);
```
### Guzzle Cache
Besides caching the circuit breaker state, it may be interesting to cache the successful responses. The circuit breaker library doesn't manage the cache itself,
however you can use [Guzzle Subscriber](https://github.com/guzzle/cache-subscriber) to manage it.
```php
use Doctrine\Common\Cache\FilesystemCache;
use GuzzleHttp\Subscriber\Cache\CacheStorage;
use GuzzleHttp\Subscriber\Cache\CacheSubscriber;
use PrestaShop\CircuitBreaker\FactorySettings;
use PrestaShop\CircuitBreaker\SimpleCircuitBreakerFactory;
$circuitBreakerFactory = new SimpleCircuitBreakerFactory();
$settings = new FactorySettings(2, 0.1, 60); //60 seconds threshold
//Guzzle subsriber is also compatible with doctrine cache, which is why we proposed a storage based on it, this allows you to use for storage and cache
$doctrineCache = new FilesystemCache(_PS_CACHE_DIR_ . '/addons_category');
//By default the ttl is defined by the response headers but you can set a default one
$cacheStorage = new CacheStorage($doctrineCache, null, 60);
$cacheSubscriber = new CacheSubscriber($cacheStorage, function (Request $request) { return true; });
$settings->setClientOptions(['subscribers' => [$cacheSubscriber]]);
$circuitBreaker = $circuitBreakerFactory->create($settings);
$response = $circuitBreaker->call('https://api.domain.com/create/user', []);
```
## Tests
```
composer test
```
## Code quality
```
composer cs-fix && composer phpcb && composer psalm && composer phpcs
```
We also use [PHPQA](https://github.com/EdgedesignCZ/phpqa#phpqa) to check the Code quality
during the CI management of the contributions.
If you want to use it (using Docker):
```
docker run --rm -u $UID -v $(pwd):/app eko3alpha/docker-phpqa --report --ignoredDirs vendor,tests
```
If you want to use it (using Composer):
```
composer global require edgedesign/phpqa=v1.20.0 --update-no-dev
phpqa --report --ignoredDirs vendor,tests
```

View File

@@ -0,0 +1,66 @@
{
"name": "prestashop/circuit-breaker",
"description": "A circuit breaker implementation for PHP",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "PrestaShop SA",
"email": "contact@prestashop.com"
},
{
"name": "PrestaShop Community",
"homepage": "http://contributors.prestashop.com/"
}
],
"require": {
"php": ">=5.6",
"guzzlehttp/guzzle": "^5"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.12",
"phpunit/phpunit": "^5.7.0",
"doctrine/cache": "^1.6.0",
"symfony/cache": "^3.4.0",
"symfony/event-dispatcher": "^3.4",
"vimeo/psalm": "^1.1",
"squizlabs/php_codesniffer": "3.*"
},
"suggest": {
"symfony/cache": "Allows use of Symfony Cache adapters to store transactions",
"doctrine/cache": "Allows use of Doctrine Cache adapters to store transactions",
"ext-apcu": "Allows use of APCu adapter (performant) to store transactions",
"guzzlehttp/cache-subscriber": "Allow use of Guzzle cache (use dev-master for most recent changes)"
},
"autoload": {
"psr-4": {
"PrestaShop\\CircuitBreaker\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\PrestaShop\\CircuitBreaker\\": "tests/"
}
},
"scripts": {
"psalm": [
"@php ./vendor/bin/psalm --init",
"@php ./vendor/bin/psalm --find-dead-code --threads=8 --diff"
],
"phpcs": "@php ./vendor/bin/phpcs -p --standard=PSR2 --ignore=\"vendor/*\" \"./\" --extensions=php",
"phpcbf": "@php ./vendor/bin/phpcbf -p --standard=PSR2 --ignore=\"vendor/*\" \"./\" --extensions=php",
"cs-fix": "@php ./vendor/bin/php-cs-fixer fix",
"test": "@php ./vendor/bin/phpunit"
},
"scripts-descriptions": {
"psalm": "Execute Psalm on PHP7.0+, you need to install it",
"cs-fix": "Check and fix coding styles using PHP CS Fixer",
"test": "Launch PHPUnit test suite"
},
"config": {
"sort-packages": true,
"platform": {
"php": "5.6"
}
}
}

View File

@@ -0,0 +1,6 @@
parameters:
ignoreErrors:
# Can't use the PHPUnit extension on PHP 5.6, ignore Mocks false positive
- '#Access to an undefined property PHPUnit_Framework_MockObject_Invocation#'
- '#should return PHPUnit_Framework_MockObject_MockObject#'
- '#PHPUnit_Framework_MockObject_MockObject given#'

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<phpunit colors="true" bootstrap="./vendor/autoload.php">
<testsuites>
<testsuite name="PrestaShop Circuit Breaker Test suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,55 @@
<?xml version="1.0"?>
<psalm
name="Psalm configuration for the PrestaShop Circuit Breaker"
totallyTyped="false"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config file:///home/dev/Projects/circuit-breaker/vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
<directory name="tests" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<LessSpecificReturnType errorLevel="info" />
<!-- level 3 issues - slightly lazy code writing, but provably low false-negatives -->
<DeprecatedMethod errorLevel="info" />
<DeprecatedProperty errorLevel="info" />
<DeprecatedClass errorLevel="info" />
<DeprecatedConstant errorLevel="info" />
<DeprecatedInterface errorLevel="info" />
<DeprecatedTrait errorLevel="info" />
<InternalMethod errorLevel="info" />
<InternalProperty errorLevel="info" />
<InternalClass errorLevel="info" />
<MissingClosureReturnType errorLevel="info" />
<MissingReturnType errorLevel="info" />
<MissingPropertyType errorLevel="info" />
<InvalidDocblock errorLevel="info" />
<MisplacedRequiredParam errorLevel="info" />
<PropertyNotSetInConstructor errorLevel="info" />
<MissingConstructor errorLevel="info" />
<MissingClosureParamType errorLevel="info" />
<MissingParamType errorLevel="info" />
<RedundantCondition errorLevel="info" />
<DocblockTypeContradiction errorLevel="info" />
<RedundantConditionGivenDocblockType errorLevel="info" />
<UnresolvableInclude errorLevel="info" />
<RawObjectIteration errorLevel="info" />
<InvalidStringClass errorLevel="info" />
</issueHandlers>
</psalm>

View File

@@ -0,0 +1,176 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\SystemInterface;
use PrestaShop\CircuitBreaker\Contract\TransitionDispatcherInterface;
use PrestaShop\CircuitBreaker\Exception\UnavailableServiceException;
/**
* This implementation of the CircuitBreaker is a bit more advanced than the SimpleCircuitBreaker,
* it allows you to setup your client, system, storage and dispatcher.
*/
class AdvancedCircuitBreaker extends PartialCircuitBreaker
{
/** @var TransitionDispatcherInterface */
protected $dispatcher;
/** @var callable|null */
protected $defaultFallback;
/**
* @param SystemInterface $system
* @param ClientInterface $client
* @param StorageInterface $storage
* @param TransitionDispatcherInterface $dispatcher
*/
public function __construct(
SystemInterface $system,
ClientInterface $client,
StorageInterface $storage,
TransitionDispatcherInterface $dispatcher
) {
parent::__construct($system, $client, $storage);
$this->dispatcher = $dispatcher;
}
/**
* {@inheritdoc}
*/
public function call(
$service,
array $serviceParameters = [],
callable $fallback = null
) {
$transaction = $this->initTransaction($service);
try {
if ($this->isOpened()) {
if (!$this->canAccessService($transaction)) {
return $this->callFallback($fallback);
}
$this->moveStateTo(State::HALF_OPEN_STATE, $service);
$this->dispatchTransition(
Transition::CHECKING_AVAILABILITY_TRANSITION,
$service,
$serviceParameters
);
}
$response = $this->request($service, $serviceParameters);
$this->moveStateTo(State::CLOSED_STATE, $service);
$this->dispatchTransition(
Transition::CLOSING_TRANSITION,
$service,
$serviceParameters
);
return $response;
} catch (UnavailableServiceException $exception) {
$transaction->incrementFailures();
$this->storage->saveTransaction($service, $transaction);
if (!$this->isAllowedToRetry($transaction)) {
$this->moveStateTo(State::OPEN_STATE, $service);
$transition = $this->isHalfOpened() ? Transition::REOPENING_TRANSITION : Transition::OPENING_TRANSITION;
$this->dispatchTransition($transition, $service, $serviceParameters);
return $this->callFallback($fallback);
}
return $this->call(
$service,
$serviceParameters,
$fallback
);
}
}
/**
* @return callable|null
*/
public function getDefaultFallback()
{
return $this->defaultFallback;
}
/**
* @param callable $defaultFallback|null
*
* @return AdvancedCircuitBreaker
*/
public function setDefaultFallback(callable $defaultFallback = null)
{
$this->defaultFallback = $defaultFallback;
return $this;
}
/**
* {@inheritdoc}
*/
protected function callFallback(callable $fallback = null)
{
return parent::callFallback(null !== $fallback ? $fallback : $this->defaultFallback);
}
/**
* @param string $transition
* @param string $service
* @param array $serviceParameters
*
* @return void
*/
protected function dispatchTransition($transition, $service, array $serviceParameters)
{
$this->dispatcher->dispatchTransition($transition, $service, $serviceParameters);
}
/**
* {@inheritdoc}
*/
protected function initTransaction($service)
{
if (!$this->storage->hasTransaction($service)) {
$this->dispatchTransition(Transition::INITIATING_TRANSITION, $service, []);
}
return parent::initTransaction($service);
}
/**
* {@inheritdoc}
*/
protected function request($service, array $parameters = [])
{
$this->dispatchTransition(Transition::TRIAL_TRANSITION, $service, $parameters);
return parent::request($service, $parameters);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\Contract\FactoryInterface;
use PrestaShop\CircuitBreaker\Contract\FactorySettingsInterface;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\TransitionDispatcherInterface;
use PrestaShop\CircuitBreaker\Place\ClosedPlace;
use PrestaShop\CircuitBreaker\Place\HalfOpenPlace;
use PrestaShop\CircuitBreaker\Place\OpenPlace;
use PrestaShop\CircuitBreaker\Client\GuzzleClient;
use PrestaShop\CircuitBreaker\Storage\SimpleArray;
use PrestaShop\CircuitBreaker\System\MainSystem;
use PrestaShop\CircuitBreaker\Transition\NullDispatcher;
/**
* Advanced implementation of Circuit Breaker Factory
* Used to create an AdvancedCircuitBreaker instance.
*/
final class AdvancedCircuitBreakerFactory implements FactoryInterface
{
/**
* {@inheritdoc}
*/
public function create(FactorySettingsInterface $settings)
{
$closedPlace = new ClosedPlace($settings->getFailures(), $settings->getTimeout(), 0);
$openPlace = new OpenPlace(0, 0, $settings->getThreshold());
$halfOpenPlace = new HalfOpenPlace($settings->getFailures(), $settings->getStrippedTimeout(), 0);
$system = new MainSystem($closedPlace, $halfOpenPlace, $openPlace);
/** @var ClientInterface $client */
$client = $settings->getClient() ?: new GuzzleClient($settings->getClientOptions());
/** @var StorageInterface $storage */
$storage = $settings->getStorage() ?: new SimpleArray();
/** @var TransitionDispatcherInterface $dispatcher */
$dispatcher = $settings->getDispatcher() ?: new NullDispatcher();
$circuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$storage,
$dispatcher
);
if (null !== $settings->getDefaultFallback()) {
$circuitBreaker->setDefaultFallback($settings->getDefaultFallback());
}
return $circuitBreaker;
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace PrestaShop\CircuitBreaker\Client;
use Exception;
use GuzzleHttp\Client as OriginalGuzzleClient;
use GuzzleHttp\Subscriber\Mock;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\Exception\UnavailableServiceException;
use PrestaShop\CircuitBreaker\Exception\UnsupportedMethodException;
/**
* Guzzle implementation of client.
* The possibility of extending this client is intended.
*/
class GuzzleClient implements ClientInterface
{
/**
* @var string by default, calls are sent using GET method
*/
const DEFAULT_METHOD = 'GET';
/**
* Supported HTTP methods
*/
const SUPPORTED_METHODS = [
'GET' => true,
'HEAD' => true,
'POST' => true,
'PUT' => true,
'DELETE' => true,
'OPTIONS' => true,
];
/**
* @var array the Client default options
*/
private $defaultOptions;
public function __construct(array $defaultOptions = [])
{
$this->defaultOptions = $defaultOptions;
}
/**
* {@inheritdoc}
*
* @throws UnavailableServiceException
*/
public function request($resource, array $options)
{
try {
$options = array_merge($this->defaultOptions, $options);
$client = $this->buildClient($options);
$method = $this->getHttpMethod($options);
$options['exceptions'] = true;
// prevents unhandled method errors in Guzzle 5
unset($options['method'], $options['mock']);
$request = $client->createRequest($method, $resource, $options);
return (string) $client->send($request)->getBody();
} catch (Exception $e) {
throw new UnavailableServiceException($e->getMessage(), (int) $e->getCode(), $e);
}
}
/**
* @param array $options the list of options
*
* @return string the method
*
* @throws UnsupportedMethodException
*/
private function getHttpMethod(array $options)
{
if (isset($options['method'])) {
if (!array_key_exists($options['method'], self::SUPPORTED_METHODS)) {
throw UnsupportedMethodException::unsupportedMethod($options['method']);
}
return $options['method'];
}
return self::DEFAULT_METHOD;
}
/**
* @param array $options
*
* @return OriginalGuzzleClient
*/
private function buildClient(array $options)
{
if (isset($options['mock']) && $options['mock'] instanceof Mock) {
return $this->buildMockClient($options);
}
return new OriginalGuzzleClient($options);
}
/**
* Builds a client with a mock
*
* @param array $options
*
* @return OriginalGuzzleClient
*/
private function buildMockClient(array $options)
{
/** @var Mock $mock */
$mock = $options['mock'];
unset($options['mock']);
$client = new OriginalGuzzleClient($options);
$client->getEmitter()->attach($mock);
return $client;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace PrestaShop\CircuitBreaker\Contract;
/**
* A circuit breaker is used to provide
* an alternative response when a tiers service
* is unreachable.
*/
interface CircuitBreakerInterface
{
/**
* @return string the circuit breaker state
*/
public function getState();
/**
* The function that execute the service.
*
* @param string $service the service to call
* @param array $parameters the parameters for the request
* @param callable|null $fallback if the service is unavailable, rely on the fallback
*
* @return string
*/
public function call($service, array $parameters = [], callable $fallback = null);
/**
* @return bool checks if the circuit breaker is open
*/
public function isOpened();
/**
* @return bool checks if the circuit breaker is half open
*/
public function isHalfOpened();
/**
* @return bool checks if the circuit breaker is closed
*/
public function isClosed();
}

View File

@@ -0,0 +1,18 @@
<?php
namespace PrestaShop\CircuitBreaker\Contract;
/**
* In charge of calling the resource and return a response.
* Must throw UnavailableService exception if not reachable.
*/
interface ClientInterface
{
/**
* @param string $resource the URI of the service to be reached
* @param array $options the options if needed
*
* @return string
*/
public function request($resource, array $options);
}

View File

@@ -0,0 +1,16 @@
<?php
namespace PrestaShop\CircuitBreaker\Contract;
/**
* Ease the creation of the Circuit Breaker.
*/
interface FactoryInterface
{
/**
* @param FactorySettingsInterface $settings the settings for the Place
*
* @return CircuitBreakerInterface
*/
public function create(FactorySettingsInterface $settings);
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\CircuitBreaker\Contract;
/**
* Interface FactorySettingsInterface contains the settings used by the Factory
*/
interface FactorySettingsInterface
{
/**
* @param FactorySettingsInterface $settingsA
* @param FactorySettingsInterface $settingsB
*
* @return FactorySettingsInterface
*/
public static function merge(FactorySettingsInterface $settingsA, FactorySettingsInterface $settingsB);
/**
* @return int
*/
public function getFailures();
/**
* @return float
*/
public function getTimeout();
/**
* @return int
*/
public function getThreshold();
/**
* @return float
*/
public function getStrippedTimeout();
/**
* @return int
*/
public function getStrippedFailures();
/**
* @return StorageInterface|null
*/
public function getStorage();
/**
* @return TransitionDispatcherInterface|null
*/
public function getDispatcher();
/**
* @return array
*/
public function getClientOptions();
/**
* @return ClientInterface|null
*/
public function getClient();
/**
* @return callable|null
*/
public function getDefaultFallback();
}

View File

@@ -0,0 +1,33 @@
<?php
namespace PrestaShop\CircuitBreaker\Contract;
/**
* A circuit breaker can be in 3 places:
* closed, half open or open. Each place have its
* own properties and behaviors.
*/
interface PlaceInterface
{
/**
* Return the current state of the Circuit Breaker.
*
* @return string
*/
public function getState();
/**
* @return int the number of failures
*/
public function getFailures();
/**
* @return int the allowed number of trials
*/
public function getThreshold();
/**
* @return float the allowed timeout
*/
public function getTimeout();
}

View File

@@ -0,0 +1,49 @@
<?php
namespace PrestaShop\CircuitBreaker\Contract;
use PrestaShop\CircuitBreaker\Exception\TransactionNotFoundException;
/**
* Store the transaction between the Circuit Breaker
* and the tiers service.
*/
interface StorageInterface
{
/**
* Save the CircuitBreaker transaction.
*
* @param string $service The service name
* @param TransactionInterface $transaction the transaction
*
* @return bool
*/
public function saveTransaction($service, TransactionInterface $transaction);
/**
* Retrieve the CircuitBreaker transaction for a specific service.
*
* @param string $service the service name
*
* @return TransactionInterface
*
* @throws TransactionNotFoundException
*/
public function getTransaction($service);
/**
* Checks if the transaction exists.
*
* @param string $service the service name
*
* @return bool
*/
public function hasTransaction($service);
/**
* Clear the Circuit Breaker storage.
*
* @return bool
*/
public function clear();
}

View File

@@ -0,0 +1,20 @@
<?php
namespace PrestaShop\CircuitBreaker\Contract;
/**
* The System define the places available
* for the Circuit Breaker and the initial Place.
*/
interface SystemInterface
{
/**
* @return PlaceInterface[] the list of places of the system
*/
public function getPlaces();
/**
* @return PlaceInterface the initial place of the system
*/
public function getInitialPlace();
}

View File

@@ -0,0 +1,40 @@
<?php
namespace PrestaShop\CircuitBreaker\Contract;
use DateTime;
/**
* Once the circuit breaker call a service,
* a transaction is initialized and stored.
*/
interface TransactionInterface
{
/**
* @return string the service name
*/
public function getService();
/**
* @return int the number of failures to call the service
*/
public function getFailures();
/**
* @return string the current state of the Circuit Breaker
*/
public function getState();
/**
* @return DateTime the time when the circuit breaker move
* from open to half open state
*/
public function getThresholdDateTime();
/**
* Everytime the service call fails, increment the number of failures.
*
* @return bool
*/
public function incrementFailures();
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\CircuitBreaker\Contract;
/**
* This interface is used for a circuit breaker to dispatch its state transitions.
*/
interface TransitionDispatcherInterface
{
/**
* @param string $transition the transition name
* @param string $service the Service URI
* @param array $serviceParameters the Service parameters
*
* @return void
*/
public function dispatchTransition($transition, $service, array $serviceParameters);
}

View File

@@ -0,0 +1,59 @@
<?php
namespace PrestaShop\CircuitBreaker\Event;
use Symfony\Component\EventDispatcher\Event;
class TransitionEvent extends Event
{
/**
* @var string the Transition name
*/
private $eventName;
/**
* @var string the Service URI
*/
private $service;
/**
* @var array the Service parameters
*/
private $parameters;
/**
* @param string $eventName the transition name
* @param string $service the Service URI
* @param array $parameters the Service parameters
*/
public function __construct($eventName, $service, array $parameters)
{
$this->eventName = $eventName;
$this->service = $service;
$this->parameters = $parameters;
}
/**
* @return string the Transition name
*/
public function getEvent()
{
return $this->eventName;
}
/**
* @return string the Service URI
*/
public function getService()
{
return $this->service;
}
/**
* @return array the Service parameters
*/
public function getParameters()
{
return $this->parameters;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace PrestaShop\CircuitBreaker\Exception;
use Exception;
/**
* Base exception for Circuit Breaker exceptions
*/
class CircuitBreakerException extends Exception
{
}

View File

@@ -0,0 +1,26 @@
<?php
namespace PrestaShop\CircuitBreaker\Exception;
use PrestaShop\CircuitBreaker\Util\ErrorFormatter;
final class InvalidPlaceException extends CircuitBreakerException
{
/**
* @param mixed $failures the failures
* @param mixed $timeout the timeout
* @param mixed $threshold the threshold
*
* @return self
*/
public static function invalidSettings($failures, $timeout, $threshold)
{
$exceptionMessage = 'Invalid settings for Place' . PHP_EOL .
ErrorFormatter::format('failures', $failures, 'isPositiveInteger', 'a positive integer') .
ErrorFormatter::format('timeout', $timeout, 'isPositiveValue', 'a float') .
ErrorFormatter::format('threshold', $threshold, 'isPositiveInteger', 'a positive integer')
;
return new self($exceptionMessage);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace PrestaShop\CircuitBreaker\Exception;
use PrestaShop\CircuitBreaker\Util\ErrorFormatter;
final class InvalidTransactionException extends CircuitBreakerException
{
/**
* @param mixed $service the service URI
* @param mixed $failures the failures
* @param mixed $state the Circuit Breaker
* @param mixed $threshold the threshold
*
* @return self
*/
public static function invalidParameters($service, $failures, $state, $threshold)
{
$exceptionMessage = 'Invalid parameters for Transaction' . PHP_EOL .
ErrorFormatter::format('service', $service, 'isURI', 'an URI') .
ErrorFormatter::format('failures', $failures, 'isPositiveInteger', 'a positive integer') .
ErrorFormatter::format('state', $state, 'isString', 'a string') .
ErrorFormatter::format('threshold', $threshold, 'isPositiveInteger', 'a positive integer')
;
return new self($exceptionMessage);
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace PrestaShop\CircuitBreaker\Exception;
final class TransactionNotFoundException extends CircuitBreakerException
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace PrestaShop\CircuitBreaker\Exception;
final class UnavailableServiceException extends CircuitBreakerException
{
}

View File

@@ -0,0 +1,19 @@
<?php
namespace PrestaShop\CircuitBreaker\Exception;
/**
* Used when trying to use an unsupported HTTP method
*/
class UnsupportedMethodException extends CircuitBreakerException
{
/**
* @param string $methodName
*
* @return UnsupportedMethodException
*/
public static function unsupportedMethod($methodName)
{
return new static(sprintf('Unsupported method: "%s"', $methodName));
}
}

View File

@@ -0,0 +1,313 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\Contract\FactorySettingsInterface;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\TransitionDispatcherInterface;
/**
* Class FactorySettings is a simple implementation of FactorySettingsInterface, it is mainly
* a settings container and can be used with any Factory class.
*/
class FactorySettings implements FactorySettingsInterface
{
/** @var int */
private $failures;
/** @var float */
private $timeout;
/** @var int */
private $threshold;
/** @var float */
private $strippedTimeout;
/** @var int */
private $strippedFailures;
/** @var StorageInterface|null */
private $storage;
/** @var TransitionDispatcherInterface|null */
private $dispatcher;
/** @var array */
private $clientOptions = [];
/** @var ClientInterface|null */
private $client;
/** @var callable|null */
private $defaultFallback;
/**
* @param int $failures
* @param float $timeout
* @param int $threshold
*/
public function __construct(
$failures,
$timeout,
$threshold
) {
$this->failures = $this->strippedFailures = $failures;
$this->timeout = $this->strippedTimeout = $timeout;
$this->threshold = $threshold;
}
/**
* {@inheritdoc}
*/
public static function merge(FactorySettingsInterface $settingsA, FactorySettingsInterface $settingsB)
{
$mergedSettings = new FactorySettings(
$settingsB->getFailures(),
$settingsB->getTimeout(),
$settingsB->getThreshold()
);
$mergedSettings
->setStrippedFailures($settingsB->getStrippedFailures())
->setStrippedTimeout($settingsB->getStrippedTimeout())
;
$mergedSettings->setClientOptions(array_merge(
$settingsA->getClientOptions(),
$settingsB->getClientOptions()
));
if (null !== $settingsB->getClient()) {
$mergedSettings->setClient($settingsB->getClient());
} elseif (null !== $settingsA->getClient()) {
$mergedSettings->setClient($settingsA->getClient());
}
return $mergedSettings;
}
/**
* {@inheritdoc}
*/
public function getFailures()
{
return $this->failures;
}
/**
* @param int $failures
*
* @return FactorySettings
*/
public function setFailures($failures)
{
$this->failures = $failures;
return $this;
}
/**
* {@inheritdoc}
*/
public function getTimeout()
{
return $this->timeout;
}
/**
* @param float $timeout
*
* @return FactorySettings
*/
public function setTimeout($timeout)
{
$this->timeout = $timeout;
return $this;
}
/**
* {@inheritdoc}
*/
public function getThreshold()
{
return $this->threshold;
}
/**
* @param int $threshold
*
* @return FactorySettings
*/
public function setThreshold($threshold)
{
$this->threshold = $threshold;
return $this;
}
/**
* {@inheritdoc}
*/
public function getStrippedTimeout()
{
return $this->strippedTimeout;
}
/**
* @param float $strippedTimeout
*
* @return FactorySettings
*/
public function setStrippedTimeout($strippedTimeout)
{
$this->strippedTimeout = $strippedTimeout;
return $this;
}
/**
* @return int
*/
public function getStrippedFailures()
{
return $this->strippedFailures;
}
/**
* @param int $strippedFailures
*
* @return FactorySettings
*/
public function setStrippedFailures($strippedFailures)
{
$this->strippedFailures = $strippedFailures;
return $this;
}
/**
* {@inheritdoc}
*/
public function getStorage()
{
return $this->storage;
}
/**
* @param StorageInterface $storage
*
* @return FactorySettings
*/
public function setStorage(StorageInterface $storage)
{
$this->storage = $storage;
return $this;
}
/**
* {@inheritdoc}
*/
public function getDispatcher()
{
return $this->dispatcher;
}
/**
* @param TransitionDispatcherInterface $dispatcher
*
* @return FactorySettings
*/
public function setDispatcher(TransitionDispatcherInterface $dispatcher)
{
$this->dispatcher = $dispatcher;
return $this;
}
/**
* {@inheritdoc}
*/
public function getClientOptions()
{
return $this->clientOptions;
}
/**
* @param array $clientOptions
*
* @return FactorySettings
*/
public function setClientOptions(array $clientOptions)
{
$this->clientOptions = $clientOptions;
return $this;
}
/**
* {@inheritdoc}
*/
public function getClient()
{
return $this->client;
}
/**
* @param ClientInterface|null $client
*
* @return FactorySettings
*/
public function setClient(ClientInterface $client = null)
{
$this->client = $client;
return $this;
}
/**
* {@inheritdoc}
*/
public function getDefaultFallback()
{
return $this->defaultFallback;
}
/**
* @param callable $defaultFallback
*
* @return FactorySettings
*/
public function setDefaultFallback(callable $defaultFallback)
{
$this->defaultFallback = $defaultFallback;
return $this;
}
}

View File

@@ -0,0 +1,183 @@
<?php
namespace PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\Transaction\SimpleTransaction;
use PrestaShop\CircuitBreaker\Contract\CircuitBreakerInterface;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\SystemInterface;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\Contract\PlaceInterface;
use DateTime;
abstract class PartialCircuitBreaker implements CircuitBreakerInterface
{
/**
* @param SystemInterface $system
* @param ClientInterface $client
* @param StorageInterface $storage
*/
public function __construct(
SystemInterface $system,
ClientInterface $client,
StorageInterface $storage
) {
$this->currentPlace = $system->getInitialPlace();
$this->places = $system->getPlaces();
$this->client = $client;
$this->storage = $storage;
}
/**
* @var ClientInterface the Client that consumes the service URI
*/
protected $client;
/**
* @var PlaceInterface the current Place of the Circuit Breaker
*/
protected $currentPlace;
/**
* @var PlaceInterface[] the Circuit Breaker places
*/
protected $places = [];
/**
* @var StorageInterface the Circuit Breaker storage
*/
protected $storage;
/**
* {@inheritdoc}
*/
abstract public function call($service, array $serviceParameters = [], callable $fallback = null);
/**
* {@inheritdoc}
*/
public function getState()
{
return $this->currentPlace->getState();
}
/**
* {@inheritdoc}
*/
public function isOpened()
{
return State::OPEN_STATE === $this->currentPlace->getState();
}
/**
* {@inheritdoc}
*/
public function isHalfOpened()
{
return State::HALF_OPEN_STATE === $this->currentPlace->getState();
}
/**
* {@inheritdoc}
*/
public function isClosed()
{
return State::CLOSED_STATE === $this->currentPlace->getState();
}
/**
* @param callable|null $fallback
*
* @return string
*/
protected function callFallback(callable $fallback = null)
{
if (null === $fallback) {
return '';
}
return call_user_func($fallback);
}
/**
* @param string $state the Place state
* @param string $service the service URI
*
* @return bool
*/
protected function moveStateTo($state, $service)
{
$this->currentPlace = $this->places[$state];
$transaction = SimpleTransaction::createFromPlace(
$this->currentPlace,
$service
);
return $this->storage->saveTransaction($service, $transaction);
}
/**
* @param string $service the service URI
*
* @return TransactionInterface
*/
protected function initTransaction($service)
{
if ($this->storage->hasTransaction($service)) {
$transaction = $this->storage->getTransaction($service);
// CircuitBreaker needs to be in the same state as its last transaction
if ($this->getState() !== $transaction->getState()) {
$this->currentPlace = $this->places[$transaction->getState()];
}
} else {
$transaction = SimpleTransaction::createFromPlace(
$this->currentPlace,
$service
);
$this->storage->saveTransaction($service, $transaction);
}
return $transaction;
}
/**
* @param TransactionInterface $transaction the Transaction
*
* @return bool
*/
protected function isAllowedToRetry(TransactionInterface $transaction)
{
return $transaction->getFailures() < $this->currentPlace->getFailures();
}
/**
* @param TransactionInterface $transaction the Transaction
*
* @return bool
*/
protected function canAccessService(TransactionInterface $transaction)
{
return $transaction->getThresholdDateTime() < new DateTime();
}
/**
* Calls the client with the right information.
*
* @param string $service the service URI
* @param array $parameters the service URI parameters
*
* @return string
*/
protected function request($service, array $parameters = [])
{
return $this->client->request(
$service,
array_merge($parameters, [
'connect_timeout' => $this->currentPlace->getTimeout(),
'timeout' => $this->currentPlace->getTimeout(),
])
);
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace PrestaShop\CircuitBreaker\Place;
use PrestaShop\CircuitBreaker\Contract\PlaceInterface;
use PrestaShop\CircuitBreaker\Exception\InvalidPlaceException;
use PrestaShop\CircuitBreaker\Util\Assert;
abstract class AbstractPlace implements PlaceInterface
{
/**
* @var int the Place failures
*/
private $failures;
/**
* @var float the Place timeout
*/
private $timeout;
/**
* @var int the Place threshold
*/
private $threshold;
/**
* @param int $failures the Place failures
* @param float $timeout the Place timeout
* @param int $threshold the Place threshold
*
* @throws InvalidPlaceException
*/
public function __construct($failures, $timeout, $threshold)
{
$this->validate($failures, $timeout, $threshold);
$this->failures = $failures;
$this->timeout = $timeout;
$this->threshold = $threshold;
}
/**
* {@inheritdoc}
*/
abstract public function getState();
/**
* {@inheritdoc}
*/
public function getFailures()
{
return $this->failures;
}
/**
* {@inheritdoc}
*/
public function getTimeout()
{
return $this->timeout;
}
/**
* {@inheritdoc}
*/
public function getThreshold()
{
return $this->threshold;
}
/**
* Ensure the place is valid (PHP5 is permissive).
*
* @param int $failures the failures should be a positive value
* @param float $timeout the timeout should be a positive value
* @param int $threshold the threshold should be a positive value
*
* @return bool true if valid
*
* @throws InvalidPlaceException
*/
private function validate($failures, $timeout, $threshold)
{
$assertionsAreValid = Assert::isPositiveInteger($failures)
&& Assert::isPositiveValue($timeout)
&& Assert::isPositiveInteger($threshold)
;
if ($assertionsAreValid) {
return true;
}
throw InvalidPlaceException::invalidSettings($failures, $timeout, $threshold);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace PrestaShop\CircuitBreaker\Place;
use PrestaShop\CircuitBreaker\State;
final class ClosedPlace extends AbstractPlace
{
/**
* {@inheritdoc}
*/
public function getState()
{
return State::CLOSED_STATE;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace PrestaShop\CircuitBreaker\Place;
use PrestaShop\CircuitBreaker\State;
final class HalfOpenPlace extends AbstractPlace
{
/**
* {@inheritdoc}
*/
public function getState()
{
return State::HALF_OPEN_STATE;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace PrestaShop\CircuitBreaker\Place;
use PrestaShop\CircuitBreaker\State;
final class OpenPlace extends AbstractPlace
{
/**
* {@inheritdoc}
*/
public function getState()
{
return State::OPEN_STATE;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\Contract\PlaceInterface;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\System\MainSystem;
use PrestaShop\CircuitBreaker\Storage\SimpleArray;
use PrestaShop\CircuitBreaker\Exception\UnavailableServiceException;
/**
* Main implementation of Circuit Breaker.
*/
final class SimpleCircuitBreaker extends PartialCircuitBreaker
{
public function __construct(
PlaceInterface $openPlace,
PlaceInterface $halfOpenPlace,
PlaceInterface $closedPlace,
ClientInterface $client
) {
$system = new MainSystem($closedPlace, $halfOpenPlace, $openPlace);
parent::__construct($system, $client, new SimpleArray());
}
/**
* {@inheritdoc}
*/
public function call(
$service,
array $serviceParameters = [],
callable $fallback = null
) {
$transaction = $this->initTransaction($service);
try {
if ($this->isOpened()) {
if (!$this->canAccessService($transaction)) {
return $this->callFallback($fallback);
}
$this->moveStateTo(State::HALF_OPEN_STATE, $service);
}
$response = $this->request($service, $serviceParameters);
$this->moveStateTo(State::CLOSED_STATE, $service);
return $response;
} catch (UnavailableServiceException $exception) {
$transaction->incrementFailures();
$this->storage->saveTransaction($service, $transaction);
if (!$this->isAllowedToRetry($transaction)) {
$this->moveStateTo(State::OPEN_STATE, $service);
return $this->callFallback($fallback);
}
return $this->call($service, $serviceParameters, $fallback);
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\Contract\FactoryInterface;
use PrestaShop\CircuitBreaker\Contract\FactorySettingsInterface;
use PrestaShop\CircuitBreaker\Place\ClosedPlace;
use PrestaShop\CircuitBreaker\Place\HalfOpenPlace;
use PrestaShop\CircuitBreaker\Place\OpenPlace;
use PrestaShop\CircuitBreaker\Client\GuzzleClient;
/**
* Main implementation of Circuit Breaker Factory
* Used to create a SimpleCircuitBreaker instance.
*/
final class SimpleCircuitBreakerFactory implements FactoryInterface
{
/**
* {@inheritdoc}
*/
public function create(FactorySettingsInterface $settings)
{
$closedPlace = new ClosedPlace($settings->getFailures(), $settings->getTimeout(), 0);
$openPlace = new OpenPlace(0, 0, $settings->getThreshold());
$halfOpenPlace = new HalfOpenPlace($settings->getFailures(), $settings->getStrippedTimeout(), 0);
/** @var ClientInterface $client */
$client = $settings->getClient() ?: new GuzzleClient($settings->getClientOptions());
return new SimpleCircuitBreaker(
$openPlace,
$halfOpenPlace,
$closedPlace,
$client
);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace PrestaShop\CircuitBreaker;
/**
* Define the available states of the Circuit Breaker;.
*/
final class State
{
/**
* Once opened, a circuit breaker doesn't do any call
* to third-party services. Only the alternative call is done.
*/
const OPEN_STATE = 'OPEN';
/**
* After some conditions are valid, the circuit breaker
* try to access the third-party service. If the service is valid,
* the circuit breaker go to CLOSED state. If it's not, the circuit breaker
* go to OPEN state.
*/
const HALF_OPEN_STATE = 'HALF OPEN';
/**
* On the first call of the service, or if the service is valid
* the circuit breaker is in CLOSED state. This means that the callable
* to evaluate is done and not the alternative call.
*/
const CLOSED_STATE = 'CLOSED';
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\CircuitBreaker\Storage;
use Doctrine\Common\Cache\CacheProvider;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Exception\TransactionNotFoundException;
/**
* Implementation of Storage using the Doctrine Cache.
*/
class DoctrineCache implements StorageInterface
{
/** @var CacheProvider */
private $cacheProvider;
/**
* @param CacheProvider $cacheProvider
*/
public function __construct(CacheProvider $cacheProvider)
{
$this->cacheProvider = $cacheProvider;
}
/**
* {@inheritdoc}
*/
public function saveTransaction($service, TransactionInterface $transaction)
{
$key = $this->getKey($service);
return $this->cacheProvider->save($key, $transaction);
}
/**
* {@inheritdoc}
*/
public function getTransaction($service)
{
$key = $this->getKey($service);
if ($this->hasTransaction($service)) {
return $this->cacheProvider->fetch($key);
}
throw new TransactionNotFoundException();
}
/**
* {@inheritdoc}
*/
public function hasTransaction($service)
{
$key = $this->getKey($service);
return $this->cacheProvider->contains($key);
}
/**
* {@inheritdoc}
*/
public function clear()
{
return $this->cacheProvider->deleteAll();
}
/**
* Helper method to properly store the transaction.
*
* @param string $service the service URI
*
* @return string the transaction unique identifier
*/
private function getKey($service)
{
return md5($service);
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace PrestaShop\CircuitBreaker\Storage;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Exception\TransactionNotFoundException;
/**
* Very simple implementation of Storage using a simple PHP array.
*/
final class SimpleArray implements StorageInterface
{
/**
* @var array the circuit breaker transactions
*/
public static $transactions = [];
/**
* {@inheritdoc}
*/
public function saveTransaction($service, TransactionInterface $transaction)
{
$key = $this->getKey($service);
self::$transactions[$key] = $transaction;
return true;
}
/**
* {@inheritdoc}
*/
public function getTransaction($service)
{
$key = $this->getKey($service);
if ($this->hasTransaction($service)) {
return self::$transactions[$key];
}
throw new TransactionNotFoundException();
}
/**
* {@inheritdoc}
*/
public function hasTransaction($service)
{
$key = $this->getKey($service);
return array_key_exists($key, self::$transactions);
}
/**
* {@inheritdoc}
*/
public function clear()
{
self::$transactions = [];
return true;
}
/**
* Helper method to properly store the transaction.
*
* @param string $service the service URI
*
* @return string the transaction unique identifier
*/
private function getKey($service)
{
return md5($service);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace PrestaShop\CircuitBreaker\Storage;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Exception\TransactionNotFoundException;
use Psr\SimpleCache\CacheInterface;
/**
* Implementation of Storage using the Symfony Cache Component.
*/
final class SymfonyCache implements StorageInterface
{
/**
* @var CacheInterface the Symfony Cache
*/
private $symfonyCache;
public function __construct(CacheInterface $symfonyCache)
{
$this->symfonyCache = $symfonyCache;
}
/**
* {@inheritdoc}
*/
public function saveTransaction($service, TransactionInterface $transaction)
{
$key = $this->getKey($service);
return $this->symfonyCache->set($key, $transaction);
}
/**
* {@inheritdoc}
*/
public function getTransaction($service)
{
$key = $this->getKey($service);
if ($this->hasTransaction($service)) {
return $this->symfonyCache->get($key);
}
throw new TransactionNotFoundException();
}
/**
* {@inheritdoc}
*/
public function hasTransaction($service)
{
$key = $this->getKey($service);
return $this->symfonyCache->has($key);
}
/**
* {@inheritdoc}
*/
public function clear()
{
return $this->symfonyCache->clear();
}
/**
* Helper method to properly store the transaction.
*
* @param string $service the service URI
*
* @return string the transaction unique identifier
*/
private function getKey($service)
{
return md5($service);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\Contract\SystemInterface;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Transition\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Symfony implementation of Circuit Breaker.
*/
final class SymfonyCircuitBreaker extends AdvancedCircuitBreaker
{
/**
* @var EventDispatcherInterface the Symfony Event Dispatcher
*/
private $eventDispatcher;
/**
* @param SystemInterface $system
* @param ClientInterface $client
* @param StorageInterface $storage
* @param EventDispatcherInterface $eventDispatcher
*/
public function __construct(
SystemInterface $system,
ClientInterface $client,
StorageInterface $storage,
EventDispatcherInterface $eventDispatcher
) {
$this->eventDispatcher = $eventDispatcher;
parent::__construct($system, $client, $storage, new EventDispatcher($eventDispatcher));
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace PrestaShop\CircuitBreaker\System;
use PrestaShop\CircuitBreaker\Contract\PlaceInterface;
use PrestaShop\CircuitBreaker\Contract\SystemInterface;
use PrestaShop\CircuitBreaker\State;
/**
* Implement the system described by the documentation.
* The main system is built with 3 places:
* - A Closed place
* - A Half Open Place
* - An Open Place
*/
final class MainSystem implements SystemInterface
{
/**
* @var PlaceInterface[]
*/
private $places;
public function __construct(
PlaceInterface $closedPlace,
PlaceInterface $halfOpenPlace,
PlaceInterface $openPlace
) {
$this->places = [
$closedPlace->getState() => $closedPlace,
$halfOpenPlace->getState() => $halfOpenPlace,
$openPlace->getState() => $openPlace,
];
}
/**
* {@inheritdoc}
*/
public function getInitialPlace()
{
return $this->places[State::CLOSED_STATE];
}
/**
* {@inheritdoc}
*/
public function getPlaces()
{
return $this->places;
}
}

View File

@@ -0,0 +1,155 @@
<?php
namespace PrestaShop\CircuitBreaker\Transaction;
use DateTime;
use PrestaShop\CircuitBreaker\Contract\PlaceInterface;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Exception\InvalidTransactionException;
use PrestaShop\CircuitBreaker\Util\Assert;
/**
* Main implementation of Circuit Breaker transaction.
*/
final class SimpleTransaction implements TransactionInterface
{
/**
* @var string the URI of the service
*/
private $service;
/**
* @var int the failures when we call the service
*/
private $failures;
/**
* @var string the Circuit Breaker state
*/
private $state;
/**
* @var DateTime the Transaction threshold datetime
*/
private $thresholdDateTime;
/**
* @param string $service the service URI
* @param int $failures the allowed failures
* @param string $state the circuit breaker state/place
* @param int $threshold the place threshold
*/
public function __construct($service, $failures, $state, $threshold)
{
$this->validate($service, $failures, $state, $threshold);
$this->service = $service;
$this->failures = $failures;
$this->state = $state;
$this->initThresholdDateTime($threshold);
}
/**
* {@inheritdoc}
*/
public function getService()
{
return $this->service;
}
/**
* {@inheritdoc}
*/
public function getFailures()
{
return $this->failures;
}
/**
* {@inheritdoc}
*/
public function getState()
{
return $this->state;
}
/**
* {@inheritdoc}
*/
public function getThresholdDateTime()
{
return $this->thresholdDateTime;
}
/**
* {@inheritdoc}
*/
public function incrementFailures()
{
++$this->failures;
return true;
}
/**
* Helper to create a transaction from the Place.
*
* @param PlaceInterface $place the Circuit Breaker place
* @param string $service the service URI
*
* @return self
*/
public static function createFromPlace(PlaceInterface $place, $service)
{
$threshold = $place->getThreshold();
return new self(
$service,
0,
$place->getState(),
$threshold
);
}
/**
* Set the right DateTime from the threshold value.
*
* @param int $threshold the Transaction threshold
*
* @return void
*/
private function initThresholdDateTime($threshold)
{
$thresholdDateTime = new DateTime();
$thresholdDateTime->modify("+$threshold second");
$this->thresholdDateTime = $thresholdDateTime;
}
/**
* Ensure the transaction is valid (PHP5 is permissive).
*
* @param string $service the service URI
* @param int $failures the failures should be a positive value
* @param string $state the Circuit Breaker state
* @param int $threshold the threshold should be a positive value
*
* @return bool true if valid
*
* @throws InvalidTransactionException
*/
private function validate($service, $failures, $state, $threshold)
{
$assertionsAreValid = Assert::isURI($service)
&& Assert::isPositiveInteger($failures)
&& Assert::isString($state)
&& Assert::isPositiveInteger($threshold)
;
if ($assertionsAreValid) {
return true;
}
throw InvalidTransactionException::invalidParameters($service, $failures, $state, $threshold);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace PrestaShop\CircuitBreaker;
/**
* Define the available transitions of the Circuit Breaker;.
*/
final class Transition
{
/**
* Happened only once when calling the Circuit Breaker.
*/
const INITIATING_TRANSITION = 'INITIATING';
/**
* Happened when we open the Circuit Breaker.
* This means once the Circuit Breaker is in failure.
*/
const OPENING_TRANSITION = 'OPENING';
/**
* Happened once the conditions of retry are met
* in OPEN state to move to HALF_OPEN state in the
* Circuit Breaker.
*/
const CHECKING_AVAILABILITY_TRANSITION = 'CHECKING AVAILABILITY';
/**
* Happened when we come back to OPEN state
* in the Circuit Breaker from the HALF_OPEN state.
*/
const REOPENING_TRANSITION = 'REOPENING';
/**
* Happened if the service is available again.
*/
const CLOSING_TRANSITION = 'CLOSING';
/**
* Happened on each try to call the service.
*/
const TRIAL_TRANSITION = 'TRIAL';
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\CircuitBreaker\Transition;
use PrestaShop\CircuitBreaker\Contract\TransitionDispatcherInterface;
use PrestaShop\CircuitBreaker\Event\TransitionEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Class EventDispatcher implements the TransitionDispatcher using the Symfony EventDispatcherInterface
*/
class EventDispatcher implements TransitionDispatcherInterface
{
/**
* @var EventDispatcherInterface the Symfony Event Dispatcher
*/
private $eventDispatcher;
public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
/**
* {@inheritdoc}
*/
public function dispatchTransition($transition, $service, array $serviceParameters)
{
$event = new TransitionEvent($transition, $service, $serviceParameters);
$this->eventDispatcher->dispatch($transition, $event);
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\CircuitBreaker\Transition;
use PrestaShop\CircuitBreaker\Contract\TransitionDispatcherInterface;
/**
* Class NullDispatcher is used when you have no TransitionDispatcher to inject
* because you don't need it.
*/
class NullDispatcher implements TransitionDispatcherInterface
{
/**
* {@inheritdoc}
*/
public function dispatchTransition($transition, $service, array $serviceParameters)
{
// Simply does nothing
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace PrestaShop\CircuitBreaker\Util;
/**
* Util class to handle object validation
* Should be deprecated for most parts once
* the library will drop PHP5 support.
*/
final class Assert
{
/**
* @param mixed $value the value to evaluate
*
* @return bool
*/
public static function isPositiveValue($value)
{
return !is_string($value) && is_numeric($value) && $value >= 0;
}
/**
* @param mixed $value the value to evaluate
*
* @return bool
*/
public static function isPositiveInteger($value)
{
return self::isPositiveValue($value) && is_int($value);
}
/**
* @param mixed $value the value to evaluate
*
* @return bool
*/
public static function isURI($value)
{
return null !== $value
&& !is_numeric($value)
&& !is_bool($value)
&& false !== filter_var($value, FILTER_SANITIZE_URL)
;
}
/**
* @param mixed $value the value to evaluate
*
* @return bool
*/
public static function isString($value)
{
return !empty($value) && is_string($value);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace PrestaShop\CircuitBreaker\Util;
/**
* Helper to provide complete and easy to read
* error messages.
* Mostly used to build Exception messages.
*/
final class ErrorFormatter
{
/**
* Format error message.
*
* @param string $parameter the parameter to evaluate
* @param mixed $value the value to format
* @param string $function the validation function
* @param string $expectedType the expected type
*
* @return string
*/
public static function format($parameter, $value, $function, $expectedType)
{
$errorMessage = '';
$isValid = Assert::$function($value);
$type = gettype($value);
$hasStringValue = in_array($type, ['integer', 'float', 'string'], true);
if (!$isValid) {
$errorMessage = sprintf(
'Excepted %s to be %s, got %s',
$parameter,
$expectedType,
$type
);
if ($hasStringValue) {
$errorMessage .= sprintf(' (%s)', (string) $value);
}
$errorMessage .= PHP_EOL;
}
return $errorMessage;
}
}

View File

@@ -0,0 +1,150 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace Tests\PrestaShop\CircuitBreaker;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\AdvancedCircuitBreaker;
use PrestaShop\CircuitBreaker\AdvancedCircuitBreakerFactory;
use PrestaShop\CircuitBreaker\Client\GuzzleClient;
use PrestaShop\CircuitBreaker\Contract\FactorySettingsInterface;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\TransitionDispatcherInterface;
use PrestaShop\CircuitBreaker\FactorySettings;
use PrestaShop\CircuitBreaker\State;
use PrestaShop\CircuitBreaker\Transition;
class AdvancedCircuitBreakerFactoryTest extends TestCase
{
/**
* @dataProvider getSettings
*
* @param FactorySettingsInterface $settings the Circuit Breaker settings
*
* @return void
*/
public function testCircuitBreakerCreation(FactorySettingsInterface $settings)
{
$factory = new AdvancedCircuitBreakerFactory();
$circuitBreaker = $factory->create($settings);
$this->assertInstanceOf(AdvancedCircuitBreaker::class, $circuitBreaker);
}
public function testCircuitBreakerWithDispatcher()
{
$dispatcher = $this->getMockBuilder(TransitionDispatcherInterface::class)
->disableOriginalConstructor()
->getMock()
;
$localeService = 'file://' . __FILE__;
$expectedParameters = ['toto' => 'titi', 42 => 51];
$dispatcher
->expects($this->at(0))
->method('dispatchTransition')
->with(
$this->equalTo(Transition::INITIATING_TRANSITION),
$this->equalTo($localeService),
$this->equalTo([])
)
;
$dispatcher
->expects($this->at(1))
->method('dispatchTransition')
->with(
$this->equalTo(Transition::TRIAL_TRANSITION),
$this->equalTo($localeService),
$this->equalTo($expectedParameters)
)
;
$factory = new AdvancedCircuitBreakerFactory();
$settings = new FactorySettings(2, 0.1, 10);
$settings
->setStrippedTimeout(0.2)
->setDispatcher($dispatcher)
;
$circuitBreaker = $factory->create($settings);
$this->assertInstanceOf(AdvancedCircuitBreaker::class, $circuitBreaker);
$circuitBreaker->call($localeService, $expectedParameters, function () {
});
}
public function testCircuitBreakerWithStorage()
{
$storage = $this->getMockBuilder(StorageInterface::class)
->disableOriginalConstructor()
->getMock()
;
$factory = new AdvancedCircuitBreakerFactory();
$settings = new FactorySettings(2, 0.1, 10);
$settings
->setStrippedTimeout(0.2)
->setStorage($storage)
;
$circuitBreaker = $factory->create($settings);
$this->assertInstanceOf(AdvancedCircuitBreaker::class, $circuitBreaker);
}
public function testCircuitBreakerWithDefaultFallback()
{
$factory = new AdvancedCircuitBreakerFactory();
$settings = new FactorySettings(2, 0.1, 10);
$settings->setDefaultFallback(function () {
return 'default_fallback';
});
$circuitBreaker = $factory->create($settings);
$this->assertInstanceOf(AdvancedCircuitBreaker::class, $circuitBreaker);
$response = $circuitBreaker->call('unknown_service');
$this->assertEquals(State::OPEN_STATE, $circuitBreaker->getState());
$this->assertEquals('default_fallback', $response);
}
/**
* @return array
*/
public function getSettings()
{
return [
[
(new FactorySettings(2, 0.1, 10))
->setStrippedTimeout(0.2)
->setClientOptions(['proxy' => '192.168.16.1:10']),
],
[
(new FactorySettings(2, 0.1, 10))
->setStrippedTimeout(0.2)
->setClient(new GuzzleClient(['proxy' => '192.168.16.1:10'])),
],
];
}
}

View File

@@ -0,0 +1,289 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace Tests\PrestaShop\CircuitBreaker;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\Request;
use PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Subscriber\Mock;
use PHPUnit_Framework_MockObject_MockObject;
use PrestaShop\CircuitBreaker\AdvancedCircuitBreaker;
use PrestaShop\CircuitBreaker\Client\GuzzleClient;
use PrestaShop\CircuitBreaker\Contract\TransitionDispatcherInterface;
use PrestaShop\CircuitBreaker\Place\ClosedPlace;
use PrestaShop\CircuitBreaker\Place\HalfOpenPlace;
use PrestaShop\CircuitBreaker\Place\OpenPlace;
use PrestaShop\CircuitBreaker\State;
use PrestaShop\CircuitBreaker\Storage\SymfonyCache;
use PrestaShop\CircuitBreaker\System\MainSystem;
use PrestaShop\CircuitBreaker\Transition\NullDispatcher;
use Symfony\Component\Cache\Simple\ArrayCache;
class AdvancedCircuitBreakerTest extends CircuitBreakerTestCase
{
/**
* Used to track the dispatched events.
*
* @var PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount
*/
private $spy;
/**
* We should see the circuit breaker initialized,
* a call being done and then the circuit breaker closed.
*/
public function testCircuitBreakerEventsOnFirstFailedCall()
{
$circuitBreaker = $this->createCircuitBreaker();
$circuitBreaker->call(
'https://httpbin.org/get/foo',
['toto' => 'titi'],
function () {
return '{}';
}
);
/**
* The circuit breaker is initiated
* the 2 failed trials are done
* then the conditions are met to open the circuit breaker
*/
$invocations = $this->spy->getInvocations();
$this->assertCount(4, $invocations);
$this->assertSame('INITIATING', $invocations[0]->parameters[0]);
$this->assertSame('TRIAL', $invocations[1]->parameters[0]);
$this->assertSame('TRIAL', $invocations[2]->parameters[0]);
$this->assertSame('OPENING', $invocations[3]->parameters[0]);
}
public function testSimpleCall()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
$mock = new Mock([
new Response(200, [], Stream::factory('{"hello": "world"}')),
]);
$client = new GuzzleClient(['mock' => $mock]);
$circuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$symfonyCache,
new NullDispatcher()
);
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertSame(State::CLOSED_STATE, $circuitBreaker->getState());
$this->assertEquals(0, $mock->count());
$this->assertEquals('{"hello": "world"}', $response);
}
public function testOpenStateAfterTooManyFailures()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
$mock = new Mock([
new RequestException('Service unavailable', new Request('GET', 'test')),
new RequestException('Service unavailable', new Request('GET', 'test')),
]);
$client = new GuzzleClient(['mock' => $mock]);
$circuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$symfonyCache,
new NullDispatcher()
);
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertEquals(0, $mock->count());
$this->assertEquals(false, $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
}
public function testNoFallback()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
$mock = new Mock([
new RequestException('Service unavailable', new Request('GET', 'test')),
new RequestException('Service unavailable', new Request('GET', 'test')),
]);
$client = new GuzzleClient(['mock' => $mock]);
$circuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$symfonyCache,
new NullDispatcher()
);
$response = $circuitBreaker->call('anything');
$this->assertEquals(0, $mock->count());
$this->assertEquals('', $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
}
public function testBackToClosedStateAfterSuccess()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
$mock = new Mock([
new RequestException('Service unavailable', new Request('GET', 'test')),
new RequestException('Service unavailable', new Request('GET', 'test')),
new Response(200, [], Stream::factory('{"hello": "world"}')),
]);
$client = new GuzzleClient(['mock' => $mock]);
$circuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$symfonyCache,
new NullDispatcher()
);
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertEquals(1, $mock->count());
$this->assertEquals(false, $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
//Stay in OPEN state
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertEquals(1, $mock->count());
$this->assertEquals(false, $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
sleep(2);
//Switch to CLOSED state on success
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertEquals(0, $mock->count());
$this->assertEquals('{"hello": "world"}', $response);
$this->assertSame(State::CLOSED_STATE, $circuitBreaker->getState());
}
public function testStayInOpenStateAfterFailure()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
$mock = new Mock([
new RequestException('Service unavailable', new Request('GET', 'test')),
new RequestException('Service unavailable', new Request('GET', 'test')),
new RequestException('Service unavailable', new Request('GET', 'test')),
]);
$client = new GuzzleClient(['mock' => $mock]);
$circuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$symfonyCache,
new NullDispatcher()
);
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertEquals(1, $mock->count());
$this->assertEquals(false, $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
//Stay in OPEN state
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertEquals(1, $mock->count());
$this->assertEquals(false, $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
sleep(2);
//Switch to OPEN state on failure
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertEquals(0, $mock->count());
$this->assertEquals(false, $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
}
/**
* @return AdvancedCircuitBreaker the circuit breaker for testing purposes
*/
private function createCircuitBreaker()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
/** @var PHPUnit_Framework_MockObject_MockObject|TransitionDispatcherInterface $dispatcher */
$dispatcher = $this->createMock(TransitionDispatcherInterface::class);
$dispatcher->expects($this->spy = $this->any())
->method('dispatchTransition')
;
return new AdvancedCircuitBreaker(
$system,
$this->getTestClient(),
$symfonyCache,
$dispatcher
);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker;
use GuzzleHttp\Message\Request;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Subscriber\Mock;
use PrestaShop\CircuitBreaker\Client\GuzzleClient;
use GuzzleHttp\Exception\RequestException;
use PHPUnit\Framework\TestCase;
/**
* Helper to get a fake Guzzle client.
*/
abstract class CircuitBreakerTestCase extends TestCase
{
/**
* Returns an instance of Client able to emulate
* available and not available services.
*
* @return GuzzleClient
*/
protected function getTestClient()
{
$mock = new Mock([
new RequestException('Service unavailable', new Request('GET', 'test')),
new RequestException('Service unavailable', new Request('GET', 'test')),
new Response(200, [], Stream::factory('{"hello": "world"}')),
]);
return new GuzzleClient(['mock' => $mock]);
}
}

View File

@@ -0,0 +1,263 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\AdvancedCircuitBreaker;
use PrestaShop\CircuitBreaker\Client\GuzzleClient;
use PrestaShop\CircuitBreaker\Contract\CircuitBreakerInterface;
use PrestaShop\CircuitBreaker\Exception\UnavailableServiceException;
use PrestaShop\CircuitBreaker\State;
use PrestaShop\CircuitBreaker\Storage\SimpleArray;
use PrestaShop\CircuitBreaker\Transition\NullDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcher;
use PrestaShop\CircuitBreaker\Storage\SymfonyCache;
use PrestaShop\CircuitBreaker\SymfonyCircuitBreaker;
use PrestaShop\CircuitBreaker\SimpleCircuitBreaker;
use PrestaShop\CircuitBreaker\Place\HalfOpenPlace;
use PrestaShop\CircuitBreaker\Place\ClosedPlace;
use PrestaShop\CircuitBreaker\System\MainSystem;
use PrestaShop\CircuitBreaker\Place\OpenPlace;
use Symfony\Component\Cache\Simple\ArrayCache;
class CircuitBreakerWorkflowTest extends CircuitBreakerTestCase
{
const OPEN_THRESHOLD = 1;
/**
* {@inheritdoc}
*/
protected function setUp()
{
parent::setUp();
//For SimpleCircuitBreaker tests we need to clear the storage cache because it is stored in a static variable
$storage = new SimpleArray();
$storage->clear();
}
/**
* When we use the circuit breaker on unreachable service
* the fallback response is used.
*
* @dataProvider getCircuitBreakers
*
* @param CircuitBreakerInterface $circuitBreaker
*/
public function testCircuitBreakerIsInClosedStateAtStart($circuitBreaker)
{
$this->assertSame(State::CLOSED_STATE, $circuitBreaker->getState());
}
/**
* Once the number of failures is reached, the circuit breaker
* is open. This time no calls to the services are done.
*
* @dataProvider getCircuitBreakers
*
* @param CircuitBreakerInterface $circuitBreaker
*/
public function testCircuitBreakerWillBeOpenInCaseOfFailures($circuitBreaker)
{
// CLOSED
$this->assertSame(State::CLOSED_STATE, $circuitBreaker->getState());
$response = $circuitBreaker->call('https://httpbin.org/get/foo', [], $this->createFallbackResponse());
$this->assertSame('{}', $response);
//After two failed calls switch to OPEN state
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
$this->assertSame(
'{}',
$circuitBreaker->call(
'https://httpbin.org/get/foo',
[],
$this->createFallbackResponse()
)
);
}
/**
* Once the number of failures is reached, the circuit breaker
* is open. This time no calls to the services are done.
*
* @dataProvider getCircuitBreakers
*
* @param CircuitBreakerInterface $circuitBreaker
*/
public function testCircuitBreakerWillBeOpenWithoutFallback($circuitBreaker)
{
// CLOSED
$this->assertSame(State::CLOSED_STATE, $circuitBreaker->getState());
$response = $circuitBreaker->call('https://httpbin.org/get/foo');
$this->assertSame('', $response);
//After two failed calls switch to OPEN state
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
$this->assertSame(
'{}',
$circuitBreaker->call(
'https://httpbin.org/get/foo',
[],
$this->createFallbackResponse()
)
);
}
/**
* In HalfOpen state, if the service is back we can
* close the CircuitBreaker.
*
* @dataProvider getCircuitBreakers
*
* @param CircuitBreakerInterface $circuitBreaker
*/
public function testOnceInHalfOpenModeServiceIsFinallyReachable($circuitBreaker)
{
// CLOSED - first call fails (twice)
$this->assertSame(State::CLOSED_STATE, $circuitBreaker->getState());
$response = $circuitBreaker->call('https://httpbin.org/get/foo', [], $this->createFallbackResponse());
$this->assertSame('{}', $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
// OPEN - no call to client
$response = $circuitBreaker->call('https://httpbin.org/get/foo', [], $this->createFallbackResponse());
$this->assertSame('{}', $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
sleep(2 * self::OPEN_THRESHOLD);
// SWITCH TO HALF OPEN - retry to call the service
$this->assertSame(
'{"hello": "world"}',
$circuitBreaker->call(
'https://httpbin.org/get/foo',
[],
$this->createFallbackResponse()
)
);
$this->assertSame(State::CLOSED_STATE, $circuitBreaker->getState());
$this->assertTrue($circuitBreaker->isClosed());
}
/**
* This is not useful for SimpleCircuitBreaker since it has a SimpleArray storage
*/
public function testRememberLastTransactionState()
{
$system = new MainSystem(
new ClosedPlace(1, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$storage = new SymfonyCache(new ArrayCache());
$client = $this->createMock(GuzzleClient::class);
$client
->expects($this->once())
->method('request')
->willThrowException(new UnavailableServiceException())
;
$firstCircuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$storage,
new NullDispatcher()
);
$this->assertEquals(State::CLOSED_STATE, $firstCircuitBreaker->getState());
$firstCircuitBreaker->call('fake_service', [], function () {
return false;
});
$this->assertEquals(State::OPEN_STATE, $firstCircuitBreaker->getState());
$this->assertTrue($storage->hasTransaction('fake_service'));
$secondCircuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$storage,
new NullDispatcher()
);
$this->assertEquals(State::CLOSED_STATE, $secondCircuitBreaker->getState());
$secondCircuitBreaker->call('fake_service', [], function () {
return false;
});
$this->assertEquals(State::OPEN_STATE, $secondCircuitBreaker->getState());
}
/**
* Return the list of supported circuit breakers
*
* @return array
*/
public function getCircuitBreakers()
{
return [
'simple' => [$this->createSimpleCircuitBreaker()],
'symfony' => [$this->createSymfonyCircuitBreaker()],
'advanced' => [$this->createAdvancedCircuitBreaker()],
];
}
/**
* @return SimpleCircuitBreaker the circuit breaker for testing purposes
*/
private function createSimpleCircuitBreaker()
{
return new SimpleCircuitBreaker(
new OpenPlace(0, 0, self::OPEN_THRESHOLD), // threshold 1s
new HalfOpenPlace(0, 0.2, 0), // timeout 0.2s to test the service
new ClosedPlace(2, 0.2, 0), // 2 failures allowed, 0.2s timeout
$this->getTestClient()
);
}
/**
* @return AdvancedCircuitBreaker the circuit breaker for testing purposes
*/
private function createAdvancedCircuitBreaker()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, self::OPEN_THRESHOLD)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
return new AdvancedCircuitBreaker(
$system,
$this->getTestClient(),
$symfonyCache,
new NullDispatcher()
);
}
/**
* @return SymfonyCircuitBreaker the circuit breaker for testing purposes
*/
private function createSymfonyCircuitBreaker()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, self::OPEN_THRESHOLD)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
$eventDispatcherS = $this->createMock(EventDispatcher::class);
return new SymfonyCircuitBreaker(
$system,
$this->getTestClient(),
$symfonyCache,
$eventDispatcherS
);
}
/**
* @return callable the fallback callable
*/
private function createFallbackResponse()
{
return function () {
return '{}';
};
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Client;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Client\GuzzleClient;
use PrestaShop\CircuitBreaker\Exception\UnavailableServiceException;
class GuzzleClientTest extends TestCase
{
public function testRequestWorksAsExpected()
{
$client = new GuzzleClient();
$this->assertNotNull($client->request('https://www.google.com', [
'method' => 'GET',
]));
}
public function testWrongRequestThrowsAnException()
{
$this->expectException(UnavailableServiceException::class);
$client = new GuzzleClient();
$client->request('http://not-even-a-valid-domain.xxx', []);
}
public function testTheClientAcceptsHttpMethodOverride()
{
$client = new GuzzleClient([
'method' => 'HEAD',
]);
$this->assertEmpty($client->request('https://www.google.fr', []));
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Event;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Event\TransitionEvent;
class TransitionEventTest extends TestCase
{
public function testCreation()
{
$event = new TransitionEvent('foo', 'bar', []);
$this->assertInstanceOf(TransitionEvent::class, $event);
}
/**
* @depends testCreation
*/
public function testGetService()
{
$event = new TransitionEvent('eventName', 'service', []);
$this->assertSame('service', $event->getService());
}
/**
* @depends testCreation
*/
public function testGetEvent()
{
$event = new TransitionEvent('eventName', 'service', []);
$this->assertSame('eventName', $event->getEvent());
}
/**
* @depends testCreation
*/
public function testGetParameters()
{
$parameters = [
'foo' => 'myFoo',
'bar' => true,
];
$event = new TransitionEvent('eventName', 'service', $parameters);
$this->assertSame($parameters, $event->getParameters());
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Exception;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Exception\InvalidPlaceException;
class InvalidPlaceTest extends TestCase
{
public function testCreation()
{
$invalidPlace = new InvalidPlaceException();
$this->assertInstanceOf(InvalidPlaceException::class, $invalidPlace);
}
/**
* @dataProvider getSettings
*
* @param array $settings
* @param string $expectedExceptionMessage
*/
public function testInvalidSettings($settings, $expectedExceptionMessage)
{
$invalidPlace = InvalidPlaceException::invalidSettings(
$settings[0], // failures
$settings[1], // timeout
$settings[2] // threshold
);
$this->assertSame($invalidPlace->getMessage(), $expectedExceptionMessage);
}
/**
* @return array
*/
public function getSettings()
{
return [
'all_invalid_settings' => [
['0', '1', null],
'Invalid settings for Place' . PHP_EOL .
'Excepted failures to be a positive integer, got string (0)' . PHP_EOL .
'Excepted timeout to be a float, got string (1)' . PHP_EOL .
'Excepted threshold to be a positive integer, got NULL' . PHP_EOL,
],
'2_invalid_settings' => [
[0, '1', null],
'Invalid settings for Place' . PHP_EOL .
'Excepted timeout to be a float, got string (1)' . PHP_EOL .
'Excepted threshold to be a positive integer, got NULL' . PHP_EOL,
],
'1_invalid_settings' => [
[0, '1', 2],
'Invalid settings for Place' . PHP_EOL .
'Excepted timeout to be a float, got string (1)' . PHP_EOL,
],
'all_valid_settings' => [
[0, 1.1, 2],
'Invalid settings for Place' . PHP_EOL,
],
];
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Exception;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Exception\InvalidTransactionException;
class InvalidTransactionTest extends TestCase
{
public function testCreation()
{
$invalidPlace = new InvalidTransactionException();
$this->assertInstanceOf(InvalidTransactionException::class, $invalidPlace);
}
/**
* @dataProvider getParameters
*
* @param array $parameters
* @param string $expectedExceptionMessage
*/
public function testInvalidParameters($parameters, $expectedExceptionMessage)
{
$invalidPlace = InvalidTransactionException::invalidParameters(
$parameters[0], // service
$parameters[1], // failures
$parameters[2], // state
$parameters[3] // threshold
);
$this->assertSame($invalidPlace->getMessage(), $expectedExceptionMessage);
}
/**
* @return array
*/
public function getParameters()
{
return [
'all_invalid_parameters' => [
[100, '0', null, 'toto'],
'Invalid parameters for Transaction' . PHP_EOL .
'Excepted service to be an URI, got integer (100)' . PHP_EOL .
'Excepted failures to be a positive integer, got string (0)' . PHP_EOL .
'Excepted state to be a string, got NULL' . PHP_EOL .
'Excepted threshold to be a positive integer, got string (toto)' . PHP_EOL,
],
'3_invalid_parameters' => [
['http://www.prestashop.com', '1', null, 'toto'],
'Invalid parameters for Transaction' . PHP_EOL .
'Excepted failures to be a positive integer, got string (1)' . PHP_EOL .
'Excepted state to be a string, got NULL' . PHP_EOL .
'Excepted threshold to be a positive integer, got string (toto)' . PHP_EOL,
],
'2_invalid_parameters' => [
['http://www.prestashop.com', 10, null, null],
'Invalid parameters for Transaction' . PHP_EOL .
'Excepted state to be a string, got NULL' . PHP_EOL .
'Excepted threshold to be a positive integer, got NULL' . PHP_EOL,
],
'none_invalid' => [
['http://www.prestashop.com', 10, 'CLOSED_STATE', 1],
'Invalid parameters for Transaction' . PHP_EOL,
],
];
}
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace Tests\PrestaShop\CircuitBreaker;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\FactorySettings;
class FactorySettingsTest extends TestCase
{
public function testSimpleSettings()
{
$settings = new FactorySettings(2, 0.5, 10);
$this->assertNotNull($settings);
$this->assertEquals(2, $settings->getFailures());
$this->assertEquals(0.5, $settings->getTimeout());
$this->assertEquals(10, $settings->getThreshold());
$this->assertEquals(2, $settings->getStrippedFailures());
$this->assertEquals(0.5, $settings->getStrippedTimeout());
}
public function testMergeSettings()
{
$defaultSettings = new FactorySettings(2, 0.5, 10);
$defaultSettings
->setStrippedTimeout(1.2)
->setStrippedFailures(1)
;
$this->assertEquals(2, $defaultSettings->getFailures());
$this->assertEquals(0.5, $defaultSettings->getTimeout());
$this->assertEquals(10, $defaultSettings->getThreshold());
$this->assertEquals(1, $defaultSettings->getStrippedFailures());
$this->assertEquals(1.2, $defaultSettings->getStrippedTimeout());
$settings = new FactorySettings(2, 1.5, 20);
$mergedSettings = FactorySettings::merge($defaultSettings, $settings);
$this->assertEquals(2, $mergedSettings->getFailures());
$this->assertEquals(1.5, $mergedSettings->getTimeout());
$this->assertEquals(20, $mergedSettings->getThreshold());
$this->assertEquals(2, $mergedSettings->getStrippedFailures());
$this->assertEquals(1.5, $mergedSettings->getStrippedTimeout());
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Place;
use PrestaShop\CircuitBreaker\Exception\InvalidPlaceException;
use PrestaShop\CircuitBreaker\Place\ClosedPlace;
use PrestaShop\CircuitBreaker\State;
class ClosedPlaceTest extends PlaceTestCase
{
/**
* @dataProvider getFixtures
*
* @param mixed $failures
* @param mixed $timeout
* @param mixed $threshold
*/
public function testCreationWith($failures, $timeout, $threshold)
{
$closedPlace = new ClosedPlace($failures, $timeout, $threshold);
$this->assertSame($failures, $closedPlace->getFailures());
$this->assertSame($timeout, $closedPlace->getTimeout());
$this->assertSame($threshold, $closedPlace->getThreshold());
}
/**
* @dataProvider getInvalidFixtures
*
* @param mixed $failures
* @param mixed $timeout
* @param mixed $threshold
*/
public function testCreationWithInvalidValues($failures, $timeout, $threshold)
{
$this->expectException(InvalidPlaceException::class);
new ClosedPlace($failures, $timeout, $threshold);
}
public function testGetExpectedState()
{
$closedPlace = new ClosedPlace(1, 1, 1);
$this->assertSame(State::CLOSED_STATE, $closedPlace->getState());
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Place;
use PrestaShop\CircuitBreaker\Exception\InvalidPlaceException;
use PrestaShop\CircuitBreaker\Place\HalfOpenPlace;
use PrestaShop\CircuitBreaker\State;
class HalfOpenPlaceTest extends PlaceTestCase
{
/**
* @dataProvider getFixtures
*
* @param mixed $failures
* @param mixed $timeout
* @param mixed $threshold
*/
public function testCreationWith($failures, $timeout, $threshold)
{
$halfOpenPlace = new HalfOpenPlace($failures, $timeout, $threshold);
$this->assertSame($failures, $halfOpenPlace->getFailures());
$this->assertSame($timeout, $halfOpenPlace->getTimeout());
$this->assertSame($threshold, $halfOpenPlace->getThreshold());
}
/**
* @dataProvider getInvalidFixtures
*
* @param mixed $failures
* @param mixed $timeout
* @param mixed $threshold
*/
public function testCreationWithInvalidValues($failures, $timeout, $threshold)
{
$this->expectException(InvalidPlaceException::class);
new HalfOpenPlace($failures, $timeout, $threshold);
}
public function testGetExpectedState()
{
$halfOpenPlace = new HalfOpenPlace(1, 1, 1);
$this->assertSame(State::HALF_OPEN_STATE, $halfOpenPlace->getState());
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Place;
use PrestaShop\CircuitBreaker\Exception\InvalidPlaceException;
use PrestaShop\CircuitBreaker\Place\OpenPlace;
use PrestaShop\CircuitBreaker\State;
class OpenPlaceTest extends PlaceTestCase
{
/**
* @dataProvider getFixtures
*
* @param mixed $failures
* @param mixed $timeout
* @param mixed $threshold
*/
public function testCreationWith($failures, $timeout, $threshold)
{
$openPlace = new OpenPlace($failures, $timeout, $threshold);
$this->assertSame($failures, $openPlace->getFailures());
$this->assertSame($timeout, $openPlace->getTimeout());
$this->assertSame($threshold, $openPlace->getThreshold());
}
/**
* @dataProvider getInvalidFixtures
*
* @param mixed $failures
* @param mixed $timeout
* @param mixed $threshold
*/
public function testCreationWithInvalidValues($failures, $timeout, $threshold)
{
$this->expectException(InvalidPlaceException::class);
new OpenPlace($failures, $timeout, $threshold);
}
public function testGetExpectedState()
{
$openPlace = new OpenPlace(1, 1, 1);
$this->assertSame(State::OPEN_STATE, $openPlace->getState());
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Place;
use PHPUnit\Framework\TestCase;
/**
* Helper to share fixtures accross Place tests.
*/
class PlaceTestCase extends TestCase
{
/**
* @return array
*/
public function getFixtures()
{
return [
'0_0_0' => [0, 0, 0],
'1_100_0' => [1, 100, 0],
'3_0.6_3' => [3, 0.6, 3],
];
}
/**
* @return array
*/
public function getArrayFixtures()
{
return [
'assoc_array' => [[
'timeout' => 3,
'threshold' => 2,
'failures' => 1,
]],
];
}
/**
* @return array
*/
public function getInvalidFixtures()
{
return [
'minus1_null_false' => [-1, null, false],
'3_0.6_3.14' => [3, 0.6, 3.14],
];
}
/**
* @return array
*/
public function getInvalidArrayFixtures()
{
return [
'invalid_indexes' => [[
0 => 3,
1 => 2,
4 => 1,
]],
'invalid_keys' => [[
'timeout' => 3,
'max_wait' => 2,
'failures' => 1,
]],
];
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Contract\FactorySettingsInterface;
use PrestaShop\CircuitBreaker\FactorySettings;
use PrestaShop\CircuitBreaker\SimpleCircuitBreaker;
use PrestaShop\CircuitBreaker\SimpleCircuitBreakerFactory;
class SimpleCircuitBreakerFactoryTest extends TestCase
{
/**
* @return void
*/
public function testCreation()
{
$factory = new SimpleCircuitBreakerFactory();
$this->assertInstanceOf(SimpleCircuitBreakerFactory::class, $factory);
}
/**
* @depends testCreation
* @dataProvider getSettings
*
* @param FactorySettingsInterface $settings the Circuit Breaker settings
*
* @return void
*/
public function testCircuitBreakerCreation(FactorySettingsInterface $settings)
{
$factory = new SimpleCircuitBreakerFactory();
$circuitBreaker = $factory->create($settings);
$this->assertInstanceOf(SimpleCircuitBreaker::class, $circuitBreaker);
}
/**
* @return array
*/
public function getSettings()
{
return [
[
(new FactorySettings(2, 0.1, 10))
->setStrippedTimeout(0.2)
->setStrippedFailures(1),
],
[
(new FactorySettings(2, 0.1, 10))
->setStrippedTimeout(0.2)
->setStrippedFailures(1)
->setClientOptions(['proxy' => '192.168.16.1:10']),
],
];
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Storage;
use Doctrine\Common\Cache\FilesystemCache;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Exception\TransactionNotFoundException;
use PrestaShop\CircuitBreaker\Storage\DoctrineCache;
class DoctrineCacheTest extends TestCase
{
/**
* @var StorageInterface the Doctrine Cache storage
*/
private $doctrineCache;
/**
* {@inheritdoc}
*/
protected function setUp()
{
$this->doctrineCache = new DoctrineCache(
new FilesystemCache(sys_get_temp_dir() . '/ps__circuit_breaker')
);
}
/**
* {@inheritdoc}
*/
protected function tearDown()
{
$filesystemAdapter = new FilesystemCache(sys_get_temp_dir() . '/ps__circuit_breaker');
$filesystemAdapter->deleteAll();
}
public function testCreation()
{
$doctrineCache = new DoctrineCache(
new FilesystemCache(sys_get_temp_dir() . '/ps__circuit_breaker')
);
$this->assertInstanceOf(DoctrineCache::class, $doctrineCache);
}
/**
* @depends testCreation
*/
public function testSaveTransaction()
{
$operation = $this->doctrineCache->saveTransaction(
'http://test.com',
$this->createMock(TransactionInterface::class)
);
$this->assertTrue($operation);
}
/**
* @depends testCreation
* @depends testSaveTransaction
*/
public function testHasTransaction()
{
$this->doctrineCache->saveTransaction('http://test.com', $this->createMock(TransactionInterface::class));
$this->assertTrue($this->doctrineCache->hasTransaction('http://test.com'));
}
/**
* @depends testCreation
* @depends testSaveTransaction
* @depends testHasTransaction
*/
public function testGetTransaction()
{
$translationStub = $this->createMock(TransactionInterface::class);
$this->doctrineCache->saveTransaction('http://test.com', $translationStub);
$transaction = $this->doctrineCache->getTransaction('http://test.com');
$this->assertEquals($transaction, $translationStub);
}
/**
* @depends testCreation
* @depends testGetTransaction
* @depends testHasTransaction
*/
public function testGetNotFoundTransactionThrowsAnException()
{
$this->expectException(TransactionNotFoundException::class);
$this->doctrineCache->getTransaction('http://test.com');
}
/**
* @depends testSaveTransaction
* @depends testGetTransaction
*/
public function testClear()
{
$translationStub = $this->createMock(TransactionInterface::class);
$this->doctrineCache->saveTransaction('http://a.com', $translationStub);
$this->doctrineCache->saveTransaction('http://b.com', $translationStub);
// We have stored 2 transactions
$this->assertTrue($this->doctrineCache->clear());
$this->expectException(TransactionNotFoundException::class);
$this->doctrineCache->getTransaction('http://a.com');
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Storage;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Exception\TransactionNotFoundException;
use PrestaShop\CircuitBreaker\Storage\SimpleArray;
class SimpleArrayTest extends TestCase
{
/**
* {@inheritdoc}
*/
protected function setUp()
{
$simpleArray = new SimpleArray();
$simpleArray::$transactions = [];
}
/**
* @return void
*/
public function testCreation()
{
$simpleArray = new SimpleArray();
$this->assertCount(0, $simpleArray::$transactions);
$this->assertInstanceOf(SimpleArray::class, $simpleArray);
}
/**
* @depends testCreation
*
* @return void
*/
public function testSaveTransaction()
{
$simpleArray = new SimpleArray();
$operation = $simpleArray->saveTransaction(
'http://test.com',
$this->createMock(TransactionInterface::class)
);
$this->assertTrue($operation);
$this->assertCount(1, $simpleArray::$transactions);
}
/**
* @depends testCreation
* @depends testSaveTransaction
*
* @return void
*/
public function testHasTransaction()
{
$simpleArray = new SimpleArray();
$simpleArray->saveTransaction('http://test.com', $this->createMock(TransactionInterface::class));
$this->assertTrue($simpleArray->hasTransaction('http://test.com'));
}
/**
* @depends testCreation
* @depends testSaveTransaction
* @depends testHasTransaction
*
* @return void
*/
public function testGetTransaction()
{
$simpleArray = new SimpleArray();
$translationStub = $this->createMock(TransactionInterface::class);
$simpleArray->saveTransaction('http://test.com', $translationStub);
$transaction = $simpleArray->getTransaction('http://test.com');
$this->assertSame($transaction, $translationStub);
}
/**
* @depends testCreation
* @depends testGetTransaction
* @depends testHasTransaction
*
* @return void
*/
public function testGetNotFoundTransactionThrowsAnException()
{
$this->expectException(TransactionNotFoundException::class);
$simpleArray = new SimpleArray();
$simpleArray->getTransaction('http://test.com');
}
/**
* @depends testSaveTransaction
* @depends testGetTransaction
*
* @return void
*/
public function testClear()
{
$simpleArray = new SimpleArray();
$translationStub = $this->createMock(TransactionInterface::class);
$simpleArray->saveTransaction('http://a.com', $translationStub);
$simpleArray->saveTransaction('http://b.com', $translationStub);
// We have stored 2 transactions
$simpleArray->clear();
$transactions = $simpleArray::$transactions;
$this->assertEmpty($transactions);
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Storage;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Exception\TransactionNotFoundException;
use PrestaShop\CircuitBreaker\Storage\SymfonyCache;
use Symfony\Component\Cache\Simple\FilesystemCache;
class SymfonyCacheTest extends TestCase
{
/**
* @var SymfonyCache the Symfony Cache storage
*/
private $symfonyCache;
/**
* {@inheritdoc}
*/
protected function setUp()
{
$this->symfonyCache = new SymfonyCache(
new FilesystemCache('ps__circuit_breaker', 20)
);
}
/**
* {@inheritdoc}
*/
protected function tearDown()
{
$filesystemAdapter = new FilesystemCache('ps__circuit_breaker', 20);
$filesystemAdapter->clear();
}
public function testCreation()
{
$symfonyCache = new SymfonyCache(
new FilesystemCache('ps__circuit_breaker')
);
$this->assertInstanceOf(SymfonyCache::class, $symfonyCache);
}
/**
* @depends testCreation
*/
public function testSaveTransaction()
{
$operation = $this->symfonyCache->saveTransaction(
'http://test.com',
$this->createMock(TransactionInterface::class)
);
$this->assertTrue($operation);
}
/**
* @depends testCreation
* @depends testSaveTransaction
*/
public function testHasTransaction()
{
$this->symfonyCache->saveTransaction('http://test.com', $this->createMock(TransactionInterface::class));
$this->assertTrue($this->symfonyCache->hasTransaction('http://test.com'));
}
/**
* @depends testCreation
* @depends testSaveTransaction
* @depends testHasTransaction
*/
public function testGetTransaction()
{
$translationStub = $this->createMock(TransactionInterface::class);
$this->symfonyCache->saveTransaction('http://test.com', $translationStub);
$transaction = $this->symfonyCache->getTransaction('http://test.com');
$this->assertEquals($transaction, $translationStub);
}
/**
* @depends testCreation
* @depends testGetTransaction
* @depends testHasTransaction
*/
public function testGetNotFoundTransactionThrowsAnException()
{
$this->expectException(TransactionNotFoundException::class);
$this->symfonyCache->getTransaction('http://test.com');
}
/**
* @depends testSaveTransaction
* @depends testGetTransaction
*/
public function testClear()
{
$translationStub = $this->createMock(TransactionInterface::class);
$this->symfonyCache->saveTransaction('http://a.com', $translationStub);
$this->symfonyCache->saveTransaction('http://b.com', $translationStub);
// We have stored 2 transactions
$this->assertTrue($this->symfonyCache->clear());
$this->expectException(TransactionNotFoundException::class);
$this->symfonyCache->getTransaction('http://a.com');
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker;
use PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount;
use Symfony\Component\EventDispatcher\EventDispatcher;
use PrestaShop\CircuitBreaker\SymfonyCircuitBreaker;
use PrestaShop\CircuitBreaker\Storage\SymfonyCache;
use PrestaShop\CircuitBreaker\Place\HalfOpenPlace;
use PrestaShop\CircuitBreaker\System\MainSystem;
use PrestaShop\CircuitBreaker\Place\ClosedPlace;
use PrestaShop\CircuitBreaker\Place\OpenPlace;
use Symfony\Component\Cache\Simple\ArrayCache;
class SymfonyCircuitBreakerEventsTest extends CircuitBreakerTestCase
{
/**
* Used to track the dispatched events.
*
* @var PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount
*/
private $spy;
/**
* We should see the circuit breaker initialized,
* a call being done and then the circuit breaker closed.
*/
public function testCircuitBreakerEventsOnFirstFailedCall()
{
$circuitBreaker = $this->createCircuitBreaker();
$circuitBreaker->call(
'https://httpbin.org/get/foo',
[],
function () {
return '{}';
}
);
/**
* The circuit breaker is initiated
* the 2 failed trials are done
* then the conditions are met to open the circuit breaker
*/
$invocations = $this->spy->getInvocations();
$this->assertCount(4, $invocations);
$this->assertSame('INITIATING', $invocations[0]->parameters[0]);
$this->assertSame('TRIAL', $invocations[1]->parameters[0]);
$this->assertSame('TRIAL', $invocations[2]->parameters[0]);
$this->assertSame('OPENING', $invocations[3]->parameters[0]);
}
/**
* @return SymfonyCircuitBreaker the circuit breaker for testing purposes
*/
private function createCircuitBreaker()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
$eventDispatcherS = $this->createMock(EventDispatcher::class);
$eventDispatcherS->expects($this->spy = $this->any())
->method('dispatch')
;
return new SymfonyCircuitBreaker(
$system,
$this->getTestClient(),
$symfonyCache,
$eventDispatcherS
);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\System;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\State;
use PrestaShop\CircuitBreaker\Place\OpenPlace;
use PrestaShop\CircuitBreaker\Place\HalfOpenPlace;
use PrestaShop\CircuitBreaker\Place\ClosedPlace;
use PrestaShop\CircuitBreaker\Contract\PlaceInterface;
use PrestaShop\CircuitBreaker\System\MainSystem;
class MainSystemTest extends TestCase
{
public function testCreation()
{
$openPlace = new OpenPlace(1, 1, 1);
$halfOpenPlace = new HalfOpenPlace(1, 1, 1);
$closedPlace = new ClosedPlace(1, 1, 1);
$mainSystem = new MainSystem(
$openPlace,
$halfOpenPlace,
$closedPlace
);
$this->assertInstanceOf(MainSystem::class, $mainSystem);
}
/**
* @depends testCreation
*/
public function testGetInitialPlace()
{
$mainSystem = $this->createMainSystem();
$initialPlace = $mainSystem->getInitialPlace();
$this->assertInstanceOf(PlaceInterface::class, $initialPlace);
$this->assertSame(State::CLOSED_STATE, $initialPlace->getState());
}
/**
* @depends testCreation
*/
public function testGetPlaces()
{
$mainSystem = $this->createMainSystem();
$places = $mainSystem->getPlaces();
$this->assertInternalType('array', $places);
$this->assertCount(3, $places);
foreach ($places as $place) {
$this->assertInstanceOf(PlaceInterface::class, $place);
}
}
/**
* Returns an instance of MainSystem for tests.
*
* @return MainSystem
*/
private function createMainSystem()
{
$openPlace = new OpenPlace(1, 1, 1);
$halfOpenPlace = new HalfOpenPlace(1, 1, 1);
$closedPlace = new ClosedPlace(1, 1, 1);
return new MainSystem(
$openPlace,
$halfOpenPlace,
$closedPlace
);
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Transaction;
use DateTime;
use PHPUnit\Framework\TestCase;
use PHPUnit_Framework_MockObject_MockObject;
use PrestaShop\CircuitBreaker\Contract\PlaceInterface;
use PrestaShop\CircuitBreaker\Transaction\SimpleTransaction;
class SimpleTransactionTest extends TestCase
{
public function testCreation()
{
$placeStub = $this->createPlaceStub();
$simpleTransaction = new SimpleTransaction(
'http://some-uri.domain',
0,
$placeStub->getState(),
2
);
$this->assertInstanceOf(SimpleTransaction::class, $simpleTransaction);
}
/**
* @depends testCreation
*/
public function testGetService()
{
$simpleTransaction = $this->createSimpleTransaction();
$this->assertSame('http://some-uri.domain', $simpleTransaction->getService());
}
/**
* @depends testCreation
*/
public function testGetFailures()
{
$simpleTransaction = $this->createSimpleTransaction();
$this->assertSame(0, $simpleTransaction->getFailures());
}
/**
* @depends testCreation
*/
public function testGetState()
{
$simpleTransaction = $this->createSimpleTransaction();
$this->assertSame('FAKE_STATE', $simpleTransaction->getState());
}
/**
* @depends testCreation
*/
public function testGetThresholdDateTime()
{
$simpleTransaction = $this->createSimpleTransaction();
$expectedDateTime = (new DateTime('+2 second'))->format('d/m/Y H:i:s');
$simpleTransactionDateTime = $simpleTransaction->getThresholdDateTime()->format('d/m/Y H:i:s');
$this->assertSame($expectedDateTime, $simpleTransactionDateTime);
}
/**
* @depends testCreation
* @depends testGetFailures
*/
public function testIncrementFailures()
{
$simpleTransaction = $this->createSimpleTransaction();
$simpleTransaction->incrementFailures();
$this->assertSame(1, $simpleTransaction->getFailures());
}
/**
* @depends testCreation
*/
public function testCreationFromPlaceHelper()
{
$simpleTransactionFromHelper = SimpleTransaction::createFromPlace(
$this->createPlaceStub(),
'http://some-uri.domain'
);
$simpleTransaction = $this->createSimpleTransaction();
$this->assertSame($simpleTransactionFromHelper->getState(), $simpleTransaction->getState());
$this->assertSame($simpleTransactionFromHelper->getFailures(), $simpleTransaction->getFailures());
$fromPlaceDate = $simpleTransactionFromHelper->getThresholdDateTime()->format('d/m/Y H:i:s');
$expectedDate = $simpleTransaction->getThresholdDateTime()->format('d/m/Y H:i:s');
$this->assertSame($fromPlaceDate, $expectedDate);
}
/**
* Returns an instance of SimpleTransaction for tests.
*
* @return SimpleTransaction
*/
private function createSimpleTransaction()
{
$placeStub = $this->createPlaceStub();
return new SimpleTransaction(
'http://some-uri.domain',
0,
$placeStub->getState(),
2
);
}
/**
* Returns an instance of Place with State equals to "FAKE_STATE"
* and threshold equals to 2.
*
* @return PlaceInterface&PHPUnit_Framework_MockObject_MockObject
*/
private function createPlaceStub()
{
$placeStub = $this->createMock(PlaceInterface::class);
$placeStub->expects($this->any())
->method('getState')
->willReturn('FAKE_STATE')
;
$placeStub->expects($this->any())
->method('getThreshold')
->willReturn(2)
;
return $placeStub;
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Util;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Util\Assert;
use stdClass;
class AssertTest extends TestCase
{
/**
* @dataProvider getValues
*
* @param mixed $value
* @param bool $expected
*/
public function testIsPositiveValue($value, $expected)
{
$this->assertSame($expected, Assert::isPositiveValue($value));
}
/**
* @dataProvider getURIs
*
* @param mixed $value
* @param bool $expected
*/
public function testIsURI($value, $expected)
{
$this->assertSame($expected, Assert::isURI($value));
}
/**
* @dataProvider getStrings
*
* @param mixed $value
* @param bool $expected
*/
public function testIsString($value, $expected)
{
$this->assertSame($expected, Assert::isString($value));
}
/**
* @return array
*/
public function getValues()
{
return [
'0' => [0, true],
'str_0' => ['0', false],
'float' => [0.1, true],
'stdclass' => [new stdClass(), false],
'callable' => [
function () {
return 0;
},
false,
],
'negative' => [-1, false],
'bool' => [false, false],
];
}
/**
* @return array
*/
public function getURIs()
{
return [
'valid' => ['http://www.prestashop.com', true],
'int' => [0, false],
'null' => [null, false],
'bool' => [false, false],
'local' => ['http://localhost', true],
'ssh' => ['ssh://git@git.example.com/FOO/my_project.git', true],
];
}
public function getStrings()
{
return [
'valid' => ['foo', true],
'empty' => ['', false],
'null' => [null, false],
'bool' => [false, false],
'stdclass' => [new stdClass(), false],
'valid2' => ['INVALID_STATE', true],
];
}
}

View File

@@ -0,0 +1,170 @@
Academic Free License (“AFL”) v. 3.0
This Academic Free License (the "License") applies to any original work of
authorship (the "Original Work") whose owner (the "Licensor") has placed the
following licensing notice adjacent to the copyright notice for the Original
Work:
Licensed under the Academic Free License version 3.0
1) Grant of Copyright License. Licensor grants You a worldwide, royalty-free,
non-exclusive, sublicensable license, for the duration of the copyright, to do
the following:
a) to reproduce the Original Work in copies, either alone or as part of a
collective work;
b) to translate, adapt, alter, transform, modify, or arrange the Original
Work, thereby creating derivative works ("Derivative Works") based upon
the Original Work;
c) to distribute or communicate copies of the Original Work and
Derivative Works to the public, under any license of your choice that
does not contradict the terms and conditions, including Licensors
reserved rights and remedies, in this Academic Free License;
d) to perform the Original Work publicly; and
e) to display the Original Work publicly.
2) Grant of Patent License. Licensor grants You a worldwide, royalty-free,
non-exclusive, sublicensable license, under patent claims owned or controlled
by the Licensor that are embodied in the Original Work as furnished by the
Licensor, for the duration of the patents, to make, use, sell, offer for sale,
have made, and import the Original Work and Derivative Works.
3) Grant of Source Code License. The term "Source Code" means the preferred
form of the Original Work for making modifications to it and all available
documentation describing how to modify the Original Work. Licensor agrees to
provide a machine-readable copy of the Source Code of the Original Work along
with each copy of the Original Work that Licensor distributes. Licensor
reserves the right to satisfy this obligation by placing a machine-readable
copy of the Source Code in an information repository reasonably calculated to
permit inexpensive and convenient access by You for as long as Licensor
continues to distribute the Original Work.
4) Exclusions From License Grant. Neither the names of Licensor, nor the names
of any contributors to the Original Work, nor any of their trademarks or
service marks, may be used to endorse or promote products derived from this
Original Work without express prior permission of the Licensor. Except as
expressly stated herein, nothing in this License grants any license to
Licensors trademarks, copyrights, patents, trade secrets or any other
intellectual property. No patent license is granted to make, use, sell, offer
for sale, have made, or import embodiments of any patent claims other than the
licensed claims defined in Section 2. No license is granted to the trademarks
of Licensor even if such marks are included in the Original Work. Nothing in
this License shall be interpreted to prohibit Licensor from licensing under
terms different from this License any Original Work that Licensor otherwise
would have a right to license.
5) External Deployment. The term "External Deployment" means the use,
distribution, or communication of the Original Work or Derivative Works in any
way such that the Original Work or Derivative Works may be used by anyone
other than You, whether those works are distributed or communicated to those
persons or made available as an application intended for use over a network.
As an express condition for the grants of license hereunder, You must treat
any External Deployment by You of the Original Work or a Derivative Work as a
distribution under section 1(c).
6) Attribution Rights. You must retain, in the Source Code of any Derivative
Works that You create, all copyright, patent, or trademark notices from the
Source Code of the Original Work, as well as any notices of licensing and any
descriptive text identified therein as an "Attribution Notice." You must cause
the Source Code for any Derivative Works that You create to carry a prominent
Attribution Notice reasonably calculated to inform recipients that You have
modified the Original Work.
7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that
the copyright in and to the Original Work and the patent rights granted herein
by Licensor are owned by the Licensor or are sublicensed to You under the
terms of this License with the permission of the contributor(s) of those
copyrights and patent rights. Except as expressly stated in the immediately
preceding sentence, the Original Work is provided under this License on an "AS
IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without
limitation, the warranties of non-infringement, merchantability or fitness for
a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK
IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this
License. No license to the Original Work is granted by this License except
under this disclaimer.
8) Limitation of Liability. Under no circumstances and under no legal theory,
whether in tort (including negligence), contract, or otherwise, shall the
Licensor be liable to anyone for any indirect, special, incidental, or
consequential damages of any character arising as a result of this License or
the use of the Original Work including, without limitation, damages for loss
of goodwill, work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses. This limitation of liability shall not
apply to the extent applicable law prohibits such limitation.
9) Acceptance and Termination. If, at any time, You expressly assented to this
License, that assent indicates your clear and irrevocable acceptance of this
License and all of its terms and conditions. If You distribute or communicate
copies of the Original Work or a Derivative Work, You must make a reasonable
effort under the circumstances to obtain the express assent of recipients to
the terms of this License. This License conditions your rights to undertake
the activities listed in Section 1, including your right to create Derivative
Works based upon the Original Work, and doing so without honoring these terms
and conditions is prohibited by copyright law and international treaty.
Nothing in this License is intended to affect copyright exceptions and
limitations (including “fair use” or “fair dealing”). This License shall
terminate immediately and You may no longer exercise any of the rights granted
to You by this License upon your failure to honor the conditions in Section
1(c).
10) Termination for Patent Action. This License shall terminate automatically
and You may no longer exercise any of the rights granted to You by this
License as of the date You commence an action, including a cross-claim or
counterclaim, against Licensor or any licensee alleging that the Original Work
infringes a patent. This termination provision shall not apply for an action
alleging patent infringement by combinations of the Original Work with other
software or hardware.
11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this
License may be brought only in the courts of a jurisdiction wherein the
Licensor resides or in which Licensor conducts its primary business, and under
the laws of that jurisdiction excluding its conflict-of-law provisions. The
application of the United Nations Convention on Contracts for the
International Sale of Goods is expressly excluded. Any use of the Original
Work outside the scope of this License or after its termination shall be
subject to the requirements and penalties of copyright or patent law in the
appropriate jurisdiction. This section shall survive the termination of this
License.
12) Attorneys Fees. In any action to enforce the terms of this License or
seeking damages relating thereto, the prevailing party shall be entitled to
recover its costs and expenses, including, without limitation, reasonable
attorneys' fees and costs incurred in connection with such action, including
any appeal of such action. This section shall survive the termination of this
License.
13) Miscellaneous. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent necessary
to make it enforceable.
14) Definition of "You" in This License. "You" throughout this License,
whether in upper or lower case, means an individual or a legal entity
exercising rights under, and complying with all of the terms of, this License.
For legal entities, "You" includes any entity that controls, is controlled by,
or is under common control with you. For purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the direction or
management of such entity, whether by contract or otherwise, or (ii) ownership
of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial
ownership of such entity.
15) Right to Use. You may use the Original Work in all ways not otherwise
restricted or conditioned by this License or by law, and Licensor promises not
to interfere with or be responsible for such uses by You.
16) Modification of This License. This License is Copyright © 2005 Lawrence
Rosen. Permission is granted to copy, distribute, or communicate this License
without modification. Nothing in this License permits You to modify this
License as applied to the Original Work or to Derivative Works. However, You
may modify the text of this License and copy, distribute or communicate your
modified version (the "Modified License") and apply it to other original works
of authorship subject to the following conditions: (i) You may not indicate in
any way that your Modified License is the "Academic Free License" or "AFL" and
you may not use those names in the name of your Modified License; (ii) You
must replace the notice specified in the first paragraph above with the notice
"Licensed under <insert your license name here>" or with a notice of your own
that is not confusingly similar to the notice in this License; and (iii) You
may not claim that your original works are open source software unless your
Modified License has been approved by Open Source Initiative (OSI) and You
comply with its license review and certification process.

View File

@@ -0,0 +1,30 @@
# PrestaShop Cache Directory Provider for Modules
This repository provides the cache directory for PrestaShop modules.
## Pre-requisites
You should install this library only on a PrestaShop environment and with PHP 5.6.0 minimum.
## Installation
```
composer require prestashop/module-lib-cache-directory-provider
```
When this project is successfully added to your dependencies, you can add the new CacheDirectoryProvider to your module and use it.
## Usage
To use this library, it's simple :
```
$cacheDirectoryProvider = new CacheDirectoryProvider(
_PS_VERSION_,
_PS_ROOT_DIR_,
_PS_MODE_DEV_
);
```
With the getPath() function, you will retrieve the cache path of your module :
```
$cacheDirectoryProvider->getPath();
```

View File

@@ -0,0 +1,34 @@
{
"name": "prestashop/module-lib-cache-directory-provider",
"description": "Cache directory provider to use on prestashop modules",
"keywords": ["prestashop", "modules", "composer", "package"],
"license": "AFL-3.0",
"authors": [
{
"name": "PrestaShop SA",
"email": "contact@prestashop.com"
}
],
"type": "project",
"config": {
"platform": {
"php": "5.6.0"
}
},
"require": {
"php": ">=5.6.0"
},
"require-dev": {
"phpunit/phpunit": "~5.7"
},
"autoload": {
"psr-4": {
"PrestaShop\\ModuleLibCacheDirectoryProvider\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,98 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\ModuleLibCacheDirectoryProvider\Cache;
/**
* Class responsible for returning cache directory path.
*/
class CacheDirectoryProvider
{
/**
* @var string PrestaShop version
*/
private $psVersion;
/**
* @var string PrestaShop path
*/
private $psPath;
/**
* @var bool PrestaShop Debug Mode
*/
private $psIsDebugMode;
/**
* @param string $psVersion
* @param string $psPath
* @param bool $psIsDebugMode
*/
public function __construct($psVersion, $psPath, $psIsDebugMode)
{
$this->psVersion = $psVersion;
$this->psPath = $psPath;
$this->psIsDebugMode = $psIsDebugMode;
}
/**
* @return string
*/
public function getPath()
{
if (defined('_PS_CACHE_DIR_')) {
return constant('_PS_CACHE_DIR_');
}
$path = '/var/cache/' . $this->getEnvName();
if (version_compare($this->psVersion, '1.7.0.0', '<')) {
$path = '/cache';
} elseif (version_compare($this->psVersion, '1.7.4.0', '<')) {
$path = '/app/cache/' . $this->getEnvName();
}
return $this->psPath . $path;
}
/**
* @return bool
*/
public function isWritable()
{
return is_writable($this->getPath());
}
/**
* @return bool
*/
public function isReadable()
{
return is_readable($this->getPath());
}
/**
* @return string
*/
private function getEnvName()
{
return $this->psIsDebugMode ? 'dev' : 'prod';
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace Tests\Unit\Cache;
use PHPUnit\Framework\TestCase;
use PrestaShop\ModuleLibCacheDirectoryProvider\Cache\CacheDirectoryProvider;
class CacheDirectoryProviderTest extends TestCase
{
public function testItIsReturnValidPathForVersionLessThan17()
{
$cacheDirectory = new CacheDirectoryProvider('1.6.1.0', __DIR__, true);
$this->assertSame(__DIR__ . '/cache', $cacheDirectory->getPath());
}
public function testItIsReturnValidPathForVersionLessThan174()
{
$cacheDirectory = new CacheDirectoryProvider('1.7.0.0', __DIR__, true);
$this->assertSame(__DIR__ . '/app/cache/dev', $cacheDirectory->getPath());
}
public function testItIsReturnValidPathForVersionGreaterThanEq174()
{
$cacheDirectory = new CacheDirectoryProvider('1.7.4.0', __DIR__, true);
$this->assertSame(__DIR__ . '/var/cache/dev', $cacheDirectory->getPath());
}
}

View File

@@ -0,0 +1,9 @@
<phpunit stopOnFailure="true"
backupGlobals="true"
>
<testsuites>
<testsuite name="Unit">
<directory>.</directory>
</testsuite>
</testsuites>
</phpunit>

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,11 @@
<?php
$config = new PrestaShop\CodingStandards\CsFixer\Config();
$config
->setUsingCache(false)
->getFinder()
->in(__DIR__)
->exclude('vendor');
return $config;

View File

@@ -0,0 +1,84 @@
# PrestaShop module library for FAQ
This library retrieves the FAQ (Frequently Asked Questions) content from PrestaShop APIs.
It is most likely to be used in PrestaShop modules, but can be integrated in any PHP project.
This library is compatible with PHP 5.6 and above.
## Installation
```
composer require prestashop/module-lib-faq
```
## Usage
* Common case
The simplest way to use this library is:
```php
use PrestaShop\ModuleLibFaq\Faq;
// [...]
$faq = new Faq('your module key', _PS_VERSION, 'the current iso code');
$faqContent = $faq->getFaq();
```
* With additional code to run in case of error
```php
use PrestaShop\ModuleLibFaq\Faq;
// [...]
$faq = new Faq('your module key', _PS_VERSION, 'the current iso code');
$faq->setErrorCallable(function(\Exception $e) { /* send to logger */ });
$faqContent = $faq->getFaq();
```
The content returned is an array of categories, each of them having an array of questions/answers.
Example of json that will be `json_decode`d by the library:
```json
{
"id_faq": 117,
"module_key": "82bc76354cfef947e06f1cc78f5efe2e",
"id_product": 46347,
"categories": [
{
"id_faq_category": 142,
"position": 1,
"id_faq": 117,
"title": "Questions about payments",
"blocks": [
{
"id_faq_block": 522,
"question": "Will my customers be able to see the PrestaShop Checkout payment method?",
"answer": "No, PrestaShop Checkout is the name of the service and the module. Your customers will only see the payment methods they are already familiar with including: credit card, PayPal, and/or another major payment method used in their country.",
"version_min": "",
"version_max": ""
},
]
},
{
"id_faq_category": 141,
"position": 2,
"id_faq": 117,
"title": "Questions about refund feature",
"blocks": [
{
"id_faq_block": 521,
"question": "I am trying to issue a refund for a PrestaShop order, but an error message says, “Capture could not be refunded due to insufficient funds.” However, I know I have the funds in my PayPal account.",
"answer": "For refunds for an order paid for using a different currency than the ones available on your PayPal account, you have two options:\n\n- Adding all the currencies covered in your store to your PayPal account to avoid rejections of cross-currency transactions and any refund issues.\n\nTo do so, go to paypal.com > Login > Settings > My Money > Currencies management > Add a currency\n\n- Contacting PayPal and asking them to activate the cross-currency refund option. (Via the \"\"Contact us\"\" tab on paypal.com).",
"version_min": "",
"version_max": ""
}
]
}
]
}
```

View File

@@ -0,0 +1,37 @@
{
"name": "prestashop/module-lib-faq",
"description": "Library allowing modules to load FAQ from PrestaShop servers",
"type": "library",
"license": "AFL-3.0",
"authors": [
{
"name": "PrestaShop SA",
"email": "contact@prestashop.com"
}
],
"config": {
"platform": {
"php": "7.2.5"
}
},
"require": {
"php": "^7.2 || ^8.0",
"prestashop/module-lib-guzzle-adapter": "^0"
},
"require-dev": {
"phpunit/phpunit": "~5.7",
"prestashop/php-dev-tools": "^4.2",
"php-http/mock-client": "^1.5",
"guzzlehttp/guzzle": "^7.4"
},
"autoload": {
"psr-4": {
"PrestaShop\\ModuleLibFaq\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"PrestaShop\\ModuleLibFaq\\Tests\\": "tests/"
}
}
}

View File

@@ -0,0 +1,136 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\ModuleLibFaq;
use GuzzleHttp\Psr7\Request;
use Http\Client\Exception\HttpException;
use Http\Client\Exception\TransferException;
use Prestashop\ModuleLibGuzzleAdapter\ClientFactory;
use Psr\Http\Client\ClientInterface;
/**
* Retrieve the FAQ of the module
*/
class Faq
{
public const BASE_URL = 'https://api.addons.prestashop.com/request/faq/';
/**
* @var ClientInterface
*/
private $client;
/**
* @var Parameters
*/
private $parameters;
/**
* Method to call in case of error
*
* @var callable|null
*/
private $errorCallable;
/**
* @param string $moduleKey
* @param string $psVersion
* @param string $isoCode
*/
public function __construct($moduleKey, $psVersion, $isoCode, array $options = [])
{
$this->parameters = (new Parameters())
->setModuleKey($moduleKey)
->setPsVersion($psVersion)
->setIsoCode($isoCode);
// Allow client options to be customized
$options = array_merge_recursive([
'base_url' => self::BASE_URL,
// If available from PrestaShop, use _PS_CACHE_CA_CERT_FILE_ constant
'verify' => defined('_PS_CACHE_CA_CERT_FILE_') && file_exists(constant('_PS_CACHE_CA_CERT_FILE_')) ? constant('_PS_CACHE_CA_CERT_FILE_') : true,
'defaults' => [
'timeout' => 10,
],
], $options);
$this->client = (new ClientFactory())->getClient($options);
}
/**
* Wrapper of method post from guzzle client
*
* @return array|false return response or false if no response
*/
public function getFaq()
{
try {
$response = $this->client->sendRequest(new Request('POST', $this->parameters->getFaqUri()));
} catch (HttpException $e) {
if (is_callable($this->errorCallable)) {
call_user_func($this->errorCallable, $e);
}
$response = $e->getResponse();
} catch (TransferException $e) {
if (is_callable($this->errorCallable)) {
call_user_func($this->errorCallable, $e);
}
return false;
}
$data = json_decode($response->getBody()->getContents(), true);
return !empty($data['categories']) ? $data : false;
}
/**
* @return ClientInterface
*/
public function getClient()
{
return $this->client;
}
/**
* @param ClientInterface $client
*
* @return self
*/
public function setClient(ClientInterface $client)
{
$this->client = $client;
return $this;
}
/**
* Allow modules to run specific code in case of exception
*
* @return self
*/
public function setErrorCallable(callable $c)
{
$this->errorCallable = $c;
return $this;
}
}

View File

@@ -0,0 +1,115 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\ModuleLibFaq;
class Parameters
{
/**
* Module key to identify on which module we will retrieve the faq
*
* @var string
*/
private $moduleKey;
/**
* The version of PrestaShop
*
* @var string
*/
private $psVersion;
/**
* In which language the faq will be retrieved
*
* @var string
*/
private $isoCode;
/**
* Generate the route to retrieve the faq
*
* @return string route
*/
public function getFaqUri()
{
return $this->getModuleKey() . '/' . $this->getPsVersion() . '/' . $this->getIsoCode();
}
/**
* @param string $moduleKey
*
* @return self
*/
public function setModuleKey($moduleKey)
{
$this->moduleKey = $moduleKey;
return $this;
}
/**
* @param string $psVersion
*
* @return self
*/
public function setPsVersion($psVersion)
{
$this->psVersion = $psVersion;
return $this;
}
/**
* @param string $isoCode
*
* @return self
*/
public function setIsoCode($isoCode)
{
$this->isoCode = $isoCode;
return $this;
}
/**
* @return string
*/
public function getIsoCode()
{
return $this->isoCode;
}
/**
* @return string
*/
public function getPsVersion()
{
return $this->psVersion;
}
/**
* @return string
*/
public function getModuleKey()
{
return $this->moduleKey;
}
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\ModuleLibFaq\Tests;
use Exception;
use GuzzleHttp\Psr7\Response as Psr7Response;
use Http\Client\Exception\TransferException;
use Http\Mock\Client;
use PHPUnit\Framework\TestCase;
use PrestaShop\ModuleLibFaq\Faq;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class FaqTest extends TestCase
{
/**
* This is not a unit test, as it runs the call to the marketplace
* Data for PS Checkout module
*/
public function testWorkingTest()
{
$faq = new Faq('82bc76354cfef947e06f1cc78f5efe2e', '1.7.5.2', 'fr');
$faq->setClient(new class() implements ClientInterface {
public function sendRequest(RequestInterface $request): ResponseInterface
{
return new Psr7Response(200, [], file_get_contents('tests/faqExample.json'));
}
});
$faqContent = $faq->getFaq();
$this->assertInternalType('array', $faqContent);
$this->assertTrue(count($faqContent) > 0);
}
/**
* Mock Client so we fake an HTTP error
*/
public function testCallableIsTriggered()
{
$faq = (new Faq('<InvalidKey>', '1.7.5.2', 'fr'));
$client = new Client();
$faq->setClient($client);
$exception = new TransferException('Server error response [url] https://api.addons.prestashop.com/request/faq/%3CInvalidKey%3E/1.7.5.2/fr [status code] 500 [reason phrase] Internal Server Error');
$client->addException($exception);
/**
* we create here a callable that will be triggered because of the error during the API call.
*
* It provides an example in which we retrieve the exception in the caller.
*/
$message = false;
$callable = function (Exception $e) use (&$message) {
$message = $e->getMessage();
};
$faq->setErrorCallable($callable);
/**
* Run test
*/
$faqContent = $faq->getFaq();
$this->assertSame(false, $faqContent);
$this->assertSame(
'Server error response [url] https://api.addons.prestashop.com/request/faq/%3CInvalidKey%3E/1.7.5.2/fr [status code] 500 [reason phrase] Internal Server Error',
$message
);
}
public function testCallableIsNotCalledByDefault()
{
$faq = (new Faq('<InvalidKey>', '1.7.5.2', 'fr'));
/*
* Mock of the API
*/
$faq->setClient(new class() implements ClientInterface {
public function sendRequest(RequestInterface $request): ResponseInterface
{
return new Psr7Response(200);
}
});
/**
* Create error callable
*/
$callable = function () {
throw new Exception('The callable should not be called!');
};
$faq->setErrorCallable($callable);
/**
* Run test
*/
$faqContent = $faq->getFaq();
$this->assertSame(false, $faqContent);
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\ModuleLibFaq\Tests;
use PHPUnit\Framework\TestCase;
use PrestaShop\ModuleLibFaq\Parameters;
class ParametersTest extends TestCase
{
public function testReturnsTheRouteApi()
{
$parameters = new Parameters();
$parameters->setIsoCode('fr')
->setModuleKey('someModuleKey')
->setPsVersion('1.7.5.2');
$this->assertSame('someModuleKey/1.7.5.2/fr', $parameters->getFaqUri());
}
}

View File

@@ -0,0 +1,129 @@
{
"id_faq": 117,
"module_key": "82bc76354cfef947e06f1cc78f5efe2e",
"id_product": 46347,
"categories": [
{
"id_faq_category": 136,
"position": 0,
"id_faq": 117,
"title": "",
"blocks": [
{
"id_faq_block": 516,
"question": "Pourquoi le moyen de paiement par Carte de Crédit est limité à 500$ ?",
"answer": "Si le moyen de paiement \"Carte de crédit & de débit\" est indiqué come \"Limité à 500$\", cela signifie que votre compte n'est pas totalement validé.\nVous avez surement reçu un mail pour vous le préciser : il faut fournir plus d'informations dans votre compte PayPal pour lever cette limite. Ces informations complémentaires peuvent être : une carte bancaire pour prélever de l'argent si besoin, un compte banciare à créditer si besoin, une pièce d'identité pour des vérifications suplémentaires, ... Rendez vous sur votre compte PayPal sur www.paypal.com et regardez les notifications sur la cloche en haut à droite et suivez les instructions.\n\nNe tardez pas, après avoir fourni ces informations supplémentaires, les équipes PayPal léveront la limite sous 72h pour vous permettre de recevoir des paiement par carte sans limité.\nAvant cette levée de limite vous ne pourrez pas retirer l'argent de votre compte ni en virer, vous pouvez uniquement créditer votre compte PayPal avec les paiements de vos clients.\nSi vous recevez plus que 500$ de commandes par carte, le moyen de paiement par carte sera désactivé jusqu'à validation totale de votre compte.\nEn attendant, vous pouvez toujours recevoir des paiements via PayPal sans limite.",
"version_min": "",
"version_max": ""
},
{
"id_faq_block": 519,
"question": "Comment activer les moyens de paiement locaux ?",
"answer": "La simple création d'un compte PayPal avec le module PrestaShop Checkout suffit pour activer les moyens de paiements locaux. Vous n'aurez pas besoin de créer d'autres comptes marchands pour chacun des moyens de paiement.",
"version_min": "",
"version_max": ""
},
{
"id_faq_block": 520,
"question": "Comment passer du module Paypal ou Stripe à PrestaShop Checkout ? ",
"answer": "\"1 - Dans l'espace \"\"Personnaliser\"\" dans l'interface dadministration de votre boutique, cliquez sur \"\"Paiement\"\" > \"\"Modes de paiement\"\".  L'ensemble des modules de paiement qui sont activés s'affichent.  Cherchez le module \"\"PrestaShop Checkout\"\" et cliquez sur \"\"Configurer\"\". \n\n2 - Suivez les étapes d'onboarding de PrestaShop Checkout : compte PrestaShop Checkout et compte PayPal\n\n3 - Quand les moyens de paiement sont activés (Stripe, Paypal ...) et que vous les retrouvez dans votre parcours d'achat de votre boutique, allez dans “Moyen de Paiement” de votre back-office et désactivez les autres modules de paiement.  \"\n",
"version_min": "",
"version_max": ""
},
{
"id_faq_block": 523,
"question": "Quelles sont les commissions du module “PrestaShop Checkout” ?",
"answer": "Ces tarifications sont disponibles via ce lien: https://www.prestashop.com/fr/prestashop-checkout ",
"version_min": "",
"version_max": ""
},
{
"id_faq_block": 524,
"question": "Je suis déjà marchand avec un compte PayPal business, puis-je garder ma tarification actuelle ?",
"answer": "Les tarifications de PrestaShop Checkout sont uniformisées pour tous les utilisateurs mais vous pouvez contacter PayPal pour ajuster la tarification à votre utilisation et conserver votre tarification actuelle.\n\nPour cela, allez dans la rubrique \"Contactez nous\" sur paypal.com pour communiquer avec un conseiller PayPal.",
"version_min": "",
"version_max": ""
},
{
"id_faq_block": 537,
"question": "Lorsque je lie mon compte PayPal, je clique sur Retourner à la boutique, la fenêtre PayPal se ferme mais rien ne se passe et mon compte PayPal n'est pas lié. Que puis-je faire ? ",
"answer": "Si le compte PayPal n'est pas lié, même après avoir fini le parcours de connexion PayPal, vérifiez que votre serveur ou back-office n'est protégé par un htaccess ou un VPN. Si c'est le cas, le module ne pourra pas recevoir de webhook pour mettre à jour votre identifiant PayPal et lier le compte.\nÇa ne devrait pas être le cas pour une configuration classique de serveur mais si c'est le cas, pas de souci, veuillez whitelister l'adresse IP pour l'URL suivante a minima : https://YOURSTORE.com/shop/index.php?fc=module&module=ps_checkout&controller=DispatchWebHook.\n\nL'adresse IP publique à whitelister si besoin : 35.195.228.149.",
"version_min": "",
"version_max": ""
}
]
},
{
"id_faq_category": 142,
"position": 1,
"id_faq": 117,
"title": "Questions à propos des paiements",
"blocks": [
{
"id_faq_block": 522,
"question": "Est-ce que mes clients verront le moyen de paiement PrestaShop Checkout ?",
"answer": "Non, PrestaShop Checkout est le nom du service et du module. Vos clients ne verront que les moyens de paiement qu'ils connaissent : par carte de crédit, par PayPal ou autre moyen de paiement incontournable dans leur pays.",
"version_min": "",
"version_max": ""
},
{
"id_faq_block": 525,
"question": "Pourquoi certains achats aboutissent dans l'état ",
"answer": "\"Les commandes passées dans une devise non configurée dans le compte PayPal sont soient converties automatiquement soient en attente de validation manuelle depuis l'interface PayPal, selon la configuration.\n\nSi vous souhaitez accepter automatiquement les achats de certaines devises sans les valider à la main vous pouvez soit :\n\n- Ajouter cette devise dans la liste des devises supportées par votre compte PayPal:\n\nDepuis votre compte PayPal (www.paypal.com) > Paramètres de Compte > Profil Professionnel > Argent, cartes et comptes bancaires\n\n- Activer les conversions automatiques :\n\nDepuis votre compte PayPal (www.paypal.com)Paramètres de Compte > Compte et Sécurité > Préférences de Paiement > Bloquer les paiements > Mettre à jour > Préférence de réception de paiements\"",
"version_min": "",
"version_max": ""
},
{
"id_faq_block": 527,
"question": "Quelles sont les commissions du module “PrestaShop Checkout” ?",
"answer": "Ces tarifications sont affichées via ce lien: https://www.prestashop.com/fr/prestashop-checkout \n\nCe sont les seules commissions existantes, appliquées seulement sur les transaction validées, il n'y a pas de coûts additionnels ou d'abonnement à payer à PayPal ou PrestaShop.",
"version_min": "",
"version_max": ""
},
{
"id_faq_block": 528,
"question": "PrestaShop Checkout est-il compatible avec les réglementations Européennes DSP ? ",
"answer": "PrestaShop Checkout est dès à présent compatible avec les réglementations DSP 2 entrant en vigueur en septembre.\n\nLe paiement par carte est compatible avec le 3DSecure et les clés digitales des différentes banques. \n",
"version_min": "",
"version_max": ""
},
{
"id_faq_block": 529,
"question": "Quels sont les différents statuts de transactions du module PrestaShop Checkout?",
"answer": "Une commande peut être :\n\n- En attente de paiement (par Carte Bancaire ou par PayPal)\n\n- Paiement accepté,\n\n- Remboursé,\n\n- Remboursement Partiel,\n\n- Erreur de Paiement.",
"version_min": "",
"version_max": ""
},
{
"id_faq_block": 530,
"question": "Est ce que le paiement par carte est compatible avec tous types de cartes et tous les navigateurs internet ?",
"answer": "Vos clients pourront finaliser leur commande quelque soit leur carte de crédit avec PrestaShop Checkout.\nDe plus, tous les navigateurs peuvent être utilisés par vos visiteurs, sauf Internet Explorer, qui n'est plus maintenu par Microsoft, et dont l'utilisation se fait de plus en plus rare.",
"version_min": "",
"version_max": ""
},
{
"id_faq_block": 536,
"question": "Comment sont mises à jours les commandes si la configuration de ma boutique crée des commandes ",
"answer": "Si certains paniers génèrent différentes commandes (pour répartir les produits sur différents transporteurs ou pour des raisons de stockage dans différents entrepôts par exemple) seule la commande \"parent\" sera mise à jour pour des changements de statut arrivant après la finalisation de commande. Il est possible de mettre à jour les commandes \"enfants\" à la main si besoin. ",
"version_min": "",
"version_max": ""
}
]
},
{
"id_faq_category": 141,
"position": 2,
"id_faq": 117,
"title": "Questions à propos des remboursements",
"blocks": [
{
"id_faq_block": 521,
"question": "J'essaye de faire un remboursement sur ma commande PrestaShop, mais un message d'erreur me dit que je n'ai pas les fonds suffisants ",
"answer": "Pour les remboursements d'une commande faite dans une monnaie différente que celles disponibles sur votre compte PayPal, vous avez 2 choix :\n\n- Ajouter toutes les monnaies de votre boutique dans votre compte PayPal, pour éviter de refuser les transactions cross-monnaie ou les problèmes de remboursements.\n\nPour cela, rendez-vous sur paypal.com > Login > Settings > My Money > Currencies management > Add a currency\n\n- Contacter PayPal et leur demander d'activer l'option de remboursement cross-monnaie. (onglet \"\"Nous contacter\"\" sur paypal.com).",
"version_min": "",
"version_max": ""
}
]
}
]
}

View File

@@ -0,0 +1,6 @@
parameters:
paths:
- ../../src
checkMissingIterableValueType: false
level: 7

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,52 @@
# PrestaShop module library for Guzzle clients
Plug modules to the Guzzle client available on a running shop.
This library is compatible with PHP 7.2 and above.
## Installation
```
composer require prestashop/module-lib-guzzle-adapter
```
## 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,41 @@
{
"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": "^7.2 || ^8.0",
"psr/http-client": "^1.0",
"psr/http-message": "^1.0",
"php-http/message": "^1.13",
"php-http/httplug": "^2.3",
"guzzlehttp/psr7": "^2.3.0"
},
"require-dev": {
"phpstan/phpstan": "^1.7",
"phpunit/phpunit": "^9.5",
"prestashop/php-dev-tools": "^4.2"
},
"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"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
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;
use Psr\Http\Client\ClientInterface;
class ClientFactory
{
/**
* @var VersionDetection
*/
private $versionDetection;
public function __construct(VersionDetection $versionDetection = null)
{
$this->versionDetection = $versionDetection ?: new VersionDetection();
}
/**
* @param array<string, mixed> $config
*/
public function getClient(array $config = []): ClientInterface
{
return $this->initClient($config);
}
/**
* @param array<string, mixed> $config
*/
private function initClient(array $config = []): ClientInterface
{
if ($this->versionDetection->getGuzzleMajorVersionNumber() >= 7) {
return Guzzle7Client::createWithConfig(
Guzzle7Config::fixConfig($config)
);
}
return Guzzle5Client::createWithConfig(
Guzzle5Config::fixConfig($config)
);
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Prestashop\ModuleLibGuzzleAdapter;
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): array;
}

View File

@@ -0,0 +1,142 @@
<?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 Psr\Http\Client\ClientInterface as ClientClientInterface;
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 ClientClientInterface
{
/**
* @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
*/
public static function createWithConfig(array $config): Client
{
return new self(new GuzzleClient($config));
}
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request): ResponseInterface
{
$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,39 @@
<?php
declare(strict_types=1);
namespace Prestashop\ModuleLibGuzzleAdapter\Guzzle5;
use Prestashop\ModuleLibGuzzleAdapter\ConfigInterface;
class Config implements ConfigInterface
{
/**
* {@inheritdoc}
*/
public static function fixConfig(array $config): array
{
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,80 @@
<?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 Psr\Http\Client\ClientInterface as ClientClientInterface;
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 ClientClientInterface
{
/**
* @var ClientInterface
*/
private $guzzle;
public function __construct(?ClientInterface $guzzle = null)
{
if (!$guzzle) {
$guzzle = self::buildClient();
}
$this->guzzle = $guzzle;
}
/**
* Factory method to create the Guzzle 7 adapter with custom Guzzle configuration.
*
* @param array<string, mixed> $config
*/
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->guzzle->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\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,25 @@
<?php
declare(strict_types=1);
namespace Prestashop\ModuleLibGuzzleAdapter;
class VersionDetection
{
public function getGuzzleMajorVersionNumber(): ?int
{
// 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;
}
}

View File

@@ -0,0 +1,5 @@
{
"require-dev": {
"guzzlehttp/guzzle": "^5.3.4"
}
}

View File

@@ -0,0 +1,263 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "05f2fd0a83b63c86dda2ebc0cf422d8f",
"packages": [],
"packages-dev": [
{
"name": "guzzlehttp/guzzle",
"version": "5.3.4",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "b87eda7a7162f95574032da17e9323c9899cb6b2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/b87eda7a7162f95574032da17e9323c9899cb6b2",
"reference": "b87eda7a7162f95574032da17e9323c9899cb6b2",
"shasum": ""
},
"require": {
"guzzlehttp/ringphp": "^1.1",
"php": ">=5.4.0",
"react/promise": "^2.2"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "^4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"GuzzleHttp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients",
"homepage": "http://guzzlephp.org/",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"rest",
"web service"
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/5.3"
},
"time": "2019-10-30T09:32:00+00:00"
},
{
"name": "guzzlehttp/ringphp",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/RingPHP.git",
"reference": "5e2a174052995663dd68e6b5ad838afd47dd615b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/RingPHP/zipball/5e2a174052995663dd68e6b5ad838afd47dd615b",
"reference": "5e2a174052995663dd68e6b5ad838afd47dd615b",
"shasum": ""
},
"require": {
"guzzlehttp/streams": "~3.0",
"php": ">=5.4.0",
"react/promise": "~2.0"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "~4.0"
},
"suggest": {
"ext-curl": "Guzzle will use specific adapters if cURL is present"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Ring\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.",
"support": {
"issues": "https://github.com/guzzle/RingPHP/issues",
"source": "https://github.com/guzzle/RingPHP/tree/1.1.1"
},
"abandoned": true,
"time": "2018-07-31T13:22:33+00:00"
},
{
"name": "guzzlehttp/streams",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/streams.git",
"reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/streams/zipball/47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5",
"reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Stream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Provides a simple abstraction over streams of data",
"homepage": "http://guzzlephp.org/",
"keywords": [
"Guzzle",
"stream"
],
"support": {
"issues": "https://github.com/guzzle/streams/issues",
"source": "https://github.com/guzzle/streams/tree/master"
},
"abandoned": true,
"time": "2014-10-12T19:18:40+00:00"
},
{
"name": "react/promise",
"version": "v2.9.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise.git",
"reference": "234f8fd1023c9158e2314fa9d7d0e6a83db42910"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/promise/zipball/234f8fd1023c9158e2314fa9d7d0e6a83db42910",
"reference": "234f8fd1023c9158e2314fa9d7d0e6a83db42910",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36"
},
"type": "library",
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"React\\Promise\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "A lightweight implementation of CommonJS Promises/A for PHP",
"keywords": [
"promise",
"promises"
],
"support": {
"issues": "https://github.com/reactphp/promise/issues",
"source": "https://github.com/reactphp/promise/tree/v2.9.0"
},
"funding": [
{
"url": "https://github.com/WyriHaximus",
"type": "github"
},
{
"url": "https://github.com/clue",
"type": "github"
}
],
"time": "2022-02-11T10:27:51+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

Some files were not shown because too many files have changed in this diff Show More