Completed
Push — master ( 9c21b6...43c791 )
by David
04:48
created

PluginClient::sendRequest()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.1406

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 6
cts 8
cp 0.75
rs 9.6333
c 0
b 0
f 0
cc 3
nc 2
nop 1
crap 3.1406
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Http\Client\Common;
6
7
use Http\Client\Common\Exception\LoopException;
8
use Http\Client\Exception as HttplugException;
9
use Http\Client\HttpAsyncClient;
10
use Http\Client\HttpClient;
11
use Http\Client\Promise\HttpFulfilledPromise;
12
use Http\Client\Promise\HttpRejectedPromise;
13
use Psr\Http\Client\ClientInterface;
14
use Psr\Http\Message\RequestInterface;
15
use Psr\Http\Message\ResponseInterface;
16
use Symfony\Component\OptionsResolver\OptionsResolver;
17
18
/**
19
 * The client managing plugins and providing a decorator around HTTP Clients.
20
 *
21
 * @author Joel Wurtz <[email protected]>
22
 */
23
final class PluginClient implements HttpClient, HttpAsyncClient
24
{
25
    /**
26
     * An HTTP async client.
27
     *
28
     * @var HttpAsyncClient
29
     */
30
    private $client;
31
32
    /**
33
     * The plugin chain.
34
     *
35
     * @var Plugin[]
36
     */
37
    private $plugins;
38
39
    /**
40
     * A list of options.
41
     *
42
     * @var array
43
     */
44
    private $options;
45
46
    /**
47
     * @param ClientInterface|HttpAsyncClient $client
48
     * @param Plugin[]                        $plugins
49
     * @param array                           $options {
50
     *
51
     *     @var int      $max_restarts
52
     * }
53
     *
54
     * @throws \RuntimeException if client is not an instance of HttpClient or HttpAsyncClient
55
     */
56 10
    public function __construct($client, array $plugins = [], array $options = [])
57
    {
58 10
        if ($client instanceof HttpAsyncClient) {
59 3
            $this->client = $client;
60 7
        } elseif ($client instanceof ClientInterface) {
61 7
            $this->client = new EmulatedHttpAsyncClient($client);
62
        } else {
63
            throw new \RuntimeException('Client must be an instance of Psr\\Http\\Client\\ClientInterface or Http\\Client\\HttpAsyncClient');
64
        }
65
66 10
        $this->plugins = $plugins;
67 10
        $this->options = $this->configure($options);
68 10
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73 4
    public function sendRequest(RequestInterface $request): ResponseInterface
74
    {
75
        // If we don't have an http client, use the async call
76 4
        if (!($this->client instanceof HttpClient)) {
77 1
            return $this->sendAsyncRequest($request)->wait();
78
        }
79
80
        // Else we want to use the synchronous call of the underlying client, and not the async one in the case
81
        // we have both an async and sync call
82
        $pluginChain = $this->createPluginChain($this->plugins, function (RequestInterface $request) {
83
            try {
84 2
                return new HttpFulfilledPromise($this->client->sendRequest($request));
85
            } catch (HttplugException $exception) {
86
                return new HttpRejectedPromise($exception);
87
            }
88 3
        });
89
90 3
        return $pluginChain($request)->wait();
91
    }
92
93
    /**
94
     * {@inheritdoc}
95
     */
96 2
    public function sendAsyncRequest(RequestInterface $request)
97
    {
98
        $pluginChain = $this->createPluginChain($this->plugins, function (RequestInterface $request) {
99 2
            return $this->client->sendAsyncRequest($request);
100 2
        });
101
102 2
        return $pluginChain($request);
103
    }
104
105
    /**
106
     * Configure the plugin client.
107
     */
108 10
    private function configure(array $options = []): array
109
    {
110 10
        $resolver = new OptionsResolver();
111 10
        $resolver->setDefaults([
112 10
            'max_restarts' => 10,
113
        ]);
114
115 10
        $resolver->setAllowedTypes('max_restarts', 'int');
116
117 10
        return $resolver->resolve($options);
118
    }
119
120
    /**
121
     * Create the plugin chain.
122
     *
123
     * @param Plugin[] $pluginList     A list of plugins
124
     * @param callable $clientCallable Callable making the HTTP call
125
     */
126 5
    private function createPluginChain(array $pluginList, callable $clientCallable): callable
127
    {
128 5
        $firstCallable = $lastCallable = $clientCallable;
129
130 5
        while ($plugin = array_pop($pluginList)) {
131
            $lastCallable = function (RequestInterface $request) use ($plugin, $lastCallable, &$firstCallable) {
132 1
                return $plugin->handleRequest($request, $lastCallable, $firstCallable);
133 1
            };
134
135 1
            $firstCallable = $lastCallable;
136
        }
137
138 5
        $firstCalls = 0;
139
        $firstCallable = function (RequestInterface $request) use ($lastCallable, &$firstCalls) {
140 5
            if ($firstCalls > $this->options['max_restarts']) {
141 1
                throw new LoopException('Too many restarts in plugin client', $request);
142
            }
143
144 5
            ++$firstCalls;
145
146 5
            return $lastCallable($request);
147 5
        };
148
149 5
        return $firstCallable;
150
    }
151
}
152