Completed
Push — master ( 955d05...19c28e )
by David
03:37
created

src/PluginClient.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\Client\ClientInterface;
12
use Psr\Http\Message\RequestInterface;
13
use Symfony\Component\OptionsResolver\OptionsResolver;
14
15
/**
16
 * The client managing plugins and providing a decorator around HTTP Clients.
17
 *
18
 * @author Joel Wurtz <[email protected]>
19
 */
20
final class PluginClient implements HttpClient, HttpAsyncClient
21
{
22
    /**
23
     * An HTTP async client.
24
     *
25
     * @var HttpAsyncClient
26
     */
27
    private $client;
28
29
    /**
30
     * The plugin chain.
31
     *
32
     * @var Plugin[]
33
     */
34
    private $plugins;
35
36
    /**
37
     * A list of options.
38
     *
39
     * @var array
40
     */
41
    private $options;
42
43
    /**
44
     * @param HttpClient|HttpAsyncClient $client
45
     * @param Plugin[]                   $plugins
46
     * @param array                      $options {
47
     *
48
     *     @var int      $max_restarts
49
     *     @var Plugin[] $debug_plugins an array of plugins that are injected between each normal plugin
50
     * }
51
     *
52
     * @throws \RuntimeException if client is not an instance of HttpClient or HttpAsyncClient
53
     */
54 11
    public function __construct($client, array $plugins = [], array $options = [])
55
    {
56 11
        if ($client instanceof HttpAsyncClient) {
57 3
            $this->client = $client;
58 8
        } elseif ($client instanceof HttpClient || $client instanceof ClientInterface) {
59 8
            $this->client = new EmulatedHttpAsyncClient($client);
60
        } else {
61
            throw new \RuntimeException('Client must be an instance of Http\\Client\\HttpClient or Http\\Client\\HttpAsyncClient');
62
        }
63
64 11
        $this->plugins = $plugins;
65 11
        $this->options = $this->configure($options);
66 11
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71 5
    public function sendRequest(RequestInterface $request)
72
    {
73
        // If we don't have an http client, use the async call
74 5
        if (!($this->client instanceof HttpClient)) {
75 1
            return $this->sendAsyncRequest($request)->wait();
76
        }
77
78
        // Else we want to use the synchronous call of the underlying client, and not the async one in the case
79
        // we have both an async and sync call
80
        $pluginChain = $this->createPluginChain($this->plugins, function (RequestInterface $request) {
81
            try {
82 3
                return new HttpFulfilledPromise($this->client->sendRequest($request));
83
            } catch (HttplugException $exception) {
84
                return new HttpRejectedPromise($exception);
85
            }
86 4
        });
87
88 4
        return $pluginChain($request)->wait();
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94 2
    public function sendAsyncRequest(RequestInterface $request)
95
    {
96
        $pluginChain = $this->createPluginChain($this->plugins, function (RequestInterface $request) {
97 2
            return $this->client->sendAsyncRequest($request);
98 2
        });
99
100 2
        return $pluginChain($request);
101
    }
102
103
    /**
104
     * Configure the plugin client.
105
     *
106
     * @param array $options
107
     *
108
     * @return array
109
     */
110 11
    private function configure(array $options = [])
111
    {
112 11
        if (isset($options['debug_plugins'])) {
113 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...
114
        }
115
116 11
        $resolver = new OptionsResolver();
117 11
        $resolver->setDefaults([
118 11
            'max_restarts' => 10,
119
            'debug_plugins' => [],
120
        ]);
121
122
        $resolver
123 11
            ->setAllowedTypes('debug_plugins', 'array')
124
            ->setAllowedValues('debug_plugins', function (array $plugins) {
125 11
                foreach ($plugins as $plugin) {
126
                    // Make sure each object passed with the `debug_plugins` is an instance of Plugin.
127 1
                    if (!$plugin instanceof Plugin) {
128 1
                        return false;
129
                    }
130
                }
131
132 11
                return true;
133 11
            });
134
135 11
        return $resolver->resolve($options);
136
    }
137
138
    /**
139
     * Create the plugin chain.
140
     *
141
     * @param Plugin[] $pluginList     A list of plugins
142
     * @param callable $clientCallable Callable making the HTTP call
143
     *
144
     * @return callable
145
     */
146 6
    private function createPluginChain($pluginList, callable $clientCallable)
147
    {
148 6
        $firstCallable = $lastCallable = $clientCallable;
149
150
        /*
151
         * Inject debug plugins between each plugin.
152
         */
153 6
        $pluginListWithDebug = $this->options['debug_plugins'];
154 6
        foreach ($pluginList as $plugin) {
155 2
            $pluginListWithDebug[] = $plugin;
156 2
            $pluginListWithDebug = array_merge($pluginListWithDebug, $this->options['debug_plugins']);
157
        }
158
159 6
        while ($plugin = array_pop($pluginListWithDebug)) {
160
            $lastCallable = function (RequestInterface $request) use ($plugin, $lastCallable, &$firstCallable) {
161 2
                return $plugin->handleRequest($request, $lastCallable, $firstCallable);
162 2
            };
163
164 2
            $firstCallable = $lastCallable;
165
        }
166
167 6
        $firstCalls = 0;
168 6
        $firstCallable = function (RequestInterface $request) use ($lastCallable, &$firstCalls) {
169 6
            if ($firstCalls > $this->options['max_restarts']) {
170 1
                throw new LoopException('Too many restarts in plugin client', $request);
171
            }
172
173 6
            ++$firstCalls;
174
175 6
            return $lastCallable($request);
176 6
        };
177
178 6
        return $firstCallable;
179
    }
180
}
181