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 |