Completed
Push — master ( 96c43b...fcf598 )
by Tobias
03:54
created

PluginClient   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 161
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 94.23%

Importance

Changes 0
Metric Value
wmc 15
lcom 1
cbo 7
dl 0
loc 161
ccs 49
cts 52
cp 0.9423
rs 10
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A sendRequest() 0 19 3
A sendAsyncRequest() 0 8 1
A __construct() 0 13 3
B configure() 0 27 4
B createPluginChain() 0 34 4
1
<?php
2
3
namespace Http\Client\Common;
4
5
use Http\Client\Common\Exception\LoopException;
6
use Http\Client\Exception as HttplugException;
7
use Http\Client\HttpAsyncClient;
8
use Http\Client\HttpClient;
9
use Http\Client\Promise\HttpFulfilledPromise;
10
use Http\Client\Promise\HttpRejectedPromise;
11
use Psr\Http\Message\RequestInterface;
12
use Symfony\Component\OptionsResolver\OptionsResolver;
13
14
/**
15
 * The client managing plugins and providing a decorator around HTTP Clients.
16
 *
17
 * @author Joel Wurtz <[email protected]>
18
 */
19
final class PluginClient implements HttpClient, HttpAsyncClient
20
{
21
    /**
22
     * An HTTP async client.
23
     *
24
     * @var HttpAsyncClient
25
     */
26
    private $client;
27
28
    /**
29
     * The plugin chain.
30
     *
31
     * @var Plugin[]
32
     */
33
    private $plugins;
34
35
    /**
36
     * A list of options.
37
     *
38
     * @var array
39
     */
40
    private $options;
41
42
    /**
43
     * @param HttpClient|HttpAsyncClient $client
44
     * @param Plugin[]                   $plugins
45
     * @param array                      $options {
46
     *
47
     *     @var int      $max_restarts
48
     *     @var Plugin[] $debug_plugins an array of plugins that are injected between each normal plugin
49
     * }
50
     *
51
     * @throws \RuntimeException if client is not an instance of HttpClient or HttpAsyncClient
52
     */
53 11
    public function __construct($client, array $plugins = [], array $options = [])
54
    {
55 11
        if ($client instanceof HttpAsyncClient) {
56 3
            $this->client = $client;
57 8
        } elseif ($client instanceof HttpClient) {
58 8
            $this->client = new EmulatedHttpAsyncClient($client);
59
        } else {
60
            throw new \RuntimeException('Client must be an instance of Http\\Client\\HttpClient or Http\\Client\\HttpAsyncClient');
61
        }
62
63 11
        $this->plugins = $plugins;
64 11
        $this->options = $this->configure($options);
65 11
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70 5
    public function sendRequest(RequestInterface $request)
71
    {
72
        // If we don't have an http client, use the async call
73 5
        if (!($this->client instanceof HttpClient)) {
74 1
            return $this->sendAsyncRequest($request)->wait();
75
        }
76
77
        // Else we want to use the synchronous call of the underlying client, and not the async one in the case
78
        // we have both an async and sync call
79
        $pluginChain = $this->createPluginChain($this->plugins, function (RequestInterface $request) {
80
            try {
81 3
                return new HttpFulfilledPromise($this->client->sendRequest($request));
82
            } catch (HttplugException $exception) {
83
                return new HttpRejectedPromise($exception);
84
            }
85 4
        });
86
87 4
        return $pluginChain($request)->wait();
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93 2
    public function sendAsyncRequest(RequestInterface $request)
94
    {
95
        $pluginChain = $this->createPluginChain($this->plugins, function (RequestInterface $request) {
96 2
            return $this->client->sendAsyncRequest($request);
97 2
        });
98
99 2
        return $pluginChain($request);
100
    }
101
102
    /**
103
     * Configure the plugin client.
104
     *
105
     * @param array $options
106
     *
107
     * @return array
108
     */
109 11
    private function configure(array $options = [])
110
    {
111 11
        if (isset($options['debug_plugins'])) {
112 1
            @trigger_error('The "debug_plugins" option is deprecated since 1.5 and will be removed in 2.0.', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
113
        }
114
115 11
        $resolver = new OptionsResolver();
116 11
        $resolver->setDefaults([
117 11
            'max_restarts' => 10,
118
            'debug_plugins' => [],
119
        ]);
120
121
        $resolver
122 11
            ->setAllowedTypes('debug_plugins', 'array')
123
            ->setAllowedValues('debug_plugins', function (array $plugins) {
124 11
                foreach ($plugins as $plugin) {
125
                    // Make sure each object passed with the `debug_plugins` is an instance of Plugin.
126 1
                    if (!$plugin instanceof Plugin) {
127 1
                        return false;
128
                    }
129
                }
130
131 11
                return true;
132 11
            });
133
134 11
        return $resolver->resolve($options);
135
    }
136
137
    /**
138
     * Create the plugin chain.
139
     *
140
     * @param Plugin[] $pluginList     A list of plugins
141
     * @param callable $clientCallable Callable making the HTTP call
142
     *
143
     * @return callable
144
     */
145 6
    private function createPluginChain($pluginList, callable $clientCallable)
146
    {
147 6
        $firstCallable = $lastCallable = $clientCallable;
148
149
        /*
150
         * Inject debug plugins between each plugin.
151
         */
152 6
        $pluginListWithDebug = $this->options['debug_plugins'];
153 6
        foreach ($pluginList as $plugin) {
154 2
            $pluginListWithDebug[] = $plugin;
155 2
            $pluginListWithDebug = array_merge($pluginListWithDebug, $this->options['debug_plugins']);
156
        }
157
158 6
        while ($plugin = array_pop($pluginListWithDebug)) {
159
            $lastCallable = function (RequestInterface $request) use ($plugin, $lastCallable, &$firstCallable) {
160 2
                return $plugin->handleRequest($request, $lastCallable, $firstCallable);
161 2
            };
162
163 2
            $firstCallable = $lastCallable;
164
        }
165
166 6
        $firstCalls = 0;
167 6
        $firstCallable = function (RequestInterface $request) use ($lastCallable, &$firstCalls) {
168 6
            if ($firstCalls > $this->options['max_restarts']) {
169 1
                throw new LoopException('Too many restarts in plugin client', $request);
170
            }
171
172 6
            ++$firstCalls;
173
174 6
            return $lastCallable($request);
175 6
        };
176
177 6
        return $firstCallable;
178
    }
179
}
180