Skip to content
This repository has been archived by the owner on Aug 17, 2022. It is now read-only.

Commit

Permalink
Merge pull request #32 from loveOSS/introduce-sf-http-client
Browse files Browse the repository at this point in the history
Implemented Symfony 4 HTTP client adapter
  • Loading branch information
mickaelandrieu authored Sep 20, 2019
2 parents 411abb0 + 6820f1e commit 9c1a93b
Show file tree
Hide file tree
Showing 14 changed files with 199 additions and 50 deletions.
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ php:

matrix:
fast_finish: true
allow_failures:
- php: 7.3

cache:
directories:
Expand Down
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,10 @@ You need to configure a system for the Circuit Breaker:
use Resiliency\MainCircuitBreaker;
use Resiliency\Systems\MainSystem;
use Resiliency\Storages\SimpleArray;
use Resiliency\Clients\GuzzleClient;
use Resiliency\Clients\SymfonyClient;
use Symfony\Component\HttpClient\HttpClient;

$client = new GuzzleClient([
'proxy' => '192.168.16.1:10',
'method' => 'POST',
]);
$client = new SymfonyClient(HttpClient::create());

$mainSystem = MainSystem::createFromArray([
'failures' => 2,
Expand Down Expand Up @@ -72,24 +70,31 @@ $circuitBreaker->call(
);
```

### Clients

Since v0.6, Resiliency library supports both Guzzle 6 and HttpClient Component from Symfony.

> For the Guzzle implementation, the Client options are described
> in the [HttpGuzzle documentation](http://docs.guzzlephp.org/en/stable/index.html).
> For the Symfony implementation, the Client options are described
> in the [HttpClient Component documentation](https://symfony.com/doc/current/components/http_client.html).
### Monitoring

This library is shipped with a minimalist system to help you monitor your circuits.

```php
$monitor = new SimpleMonitor();

// on some circuit breaker events...
// Collect information while listening
// to some circuit breaker events...
function listener(Event $event) {
$monitor->add($event);
};

// retrieve a complete report for analysis or storage
// Retrieve a complete report for analysis or storage
$report = $monitor->getReport();

```

## Tests
Expand All @@ -100,6 +105,8 @@ composer test

## Code quality

This library have high quality standards:

```
composer cs-fix && composer phpstan && composer psalm
```
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@
"phpunit/phpunit": "^8.0",
"sensiolabs/security-checker": "^5.0",
"symfony/cache": "~3.4|~4.3",
"symfony/http-client": "^4.3",
"vimeo/psalm": "^3.4"
},
"suggest": {
"symfony/cache": "Allows use of Symfony Cache adapters to store transactions",
"ext-apcu": "Allows use of APCu adapter (performant) to store transactions",
"guzzlehttp/guzzle": "Allows use of Guzzle 6 HTTP Client"
"guzzlehttp/guzzle": "Allows use of Guzzle 6 HTTP Client",
"symfony/http-client": "Allows use of any Symfony HTTP Clients"
},
"autoload": {
"psr-4": {
Expand Down
43 changes: 43 additions & 0 deletions src/Clients/ClientHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Resiliency\Clients;

use Resiliency\Contracts\Client;
use Resiliency\Contracts\Place;
use Resiliency\Contracts\Service;

abstract class ClientHelper implements Client
{
/**
* @var array the Client main options
*/
protected $mainOptions;

public function __construct(array $mainOptions = [])
{
$this->mainOptions = $mainOptions;
}

/**
* @param array $options the list of options
*
* @return string the method
*/
protected function defineMethod(array $options): string
{
if (isset($this->mainOptions['method'])) {
return (string) $this->mainOptions['method'];
}

if (isset($options['method'])) {
return (string) $options['method'];
}

return self::DEFAULT_METHOD;
}

/**
* {@inheritdoc}
*/
abstract public function request(Service $service, Place $place): string;
}
31 changes: 1 addition & 30 deletions src/Clients/GuzzleClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,15 @@
use GuzzleHttp\Client as OriginalGuzzleClient;
use Resiliency\Exceptions\UnavailableService;
use Resiliency\Contracts\Service;
use Resiliency\Contracts\Client;
use Resiliency\Contracts\Place;
use Exception;

/**
* Guzzle implementation of client.
* The possibility of extending this client is intended.
*/
class GuzzleClient implements Client
class GuzzleClient extends ClientHelper
{
/**
* @var array the Client main options
*/
private $mainOptions;

public function __construct(array $mainOptions = [])
{
$this->mainOptions = $mainOptions;
}

/**
* {@inheritdoc}
*/
Expand All @@ -49,22 +38,4 @@ public function request(Service $service, Place $place): string
);
}
}

/**
* @param array $options the list of options
*
* @return string the method
*/
private function defineMethod(array $options): string
{
if (isset($this->mainOptions['method'])) {
return (string) $this->mainOptions['method'];
}

if (isset($options['method'])) {
return (string) $options['method'];
}

return self::DEFAULT_METHOD;
}
}
53 changes: 53 additions & 0 deletions src/Clients/SymfonyClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Resiliency\Clients;

use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Resiliency\Exceptions\UnavailableService;
use Resiliency\Contracts\Service;
use Resiliency\Contracts\Place;

/**
* Symfony implementation of client.
* The possibility of extending this client is intended.
* /!\ The HttpClient of Symfony is experimental.
*/
class SymfonyClient extends ClientHelper
{
/**
* @var HttpClientInterface the Symfony HTTP client
*/
private $httpClient;

public function __construct(
HttpClientInterface $httpClient,
array $mainOptions = []
) {
$this->httpClient = $httpClient;
parent::__construct($mainOptions);
}

/**
* {@inheritdoc}
*/
public function request(Service $service, Place $place): string
{
$options = [];
try {
$method = $this->defineMethod($service->getParameters());
$options['timeout'] = $place->getTimeout();

$clientParameters = array_merge($service->getParameters(), $options);
unset($clientParameters['method']);

return $this->httpClient->request($method, $service->getURI(), $clientParameters)->getContent();
} catch (TransportExceptionInterface $exception) {
throw new UnavailableService(
$exception->getMessage(),
(int) $exception->getCode(),
$exception
);
}
}
}
2 changes: 1 addition & 1 deletion src/Places/Closed.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* The circuit-breaker executes actions placed through it, measuring the failures and successes of those actions.
* If the failures exceed a certain threshold, the circuit will break (open).
*/
final class Closed extends AbstractPlace
final class Closed extends PlaceHelper
{
/**
* @var Client the client
Expand Down
2 changes: 1 addition & 1 deletion src/Places/HalfOpened.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*
* If the call throws no exception, the circuit transitions back to closed.
*/
final class HalfOpened extends AbstractPlace
final class HalfOpened extends PlaceHelper
{
/**
* @var Client the client
Expand Down
2 changes: 1 addition & 1 deletion src/Places/Isolated.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* This state is manually triggered to ensure the Circuit Breaker
* remains open until we reset it.
*/
class Isolated extends AbstractPlace
class Isolated extends PlaceHelper
{
public function __construct()
{
Expand Down
2 changes: 1 addition & 1 deletion src/Places/Opened.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* While the circuit is in an open state: every call to the service
* won't be executed and the fallback callback is executed.
*/
final class Opened extends AbstractPlace
final class Opened extends PlaceHelper
{
/**
* @param float $threshold the Place threshold
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use Resiliency\Utils\Assert;
use DateTime;

abstract class AbstractPlace implements Place
abstract class PlaceHelper implements Place
{
/**
* @var int the Place failures
Expand Down
6 changes: 3 additions & 3 deletions tests/Clients/GuzzleClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@

class GuzzleClientTest extends CircuitBreakerTestCase
{
public function testRequestWorksAsExpected()
public function testRequestWorksAsExpected(): void
{
$client = new GuzzleClient();
$service = $this->getService('https://www.google.com', ['method' => 'GET']);

self::assertNotNull($client->request($service, $this->createMock(Place::class)));
}

public function testWrongRequestThrowsAnException()
public function testWrongRequestThrowsAnException(): void
{
$this->expectException(UnavailableService::class);

Expand All @@ -27,7 +27,7 @@ public function testWrongRequestThrowsAnException()
$client->request($service, $this->createMock(Place::class));
}

public function testTheClientAcceptsHttpMethodOverride()
public function testTheClientAcceptsHttpMethodOverride(): void
{
$client = new GuzzleClient([
'method' => 'HEAD',
Expand Down
74 changes: 74 additions & 0 deletions tests/Clients/SymfonyClientTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace Tests\Resiliency\Clients;

use Resiliency\Contracts\Place;
use Resiliency\Clients\SymfonyClient;
use Tests\Resiliency\CircuitBreakerTestCase;
use Resiliency\Exceptions\UnavailableService;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Component\HttpClient\Response\MockResponse;

class SymfonyClientTest extends CircuitBreakerTestCase
{
public function testRequestWorksAsExpected(): void
{
$client = new SymfonyClient($this->getClient());
$service = $this->getService('https://www.google.com', ['method' => 'GET']);

self::assertNotNull($client->request($service, $this->getPlace()));
}

public function testWrongRequestThrowsAnException(): void
{
$this->expectException(UnavailableService::class);

$client = new SymfonyClient($this->getClient());
$service = $this->getService('http://not-even-a-valid-domain.xxx');

$client->request($service, $this->getPlace());
}

public function testTheClientAcceptsHttpMethodOverride(): void
{
$client = new SymfonyClient($this->getClient(), [
'method' => 'HEAD',
]);

$service = $this->getService('https://www.google.com');

self::assertSame(
'',
$client->request(
$service,
$this->getPlace()
)
);
}

private function getClient(): HttpClientInterface
{
$callback = function ($method, $url, $options) {
if ($url === 'http://not-even-a-valid-domain.xxx/') {
return new MockResponse('', ['error' => 'Unavailable']);
}

if ($method === 'HEAD') {
return new MockResponse('');
}

return new MockResponse('mocked');
};

return new MockHttpClient($callback);
}

private function getPlace(): Place
{
$placeMock = $this->createMock(Place::class);
$placeMock->method('getTimeout')->willReturn(2.0);

return $placeMock;
}
}
3 changes: 2 additions & 1 deletion tests/Monitors/SimpleMonitorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public function testCreation()
}

/**
* @covers SimpleMonitor::collect
* @covers \SimpleMonitor::collect
*
* @return void
*/
public function testCollect()
Expand Down

0 comments on commit 9c1a93b

Please sign in to comment.