1
|
|
|
<?php |
2
|
|
|
namespace PhpBoot\RPC; |
3
|
|
|
|
4
|
|
|
use GuzzleHttp\Client; |
5
|
|
|
use GuzzleHttp\ClientInterface; |
6
|
|
|
use PhpBoot\Application; |
7
|
|
|
use PhpBoot\Controller\ControllerContainer; |
8
|
|
|
use PhpBoot\Controller\ControllerContainerBuilder; |
9
|
|
|
use PhpBoot\Controller\ExceptionRenderer; |
10
|
|
|
use PhpBoot\Controller\Route; |
11
|
|
|
use PhpBoot\Utils\ArrayAdaptor; |
12
|
|
|
use PhpBoot\Utils\ArrayHelper; |
13
|
|
|
use Psr\Http\Message\RequestInterface; |
14
|
|
|
use Psr\Http\Message\ResponseInterface; |
15
|
|
|
|
16
|
|
|
class RpcProxy |
17
|
|
|
{ |
18
|
|
|
/** |
19
|
|
|
* PRC 代理 |
20
|
|
|
* |
21
|
|
|
* 通过__call实现代理时, 无法传递引用参数, 默认处理方式是抛出异常。如果想忽略此参数, 可以把ignoreReferenceParam设置为 true。 |
22
|
|
|
* |
23
|
|
|
* 如果需要使用引用参数, 可以继承RpcProxy, 并重对应方法。 |
24
|
|
|
* |
25
|
|
|
* TODO 支持鉴权 |
26
|
|
|
* |
27
|
|
|
* RpcProxy constructor. |
28
|
|
|
* @param Application $app |
29
|
|
|
* @param ControllerContainerBuilder $builder |
30
|
|
|
* @param Client $http |
31
|
|
|
* @param string $interface |
32
|
|
|
* @param string $prefix |
33
|
|
|
*/ |
34
|
4 |
|
public function __construct(Application $app, |
35
|
|
|
ControllerContainerBuilder $builder, |
36
|
|
|
Client $http, |
37
|
|
|
$interface, |
38
|
|
|
$prefix='/') |
39
|
|
|
{ |
40
|
4 |
|
$this->container = $builder->build($interface); |
41
|
4 |
|
$this->http = $http; |
42
|
4 |
|
$this->uriPrefix = $prefix; |
43
|
4 |
|
$this->app = $app; |
44
|
4 |
|
} |
45
|
|
|
|
46
|
|
|
public function __call($method, $args) |
47
|
|
|
{ |
48
|
|
|
$route = $this->container->getRoute($method) |
49
|
|
|
or \PhpBoot\abort($this->container->getClassName()." $method is not a valid http interface"); |
50
|
|
|
|
51
|
|
|
$request = $this->createRequest($method, $route, $args); |
|
|
|
|
52
|
|
|
|
53
|
|
|
if(MultiRpc::isRunning()){ |
54
|
|
|
$op = $this->http->sendAsync($request); |
55
|
|
|
$res = MultiRpc::wait($op); |
|
|
|
|
56
|
|
|
return $this->mapResponse($method, $route, $res, $args); |
|
|
|
|
57
|
|
|
}else{ |
58
|
|
|
$res = $this->http->send($request); |
59
|
|
|
return $this->mapResponse($method, $route, $res, $args); |
|
|
|
|
60
|
|
|
} |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @param $actionName |
65
|
|
|
* @param Route $route |
66
|
|
|
* @param array $args |
67
|
|
|
* @return RequestInterface |
68
|
|
|
*/ |
69
|
3 |
|
public function createRequest($actionName, Route $route, array $args) |
70
|
|
|
{ |
71
|
3 |
|
$params = $route->getRequestHandler()->getParamMetas(); |
72
|
|
|
//TODO 支持 query、content、path以外的其他参数, 如cookie,path等 |
73
|
3 |
|
$request = []; |
74
|
3 |
|
foreach ($params as $pos=>$param){ |
75
|
3 |
|
if(!array_key_exists($pos, $args) && $param->isOptional){ |
76
|
3 |
|
$args[$pos] = $param->default; |
77
|
3 |
|
} |
78
|
3 |
|
array_key_exists($pos, $args) or \PhpBoot\abort( |
79
|
|
|
$this->container->getClassName()." $actionName missing param {$param->name}"); |
80
|
|
|
|
81
|
3 |
|
if(!$param->isPassedByReference){ |
82
|
3 |
|
ArrayHelper::set($request, $param->source, $args[$pos]); |
83
|
3 |
|
} |
84
|
3 |
|
} |
85
|
|
|
|
86
|
3 |
|
if(isset($request['request'])){ |
87
|
3 |
|
$request = $request['request']; |
88
|
3 |
|
} |
89
|
3 |
|
$uri = $route->getUri(); |
90
|
3 |
|
foreach($route->getPathParams() as $path){ |
91
|
1 |
|
if(isset($request[$path])){ |
92
|
1 |
|
$uri = str_replace('{'.$path.'}', urlencode($request[$path]) , $uri); |
93
|
1 |
|
unset($request[$path]); |
94
|
1 |
|
} |
95
|
3 |
|
} |
96
|
3 |
|
$httpMethod = $route->getMethod(); |
97
|
|
|
|
98
|
3 |
|
$query = []; |
99
|
3 |
|
$body = null; |
100
|
3 |
|
$headers = ['Content-Type'=>'application/json']; |
101
|
|
|
|
102
|
3 |
|
if(isset($request['query'])){ |
103
|
1 |
|
$query += $request['query']; |
104
|
1 |
|
} |
105
|
3 |
|
unset($request['query']); |
106
|
|
|
|
107
|
3 |
|
if(isset($request['headers'])){ |
108
|
1 |
|
$headers += $request['headers']; |
109
|
1 |
|
} |
110
|
3 |
|
unset($request['headers']); |
111
|
|
|
|
112
|
3 |
|
if(isset($request['cookies'])){ |
113
|
1 |
|
$cookies = []; |
114
|
1 |
|
foreach ($request['cookies'] as $k=>$v){ |
115
|
1 |
|
$cookies[] = "$k=$v"; |
116
|
1 |
|
} |
117
|
1 |
|
$headers['Cookie'] = implode('; ', $cookies); |
118
|
1 |
|
} |
119
|
3 |
|
unset($request['cookies']); |
120
|
|
|
|
121
|
3 |
|
if(isset($request['request'])){ |
122
|
1 |
|
if($body === null){ |
123
|
1 |
|
$body = []; |
124
|
1 |
|
} |
125
|
1 |
|
$body += $request['request']; |
126
|
1 |
|
} |
127
|
3 |
|
unset($request['request']); |
128
|
|
|
|
129
|
3 |
|
if(isset($request['files'])){ |
130
|
|
|
\PhpBoot\abort(new \UnexpectedValueException("sending request with files is not support")); |
131
|
|
|
} |
132
|
3 |
|
if(in_array($httpMethod, ['GET', 'OPTION'])){ |
133
|
1 |
|
foreach ($request as $k => $v){ |
134
|
1 |
|
if(!in_array($k, ['query', 'request', 'files', 'cookies', 'headers'])){ |
135
|
1 |
|
$query[$k] = $v; |
136
|
1 |
|
} |
137
|
1 |
|
} |
138
|
1 |
|
}else{ |
139
|
2 |
|
foreach ($request as $k => $v){ |
140
|
2 |
|
if(!in_array($k, ['query', 'request', 'files', 'cookies', 'headers'])){ |
141
|
2 |
|
if($body === null){ |
142
|
1 |
|
$body = []; |
143
|
1 |
|
} |
144
|
2 |
|
$body[$k] = $v; |
145
|
2 |
|
} |
146
|
2 |
|
} |
147
|
|
|
} |
148
|
3 |
|
if($body !== null){ |
149
|
2 |
|
$body = json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); |
150
|
2 |
|
} |
151
|
3 |
|
$uri = $this->uriPrefix.ltrim($uri, '/'); |
152
|
3 |
|
if(strpos($uri,'?') === false){ |
153
|
3 |
|
$uri = $uri.'?'.http_build_query($query); |
154
|
3 |
|
}else{ |
155
|
|
|
$uri = $uri.'&'.http_build_query($query); |
156
|
|
|
} |
157
|
3 |
|
return new \GuzzleHttp\Psr7\Request( |
158
|
3 |
|
$httpMethod, |
159
|
3 |
|
$uri, |
160
|
3 |
|
$headers, |
161
|
|
|
$body |
162
|
3 |
|
); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
|
166
|
1 |
|
public function mapResponse($actionName, Route $route, ResponseInterface $response, $requestArg=[]) |
|
|
|
|
167
|
|
|
{ |
168
|
1 |
|
$response = \Symfony\Component\HttpFoundation\Response::create( |
169
|
1 |
|
(string)$response->getBody(), |
170
|
1 |
|
$response->getStatusCode(), |
171
|
1 |
|
$response->getHeaders() |
172
|
1 |
|
); |
173
|
1 |
|
$namedArgs = []; |
174
|
|
|
|
175
|
1 |
|
foreach ($route->getRequestHandler()->getParamMetas() as $pos=>$param){ |
176
|
|
|
|
177
|
1 |
|
if($param->isPassedByReference){ |
178
|
1 |
|
$namedArgs[$param->name] = &$requestArg[$pos]; |
179
|
1 |
|
} |
180
|
1 |
|
} |
181
|
|
|
|
182
|
|
|
|
183
|
1 |
|
$content = json_decode((string)$response->getContent(), true); |
184
|
|
|
|
185
|
|
|
//TODO 远端接口没有抛出异常,但设置了 status( status 不是200)时如何处理 |
186
|
1 |
|
$handler = $route->getResponseHandler(); |
187
|
|
|
|
188
|
1 |
|
if($response->getStatusCode() >= 200 && $response->getStatusCode() <300){ |
189
|
|
|
|
190
|
|
|
|
191
|
1 |
|
$returns = $handler->getMappings(); |
192
|
1 |
|
$buffer = []; |
193
|
1 |
|
ArrayHelper::set($buffer, 'response.content', $content); |
194
|
1 |
|
ArrayHelper::set($buffer, 'response.headers', new ArrayAdaptor($response->headers)); |
195
|
1 |
|
ArrayHelper::set($buffer, 'response.cookies', $response->headers->getCookies()); |
196
|
|
|
|
197
|
|
|
//TODO 支持 cookie |
198
|
|
|
//ArrayHelper::set($response, 'response.cookies'); |
199
|
|
|
|
200
|
|
|
|
201
|
|
|
$mapping = [ |
202
|
|
|
'params'=>$namedArgs |
203
|
1 |
|
]; |
204
|
|
|
|
205
|
1 |
|
foreach ($returns as $map=>$return){ |
206
|
|
|
|
207
|
1 |
|
$data = \JmesPath\search($map, $buffer); |
208
|
1 |
|
if(!$return->container){ |
209
|
|
|
continue; |
210
|
|
|
} |
211
|
1 |
|
$data = $return->container->make($data, false); |
212
|
1 |
|
ArrayHelper::set($mapping, $return->source, $data); |
213
|
1 |
|
} |
214
|
|
|
|
215
|
1 |
|
}else{ |
216
|
|
|
|
217
|
|
|
//TODO 如果多个 异常对应同一个 statusCode 怎么处理 |
218
|
|
|
$exceptions = $route->getExceptionHandler()->getExceptions(); |
219
|
|
|
|
220
|
|
|
$errName = null; |
|
|
|
|
221
|
|
|
foreach ($exceptions as $err){ |
222
|
|
|
|
223
|
|
|
$renderer = $this->app->get(ExceptionRenderer::class); |
224
|
|
|
$exec = $renderer->render( |
225
|
|
|
$this->app->make($err, ['message'=>(string)$response->getContent()]) |
226
|
|
|
); |
227
|
|
|
|
228
|
|
|
if( $exec->getStatusCode() == $response->getStatusCode()){ |
229
|
|
|
throw $exec; |
230
|
|
|
} |
231
|
|
|
} |
232
|
|
|
throw new \RuntimeException((string)$response->getContent()); |
233
|
|
|
|
234
|
|
|
}; |
235
|
|
|
|
236
|
1 |
|
if(isset($mapping['return'])){ |
237
|
1 |
|
return $mapping['return']; |
238
|
|
|
}; |
239
|
|
|
} |
240
|
|
|
/** |
241
|
|
|
* @var ControllerContainer |
242
|
|
|
*/ |
243
|
|
|
protected $container; |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* @var ClientInterface |
247
|
|
|
*/ |
248
|
|
|
protected $http; |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* @var string |
252
|
|
|
*/ |
253
|
|
|
protected $uriPrefix; |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* @var Application |
257
|
|
|
*/ |
258
|
|
|
protected $app; |
259
|
|
|
} |
260
|
|
|
|
This check looks for type mismatches where the missing type is
false
. This is usually indicative of an error condtion.Consider the follow example
This function either returns a new
DateTime
object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returnedfalse
before passing on the value to another function or method that may not be able to handle afalse
.