ServiceClient   A
last analyzed

Complexity

Total Complexity 28

Size/Duplication

Total Lines 257
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 10

Test Coverage

Coverage 53.49%

Importance

Changes 0
Metric Value
wmc 28
lcom 2
cbo 10
dl 0
loc 257
ccs 46
cts 86
cp 0.5349
rs 10
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 4
A getHttpClient() 0 4 1
A getHandlerStack() 0 4 1
A getCommand() 0 4 1
A execute() 0 4 1
A executeAsync() 0 7 2
B executeAll() 0 26 3
B executeAllAsync() 0 22 4
A __call() 0 11 3
B createCommandHandler() 0 25 5
A transformCommandToRequest() 0 6 1
A transformResponseToResult() 0 9 1
A parseBadResponseException() 0 6 1
1
<?php declare(strict_types=1);
2
3
/*
4
 * This file is part of indragunawan/rest-service package.
5
 *
6
 * (c) Indra Gunawan <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace IndraGunawan\RestService;
13
14
use GuzzleHttp\Client as HttpClient;
15
use GuzzleHttp\Command\Command;
16
use GuzzleHttp\Command\CommandInterface;
17
use GuzzleHttp\Command\ServiceClientInterface;
18
use GuzzleHttp\Exception\BadResponseException;
19
use GuzzleHttp\HandlerStack;
20
use GuzzleHttp\Promise;
21
use GuzzleHttp\Promise\PromiseInterface;
22
use IndraGunawan\RestService\Exception\BadRequestException;
23
use IndraGunawan\RestService\Exception\CommandException;
24
use IndraGunawan\RestService\Exception\ValidatorException;
25
use Psr\Http\Message\RequestInterface;
26
use Psr\Http\Message\ResponseInterface;
27
28
/**
29
 * The Guzzle ServiceClient serves as the foundation for creating web service
30
 * clients that interact with RPC-style APIs.
31
 */
32
class ServiceClient implements ServiceClientInterface
33
{
34
    /**
35
     * @var HttpClient HTTP client used to send requests
36
     */
37
    private $httpClient;
38
39
    /**
40
     * @var HandlerStack
41
     */
42
    private $handlerStack;
43
44
    /**
45
     * @var callable
46
     */
47
    private $commandToRequestTransformer;
48
49
    /**
50
     * @var callable
51
     */
52
    private $responseToResultTransformer;
53
54
    /**
55
     * @var callable
56
     */
57
    private $badResponseExceptionParser;
58
59
    /**
60
     * @param string $specificationFile
61
     * @param array  $config
62
     * @param string $cacheDir
63
     * @param bool   $debug
64
     *
65
     * @throws \IndraGunawan\RestService\Exception\InvalidSpecificationException
66
     */
67 2
    public function __construct($specificationFile, array $config = [], $cacheDir = null, $debug = false)
68
    {
69 2
        $this->httpClient = new HttpClient(isset($config['httpClient']) ? $config['httpClient'] : []);
70 2
        $this->handlerStack = new HandlerStack();
71 2
        $this->handlerStack->setHandler($this->createCommandHandler());
72
73 2
        $service = new Service(
74 2
            $specificationFile,
75 2
            (isset($config['defaults']) && is_array($config['defaults'])) ? $config['defaults'] : [],
76 2
            $cacheDir,
77 2
            $debug
78
        );
79
80 2
        $builder = new Builder($service);
81
82 2
        $this->commandToRequestTransformer = $builder->commandToRequestTransformer();
83 2
        $this->responseToResultTransformer = $builder->responseToResultTransformer();
84 2
        $this->badResponseExceptionParser = $builder->badResponseExceptionParser();
85 2
    }
86
87
    public function getHttpClient()
88
    {
89
        return $this->httpClient;
90
    }
91
92
    public function getHandlerStack()
93
    {
94
        return $this->handlerStack;
95
    }
96
97
    /**
98
     * {@inheritdoc}
99
     */
100 2
    public function getCommand($name, array $params = [])
101
    {
102 2
        return new Command($name, $params, clone $this->handlerStack);
103
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108 2
    public function execute(CommandInterface $command)
109
    {
110 2
        return $this->executeAsync($command)->wait();
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116 2
    public function executeAsync(CommandInterface $command)
117
    {
118 2
        $stack = $command->getHandlerStack() ?: $this->handlerStack;
119 2
        $handler = $stack->resolve();
120
121 2
        return $handler($command);
122
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127
    public function executeAll($commands, array $options = [])
128
    {
129
        // Modify provided callbacks to track results.
130
        $results = [];
131
        $options['fulfilled'] = function ($v, $k) use (&$results, $options) {
132
            if (isset($options['fulfilled'])) {
133
                $options['fulfilled']($v, $k);
134
            }
135
            $results[$k] = $v;
136
        };
137
        $options['rejected'] = function ($v, $k) use (&$results, $options) {
138
            if (isset($options['rejected'])) {
139
                $options['rejected']($v, $k);
140
            }
141
            $results[$k] = $v;
142
        };
143
144
        // Execute multiple commands synchronously, then sort and return the results.
145
        return $this->executeAllAsync($commands, $options)
146
            ->then(function () use (&$results) {
147
                ksort($results);
148
149
                return $results;
150
            })
151
            ->wait();
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157
    public function executeAllAsync($commands, array $options = [])
158
    {
159
        // Apply default concurrency.
160
        if (!isset($options['concurrency'])) {
161
            $options['concurrency'] = 25;
162
        }
163
164
        // Convert the iterator of commands to a generator of promises.
165
        $commands = Promise\iter_for($commands);
166
        $promises = function () use ($commands) {
167
            foreach ($commands as $key => $command) {
168
                if (!$command instanceof CommandInterface) {
169
                    throw new \InvalidArgumentException('The iterator must '
170
                        .'yield instances of '.CommandInterface::class);
171
                }
172
                yield $key => $this->executeAsync($command);
173
            }
174
        };
175
176
        // Execute the commands using a pool.
177
        return (new Promise\EachPromise($promises(), $options))->promise();
178
    }
179
180
    /**
181
     * Creates and executes a command for an operation by name.
182
     *
183
     * @param string $name Name of the command to execute.
184
     * @param array  $args Arguments to pass to the getCommand method.
185
     *
186
     * @throws \IndraGunawan\RestService\Exception\BadRequestException
187
     * @throws \IndraGunawan\RestService\Exception\BadResponseException
188
     * @throws \IndraGunawan\RestService\Exception\CommandException
189
     *
190
     * @return ResultInterface|PromiseInterface
191
     *
192
     * @see \GuzzleHttp\Command\ServiceClientInterface::getCommand
193
     */
194 2
    public function __call($name, array $args)
195
    {
196 2
        $args = isset($args[0]) ? $args[0] : [];
197 2
        if (substr($name, -5) === 'Async') {
198
            $command = $this->getCommand(substr($name, 0, -5), $args);
199
200
            return $this->executeAsync($command);
201
        } else {
202 2
            return $this->execute($this->getCommand($name, $args));
203
        }
204
    }
205
206
    /**
207
     * Defines the main handler for commands that uses the HTTP client.
208
     *
209
     * @throws \IndraGunawan\RestService\Exception\BadRequestException
210
     * @throws \IndraGunawan\RestService\Exception\BadResponseException
211
     * @throws \IndraGunawan\RestService\Exception\CommandException
212
     *
213
     * @return callable
214
     */
215
    private function createCommandHandler()
216
    {
217
        return function (CommandInterface $command) {
218 2
            return Promise\coroutine(function () use ($command) {
219
                // Prepare the HTTP options.
220 2
                $opts = $command['@http'] ?: [];
221
222
                try {
223
                    // Prepare the request from the command and send it.
224 2
                    $request = $this->transformCommandToRequest($command);
225 2
                    $promise = $this->httpClient->sendAsync($request, $opts);
226
227
                    // Create a result from the response.
228 2
                    $response = (yield $promise);
229 2
                    yield $this->transformResponseToResult($response, $request, $command);
230 1
                } catch (ValidatorException $e) {
231
                    throw new BadRequestException($e->getField(), $e->getErrorMessage(), $e);
232 1
                } catch (BadResponseException $e) {
233 1
                    $this->parseBadResponseException($command, $e);
234
                } catch (\Exception $e) {
235
                    throw new CommandException($e->getMessage(), $command, $e);
236
                }
237 2
            });
238 2
        };
239
    }
240
241
    /**
242
     * Transforms a Command object into a Request object.
243
     *
244
     * @param CommandInterface $command
245
     *
246
     * @return RequestInterface
247
     */
248 2
    private function transformCommandToRequest(CommandInterface $command)
249
    {
250 2
        $transform = $this->commandToRequestTransformer;
251
252 2
        return $transform($command);
253
    }
254
255
    /**
256
     * Transforms a Response object, also using data from the Request object,
257
     * into a Result object.
258
     *
259
     * @param ResponseInterface $response
260
     * @param RequestInterface  $request
261
     *
262
     * @return ResultInterface
263
     */
264 2
    private function transformResponseToResult(
265
        ResponseInterface $response,
266
        RequestInterface $request,
267
        CommandInterface $command
268
    ) {
269 2
        $transform = $this->responseToResultTransformer;
270
271 2
        return $transform($response, $request, $command);
272
    }
273
274
    /**
275
     * Parse BadResponseException when retrive response.
276
     *
277
     * @param CommandInterface     $command
278
     * @param BadResponseException $e
279
     *
280
     * @throws \IndraGunawan\RestService\Exception\BadResponseException
281
     */
282 1
    private function parseBadResponseException(CommandInterface $command, BadResponseException $e)
283
    {
284 1
        $parser = $this->badResponseExceptionParser;
285
286 1
        $parser($command, $e);
287
    }
288
}
289