├── COPYRIGHT.md
├── LICENSE.md
├── README.md
├── composer.json
├── composer.lock
├── psalm-baseline.xml
├── psalm.xml.dist
├── renovate.json
└── src
├── Cache.php
├── Client.php
├── Error.php
├── Exception
├── BadMethodCallException.php
├── ErrorException.php
├── ExceptionInterface.php
├── HttpException.php
├── InvalidArgumentException.php
└── RuntimeException.php
├── Request.php
├── Request
└── Http.php
├── Response.php
├── Response
└── Http.php
├── Server.php
├── Smd.php
└── Smd
└── Service.php
/COPYRIGHT.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/)
2 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are met:
5 |
6 | - Redistributions of source code must retain the above copyright notice, this
7 | list of conditions and the following disclaimer.
8 |
9 | - Redistributions in binary form must reproduce the above copyright notice,
10 | this list of conditions and the following disclaimer in the documentation
11 | and/or other materials provided with the distribution.
12 |
13 | - Neither the name of Laminas Foundation nor the names of its contributors may
14 | be used to endorse or promote products derived from this software without
15 | specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # laminas-json-server
2 |
3 | [](https://github.com/laminas/laminas-json-server/actions/workflows/continuous-integration.yml)
4 |
5 | > ## 🇷🇺 Русским гражданам
6 | >
7 | > Мы, участники Laminas, родились и живем в разных странах. У многих из нас есть друзья, родственники и коллеги как в России, так и в Украине. Некоторые из нас родились в России. Некоторые из нас живут в России. У некоторых бабушки и дедушки сражались с фашистами во Второй мировой войне. Здесь никто не поддерживает фашизм.
8 | >
9 | > У одного из нас есть украинская родственница, которая спаслась из дома вместе с сыном. Поезд задержался из-за бомбежки на дороге впереди. У нас есть друзья, которые прячутся в бомбоубежищах. Мы с тревогой ждем весточки от них после воздушных налетов, которые беспорядочно наносят удары и попадают по больницам, школам, детским садам и домам. Мы не берем это из каких-либо СМИ. Мы наблюдаем это напрямую.
10 | >
11 | > Вы доверяете нам достаточно, чтоб использовать наши программы, и мы просим вас довериться нам вновь. Мы нуждаемся в помощи. Выходите и протестуйте против этой бесполезной войны. Остановите кровопролитие. Скажите "Нет войне!"
12 | >
13 | > ## 🇺🇸 To Citizens of Russia
14 | >
15 | > We at Laminas come from all over the world. Many of us have friends, family and colleagues in both Russia and Ukraine. Some of us were born in Russia. Some of us currently live in Russia. Some have grandparents who fought Nazis in World War II. Nobody here supports fascism.
16 | >
17 | > One team member has a Ukrainian relative who fled her home with her son. The train was delayed due to bombing on the road ahead. We have friends who are hiding in bomb shelters. We anxiously follow up on them after the air raids, which indiscriminately fire at hospitals, schools, kindergartens and houses. We're not taking this from any media. These are our actual experiences.
18 | >
19 | > You trust us enough to use our software. We ask that you trust us to say the truth on this. We need your help. Go out and protest this unnecessary war. Stop the bloodshed. Say "stop the war!"
20 |
21 | Provides a JSON-RPC server implementation.
22 |
23 | - File issues at https://github.com/laminas/laminas-json-server/issues
24 | - Documentation is at https://docs.laminas.dev/laminas-json-server/
25 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "laminas/laminas-json-server",
3 | "description": "Laminas Json-Server is a JSON-RPC server implementation.",
4 | "license": "BSD-3-Clause",
5 | "keywords": [
6 | "laminas",
7 | "json",
8 | "server",
9 | "json-server"
10 | ],
11 | "homepage": "https://laminas.dev",
12 | "support": {
13 | "docs": "https://docs.laminas.dev/laminas-json-server/",
14 | "issues": "https://github.com/laminas/laminas-json-server/issues",
15 | "source": "https://github.com/laminas/laminas-json-server",
16 | "rss": "https://github.com/laminas/laminas-json-server/releases.atom",
17 | "chat": "https://laminas.dev/chat",
18 | "forum": "https://discourse.laminas.dev"
19 | },
20 | "config": {
21 | "allow-plugins": {
22 | "dealerdirect/phpcodesniffer-composer-installer": true
23 | },
24 | "sort-packages": true,
25 | "platform": {
26 | "php": "8.1.99"
27 | }
28 | },
29 | "require": {
30 | "php": "~8.1.0 || ~8.2.0 || ~8.3.0",
31 | "laminas/laminas-http": "^2.19.0",
32 | "laminas/laminas-json": "^3.6.0",
33 | "laminas/laminas-server": "^2.16.0"
34 | },
35 | "require-dev": {
36 | "ext-json": "*",
37 | "laminas/laminas-coding-standard": "^2.4.0",
38 | "phpunit/phpunit": "^10.5",
39 | "psalm/plugin-phpunit": "^0.18.0",
40 | "vimeo/psalm": "^5.17"
41 | },
42 | "conflict": {
43 | "laminas/laminas-stdlib": "<3.2.1"
44 | },
45 | "autoload": {
46 | "psr-4": {
47 | "Laminas\\Json\\Server\\": "src/"
48 | }
49 | },
50 | "autoload-dev": {
51 | "files": [
52 | "test/TestAsset/FooFunc.php"
53 | ],
54 | "psr-4": {
55 | "LaminasTest\\Json\\Server\\": "test/"
56 | }
57 | },
58 | "scripts": {
59 | "check": [
60 | "@cs-check",
61 | "@test"
62 | ],
63 | "cs-check": "phpcs",
64 | "cs-fix": "phpcbf",
65 | "static-analysis": "psalm --shepherd --stats",
66 | "test": "phpunit --colors=always",
67 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
68 | },
69 | "replace": {
70 | "zendframework/zend-json-server": "^3.2.0"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/psalm-baseline.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ! is_string($filename)
6 | ! is_string($filename)
7 |
8 |
9 | static function ($errno, $errstr): void {
10 | // swallow errors; method returns false on failure
11 | }
12 | static function ($errno, $errstr): void {
13 | // swallow errors; method returns false on failure
14 | }
15 |
16 |
17 | is_string($filename)
18 |
19 |
20 | $errno
21 | $errno
22 | $errstr
23 | $errstr
24 |
25 |
26 |
27 |
28 | Client
29 |
30 |
31 | getUriString() === null]]>
32 |
33 |
34 | addHeaderLine
35 | addHeaders
36 | get
37 | has
38 | has
39 |
40 |
41 | getMessage
42 |
43 |
44 | addHeaderLine
45 | addHeaders
46 | get
47 | has
48 | has
49 |
50 |
51 | $lastRequest
52 | $lastResponse
53 |
54 |
55 |
56 |
57 | methodRegex]]>
58 |
59 |
60 | ! is_string($key)
61 |
62 |
63 | $method
64 |
65 |
66 | $options
67 | $value
68 |
69 |
70 | $key
71 | $key
72 |
73 |
74 | $options
75 | $value
76 | $value
77 |
78 |
79 | string
80 |
81 |
82 | id]]>
83 |
84 |
85 | getId())]]>
86 |
87 |
88 |
89 |
90 | getRawJson
91 |
92 |
93 | Http
94 |
95 |
96 |
97 |
98 | $id
99 | $id
100 | $serviceMap
101 | $serviceMap
102 |
103 |
104 |
105 | $value
106 |
107 |
108 | $errorData
109 |
110 | error]]>
111 | id]]>
112 | $value
113 |
114 |
115 | toArray
116 |
117 |
118 | getArgs
119 | setArgs
120 |
121 |
122 | (string) $version
123 |
124 |
125 |
126 |
127 | getClass()]]>
128 | getFunction()]]>
129 |
130 |
131 | Server
132 |
133 |
134 | _buildSignature
135 | _buildSignature
136 | _dispatch
137 |
138 |
139 | getMethod()]]>
140 | request]]>
141 | response]]>
142 | serviceMap]]>
143 |
144 |
145 | $function
146 |
147 |
148 | Server
149 | Server
150 | self
151 |
152 |
153 | static function ($count, $param) {
154 |
155 |
156 | $argv
157 | $method
158 | $method
159 | $method
160 | $method
161 | $param
162 |
163 |
164 |
165 | $key
166 |
167 |
168 |
169 |
170 |
171 |
172 | getType()]]>
173 |
174 |
175 |
176 |
177 | $params[$key]
178 | $params[$key]
179 | $params[$key]
180 | $params[$key]
181 | $params[$key]
182 | $params[$key]
183 |
184 |
185 |
186 | $count
187 | $default
188 | $description
189 | $key
190 | $method
191 | $method
192 | $method
193 | $newType
194 | getName()]]]>
195 | $param
196 | $parameter
197 |
198 |
199 | $prototype
200 | $prototype
201 | $requiredParamsCount
202 | $result
203 | $return[]
204 | $value
205 |
206 |
207 | string|array
208 |
209 |
210 | getDefaultValue
211 | getDescription
212 | getName
213 | getParameterObjects
214 | getReturnType
215 | getType
216 | getType
217 | getType
218 | isOptional
219 |
220 |
221 | $count
222 |
223 |
224 | $return[0]
225 |
226 |
227 | $class
228 | $fault
229 | $request
230 |
231 |
232 | getCode()]]>
233 | $function
234 |
235 |
236 | $function
237 |
238 |
239 | getParams
240 |
241 |
242 | $argv
243 | $argv
244 | getClass()]]>
245 | getFunction()]]>
246 | getMethod()]]>
247 | $invokable
248 |
249 |
250 | $method
251 |
252 |
253 | Error|null
254 |
255 |
256 | $request
257 | $response
258 | $serviceMap
259 | $smdMethods
260 |
261 |
262 | (bool) $flag
263 |
264 |
265 | smdMethods]]>
266 | getId())]]>
267 | getVersion())]]>
268 |
269 |
270 |
271 |
272 | contentTypeRegex]]>
273 |
274 |
275 | $description
276 | $id
277 | $target
278 |
279 |
280 | $param
281 | $service
282 |
283 |
284 | $key
285 |
286 |
287 | $param
288 | $service
289 |
290 | $svc
291 | $svc
292 | $value
293 |
294 |
295 | bool|Smd\Service
296 |
297 |
298 | getParams
299 | setEnvelope
300 | toArray
301 |
302 |
303 | services[$name]]]>
304 |
305 |
306 | self
307 |
308 |
309 | (bool) $flag
310 | (string) $description
311 | (string) $id
312 | (string) $target
313 |
314 |
315 | getId())]]>
316 | getTarget())]]>
317 |
318 |
319 |
320 |
321 | nameRegex]]>
322 |
323 |
324 |
325 |
326 | is_string($type)
327 |
328 |
329 | $order
330 | $paramType
331 | $returnType
332 | $type
333 |
334 |
335 | $key
336 | $key
337 | envelopeTypes]]>
338 | transportTypes]]>
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 | $callback
351 | $order
352 | $param
353 | $paramOptions[$key]
354 | $paramType
355 | $paramType
356 | $params[$index]
357 |
358 | $returnType
359 | $type
360 | $value
361 | $value
362 |
363 |
364 | $callback($value)
365 |
366 |
367 | string
368 |
369 |
370 | $paramType
371 |
372 |
373 | $type
374 |
375 |
376 | self
377 |
378 |
379 | (string) $target
380 |
381 |
382 | is_array($spec)
383 |
384 |
385 |
386 |
387 | cacheFile]]>
388 | cacheFile]]>
389 | cacheFile]]>
390 | cacheFile]]>
391 | cacheFile]]>
392 | cacheFile]]>
393 | cacheFile]]>
394 |
395 |
396 |
397 |
398 | Exception\ExceptionInterface::class
399 |
400 |
401 | makeHttpResponseFor
402 | mockHttpClient
403 |
404 |
405 |
406 |
407 | Json\Json::decode($json, Json\Json::TYPE_ARRAY)
408 | Json\Json::decode($json, Json\Json::TYPE_ARRAY)
409 |
410 |
411 | array
412 |
413 |
414 | assertIsArray
415 |
416 |
417 |
418 |
419 | $spec[1]
420 |
421 |
422 | $test
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 | $version
432 |
433 |
434 | getCode
435 | getData
436 | getMessage
437 |
438 |
439 |
440 |
441 | $object
442 | $object
443 | new Json\Json()
444 |
445 |
446 | $object
447 | $object
448 | new Json\Json()
449 |
450 |
451 |
452 |
453 |
454 | setContentType
455 | setContentType
456 | setDescription
457 | setDescription
458 | setEnvelope
459 | setEnvelope
460 | setId
461 | setId
462 | setTarget
463 | setTarget
464 |
465 |
466 | getCallback
467 | getCode
468 | getCode
469 | getCode
470 | getCode
471 | getCode
472 | getCode
473 | getCode
474 | getMessage
475 | getResult
476 | getResult
477 | getResult
478 | getResult
479 |
480 |
481 | assertIsArray
482 |
483 |
484 |
485 |
486 | $params
487 | $params
488 | $params
489 | 123
490 | new stdClass()
491 | new stdClass()
492 |
493 |
494 | $params
495 | $smd
496 |
497 |
498 |
499 |
500 |
501 |
502 |
503 |
504 |
505 |
506 |
507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 |
516 | $param
517 | $param
518 | $param
519 | $param
520 | $param
521 | $param
522 | $param
523 | $param
524 | $param
525 | $param
526 | $param
527 | $param
528 | $params
529 | $smd
530 |
531 |
532 | null
533 | null
534 |
535 |
536 |
537 |
538 | $methods
539 | $methods
540 | $service
541 | $services
542 | $services
543 |
544 |
545 | $bar
546 | $foo
547 | $methods
548 | $services
549 | $smd
550 | $smd
551 |
552 |
553 | $bar
554 | $foo
555 | $methods
556 | $services
557 | $smd
558 | $smd
559 |
560 |
561 | assertIsArray
562 |
563 |
564 | assertIsArray
565 | assertIsArray
566 |
567 |
568 |
569 |
570 | $someval
571 |
572 |
573 | $val
574 |
575 |
576 | baz
577 |
578 |
579 | array
580 |
581 |
582 |
583 |
584 | bar
585 | baz
586 |
587 |
588 |
589 |
590 | bar
591 | baz
592 |
593 |
594 |
595 |
596 | JsonSerializableBuiltinImpl
597 |
598 |
599 |
600 |
601 | TestIteratorAggregate
602 |
603 |
604 |
605 |
--------------------------------------------------------------------------------
/psalm.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "local>laminas/.github:renovate-config"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/src/Cache.php:
--------------------------------------------------------------------------------
1 | getServiceMap()->toJson());
49 |
50 | restore_error_handler();
51 |
52 | if (0 === $test) {
53 | return false;
54 | }
55 |
56 | return true;
57 | }
58 |
59 | /**
60 | * Retrieve a cached SMD
61 | *
62 | * On success, returns the cached SMD (a JSON string); a failure, returns
63 | * boolean false.
64 | *
65 | * @param string $filename
66 | * @return string|false
67 | */
68 | public static function getSmd($filename)
69 | {
70 | if (
71 | ! is_string($filename)
72 | || ! file_exists($filename)
73 | || ! is_readable($filename)
74 | ) {
75 | return false;
76 | }
77 |
78 | set_error_handler(static function ($errno, $errstr): void {
79 | // swallow errors; method returns false on failure
80 | }, E_WARNING);
81 |
82 | $smd = file_get_contents($filename);
83 |
84 | restore_error_handler();
85 |
86 | if (false === $smd) {
87 | return false;
88 | }
89 |
90 | return $smd;
91 | }
92 |
93 | /**
94 | * Delete a file containing a cached SMD
95 | *
96 | * @param string $filename
97 | * @return bool
98 | */
99 | public static function deleteSmd($filename)
100 | {
101 | if (is_string($filename) && file_exists($filename)) {
102 | unlink($filename);
103 | return true;
104 | }
105 |
106 | return false;
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/Client.php:
--------------------------------------------------------------------------------
1 | httpClient = $httpClient ?: new HttpClient();
56 | $this->serverAddress = $server;
57 | }
58 |
59 | /**
60 | * Sets the HTTP client object to use for connecting the JSON-RPC server.
61 | *
62 | * @param HttpClient $httpClient New HTTP client to use.
63 | * @return Client Self instance.
64 | */
65 | public function setHttpClient(HttpClient $httpClient)
66 | {
67 | $this->httpClient = $httpClient;
68 | return $this;
69 | }
70 |
71 | /**
72 | * Gets the HTTP client object.
73 | *
74 | * @return HttpClient HTTP client.
75 | */
76 | public function getHttpClient()
77 | {
78 | return $this->httpClient;
79 | }
80 |
81 | /**
82 | * The request of the last method call.
83 | *
84 | * @return Request
85 | */
86 | public function getLastRequest()
87 | {
88 | return $this->lastRequest;
89 | }
90 |
91 | /**
92 | * The response received from the last method call.
93 | *
94 | * @return Response
95 | */
96 | public function getLastResponse()
97 | {
98 | return $this->lastResponse;
99 | }
100 |
101 | /**
102 | * Perform a JSON-RPC request and return a response.
103 | *
104 | * @param Request $request Request.
105 | * @return Response Response.
106 | * @throws Exception\HttpException When HTTP communication fails.
107 | */
108 | public function doRequest($request)
109 | {
110 | $this->lastRequest = $request;
111 |
112 | $httpRequest = $this->httpClient->getRequest();
113 | if ($httpRequest->getUriString() === null) {
114 | $this->httpClient->setUri($this->serverAddress);
115 | }
116 |
117 | // Set default Accept and Content-Type headers unless already set.
118 | $headers = $httpRequest->getHeaders();
119 | $headersToAdd = [];
120 | if (! $headers->has('Content-Type')) {
121 | $headersToAdd['Content-Type'] = 'application/json-rpc';
122 | }
123 | if (! $headers->has('Accept')) {
124 | $headersToAdd['Accept'] = 'application/json-rpc';
125 | }
126 | $headers->addHeaders($headersToAdd);
127 |
128 | if (! $headers->get('User-Agent')) {
129 | $headers->addHeaderLine('User-Agent', 'Laminas_Json_Server_Client');
130 | }
131 |
132 | $this->httpClient->setRawBody($request->__toString());
133 | $this->httpClient->setMethod('POST');
134 | $httpResponse = $this->httpClient->send();
135 |
136 | if (! $httpResponse->isSuccess()) {
137 | throw new Exception\HttpException(
138 | $httpResponse->getReasonPhrase(),
139 | $httpResponse->getStatusCode()
140 | );
141 | }
142 |
143 | $response = new Response();
144 |
145 | $this->lastResponse = $response;
146 |
147 | // import all response data from JSON HTTP response
148 | $response->loadJson($httpResponse->getBody());
149 |
150 | return $response;
151 | }
152 |
153 | /**
154 | * Send a JSON-RPC request to the service (for a specific method).
155 | *
156 | * @param string $method Name of the method we want to call.
157 | * @param array $params Array of parameters for the method.
158 | * @return mixed Method call results.
159 | * @throws Exception\ErrorException When remote call fails.
160 | */
161 | public function call($method, $params = [])
162 | {
163 | $request = $this->createRequest($method, $params);
164 |
165 | $response = $this->doRequest($request);
166 |
167 | if ($response->isError()) {
168 | $error = $response->getError();
169 | throw new Exception\ErrorException(
170 | $error->getMessage(),
171 | $error->getCode()
172 | );
173 | }
174 |
175 | return $response->getResult();
176 | }
177 |
178 | /**
179 | * Create request object.
180 | *
181 | * @param string $method Method to call.
182 | * @param array $params List of arguments.
183 | * @return Request Created request.
184 | */
185 | protected function createRequest($method, array $params)
186 | {
187 | $request = new Request();
188 | $request->setMethod($method)
189 | ->setParams($params)
190 | ->setId(++$this->id);
191 | return $request;
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/src/Error.php:
--------------------------------------------------------------------------------
1 | setMessage($message)
53 | ->setCode($code)
54 | ->setData($data);
55 | }
56 |
57 | /**
58 | * Set error code.
59 | *
60 | * If the error code is 0, it will be set to -32000 (ERROR_OTHER).
61 | *
62 | * @param mixed $code
63 | * @return self
64 | */
65 | public function setCode($code)
66 | {
67 | if (! is_scalar($code) || is_bool($code) || is_float($code)) {
68 | return $this;
69 | }
70 |
71 | if (is_string($code) && ! is_numeric($code)) {
72 | return $this;
73 | }
74 |
75 | $code = (int) $code;
76 |
77 | if (0 === $code) {
78 | $this->code = self::ERROR_OTHER;
79 | return $this;
80 | }
81 |
82 | $this->code = $code;
83 | return $this;
84 | }
85 |
86 | /**
87 | * Get error code
88 | *
89 | * @return int
90 | */
91 | public function getCode()
92 | {
93 | return $this->code;
94 | }
95 |
96 | /**
97 | * Set error message
98 | *
99 | * @param mixed $message
100 | * @return self
101 | */
102 | public function setMessage($message)
103 | {
104 | if (! is_scalar($message)) {
105 | return $this;
106 | }
107 |
108 | $this->message = (string) $message;
109 |
110 | return $this;
111 | }
112 |
113 | /**
114 | * Get error message
115 | *
116 | * @return string
117 | */
118 | public function getMessage()
119 | {
120 | return $this->message;
121 | }
122 |
123 | /**
124 | * Set error data
125 | *
126 | * @param mixed $data
127 | * @return self
128 | */
129 | public function setData($data)
130 | {
131 | $this->data = $data;
132 |
133 | return $this;
134 | }
135 |
136 | /**
137 | * Get error data
138 | *
139 | * @return mixed
140 | */
141 | public function getData()
142 | {
143 | return $this->data;
144 | }
145 |
146 | /**
147 | * Cast error to array
148 | *
149 | * @return array
150 | */
151 | public function toArray()
152 | {
153 | return [
154 | 'code' => $this->getCode(),
155 | 'message' => $this->getMessage(),
156 | 'data' => $this->getData(),
157 | ];
158 | }
159 |
160 | /**
161 | * Cast error to JSON
162 | *
163 | * @return string
164 | */
165 | public function toJson()
166 | {
167 | return Json::encode($this->toArray());
168 | }
169 |
170 | /**
171 | * Cast to string (JSON)
172 | *
173 | * @return string
174 | */
175 | public function __toString()
176 | {
177 | return $this->toJson();
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/Exception/BadMethodCallException.php:
--------------------------------------------------------------------------------
1 | $value) {
83 | $method = 'set' . ucfirst($key);
84 | if (in_array($method, $methods)) {
85 | $this->$method($value);
86 | continue;
87 | }
88 |
89 | if ($key === 'jsonrpc') {
90 | $this->setVersion($value);
91 | continue;
92 | }
93 | }
94 | return $this;
95 | }
96 |
97 | /**
98 | * Add a parameter to the request.
99 | *
100 | * @param mixed $value
101 | * @param string $key
102 | * @return self
103 | */
104 | public function addParam($value, $key = null)
105 | {
106 | if ((null === $key) || ! is_string($key)) {
107 | $index = count($this->params);
108 | $this->params[$index] = $value;
109 | return $this;
110 | }
111 |
112 | $this->params[$key] = $value;
113 | return $this;
114 | }
115 |
116 | /**
117 | * Add many params.
118 | *
119 | * @param array $params
120 | * @return self
121 | */
122 | public function addParams(array $params)
123 | {
124 | foreach ($params as $key => $value) {
125 | $this->addParam($value, $key);
126 | }
127 | return $this;
128 | }
129 |
130 | /**
131 | * Overwrite params.
132 | *
133 | * @param array $params
134 | * @return Request
135 | */
136 | public function setParams(array $params)
137 | {
138 | $this->params = [];
139 | return $this->addParams($params);
140 | }
141 |
142 | /**
143 | * Retrieve param by index or key.
144 | *
145 | * @param int|string $index
146 | * @return mixed|null Null when not found
147 | */
148 | public function getParam($index)
149 | {
150 | if (! array_key_exists($index, $this->params)) {
151 | return null;
152 | }
153 |
154 | return $this->params[$index];
155 | }
156 |
157 | /**
158 | * Retrieve parameters.
159 | *
160 | * @return array
161 | */
162 | public function getParams()
163 | {
164 | return $this->params;
165 | }
166 |
167 | /**
168 | * Set request method.
169 | *
170 | * @param string $name
171 | * @return self
172 | */
173 | public function setMethod($name)
174 | {
175 | if (! preg_match($this->methodRegex, $name)) {
176 | $this->isMethodError = true;
177 | return $this;
178 | }
179 |
180 | $this->method = $name;
181 | return $this;
182 | }
183 |
184 | /**
185 | * Get request method name.
186 | *
187 | * @return string
188 | */
189 | public function getMethod()
190 | {
191 | return $this->method;
192 | }
193 |
194 | /**
195 | * Was a bad method provided?
196 | *
197 | * @return bool
198 | */
199 | public function isMethodError()
200 | {
201 | return $this->isMethodError;
202 | }
203 |
204 | /**
205 | * Was a malformed JSON provided?
206 | *
207 | * @return bool
208 | */
209 | public function isParseError()
210 | {
211 | return $this->isParseError;
212 | }
213 |
214 | /**
215 | * Set request identifier
216 | *
217 | * @param mixed $name
218 | * @return self
219 | */
220 | public function setId($name)
221 | {
222 | $this->id = (string) $name;
223 | return $this;
224 | }
225 |
226 | /**
227 | * Retrieve request identifier.
228 | *
229 | * @return string
230 | */
231 | public function getId()
232 | {
233 | return $this->id;
234 | }
235 |
236 | /**
237 | * Set JSON-RPC version
238 | *
239 | * @param string $version
240 | * @return self
241 | */
242 | public function setVersion($version)
243 | {
244 | if ('2.0' === $version) {
245 | $this->version = '2.0';
246 | return $this;
247 | }
248 |
249 | $this->version = '1.0';
250 | return $this;
251 | }
252 |
253 | /**
254 | * Retrieve JSON-RPC version.
255 | *
256 | * @return string
257 | */
258 | public function getVersion()
259 | {
260 | return $this->version;
261 | }
262 |
263 | /**
264 | * Set request state based on JSON.
265 | *
266 | * @param string $json
267 | * @return void
268 | */
269 | public function loadJson($json)
270 | {
271 | try {
272 | $options = Json::decode($json, Json::TYPE_ARRAY);
273 | $this->setOptions($options);
274 | } catch (Exception $e) {
275 | $this->isParseError = true;
276 | }
277 | }
278 |
279 | /**
280 | * Cast request to JSON.
281 | *
282 | * @return string
283 | */
284 | public function toJson()
285 | {
286 | $jsonArray = [
287 | 'method' => $this->getMethod(),
288 | ];
289 |
290 | if (null !== ($id = $this->getId())) {
291 | $jsonArray['id'] = $id;
292 | }
293 |
294 | $params = $this->getParams();
295 | if (! empty($params)) {
296 | $jsonArray['params'] = $params;
297 | }
298 |
299 | if ('2.0' === $this->getVersion()) {
300 | $jsonArray['jsonrpc'] = '2.0';
301 | }
302 |
303 | return Json::encode($jsonArray);
304 | }
305 |
306 | /**
307 | * Cast request to string (JSON)
308 | *
309 | * @return string
310 | */
311 | public function __toString()
312 | {
313 | return $this->toJson();
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/src/Request/Http.php:
--------------------------------------------------------------------------------
1 | rawJson = $json;
27 | if (! empty($json)) {
28 | $this->loadJson($json);
29 | }
30 | }
31 |
32 | /**
33 | * Get JSON from raw POST body
34 | *
35 | * @return string
36 | */
37 | public function getRawJson()
38 | {
39 | return $this->rawJson;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Response.php:
--------------------------------------------------------------------------------
1 | $value) {
72 | $method = 'set' . ucfirst((string) $key);
73 | if (in_array($method, $methods)) {
74 | $this->$method($value);
75 | continue;
76 | }
77 |
78 | if ('jsonrpc' === $key) {
79 | $this->setVersion($value);
80 | continue;
81 | }
82 | }
83 | return $this;
84 | }
85 |
86 | /**
87 | * Set response state based on JSON.
88 | *
89 | * @param string $json
90 | * @return void
91 | * @throws Exception\RuntimeException
92 | */
93 | public function loadJson($json)
94 | {
95 | try {
96 | $options = Json::decode($json, Json::TYPE_ARRAY);
97 | } catch (RuntimeException $e) {
98 | throw new Exception\RuntimeException(
99 | 'json is not a valid response; array expected',
100 | $e->getCode(),
101 | $e
102 | );
103 | }
104 |
105 | if (! is_array($options)) {
106 | throw new Exception\RuntimeException('json is not a valid response; array expected');
107 | }
108 |
109 | $this->setOptions($options);
110 | }
111 |
112 | /**
113 | * Set result.
114 | *
115 | * @param mixed $value
116 | * @return self
117 | */
118 | public function setResult($value)
119 | {
120 | $this->result = $value;
121 | return $this;
122 | }
123 |
124 | /**
125 | * Get result.
126 | *
127 | * @return mixed
128 | */
129 | public function getResult()
130 | {
131 | return $this->result;
132 | }
133 |
134 | /**
135 | * Set result error
136 | *
137 | * RPC error, if response results in fault.
138 | *
139 | * @param mixed $error
140 | * @return self
141 | */
142 | public function setError(?Error $error = null)
143 | {
144 | $this->error = $error;
145 | return $this;
146 | }
147 |
148 | /**
149 | * Get response error
150 | *
151 | * @return null|Error
152 | */
153 | public function getError()
154 | {
155 | return $this->error;
156 | }
157 |
158 | /**
159 | * Is the response an error?
160 | *
161 | * @return bool
162 | */
163 | public function isError()
164 | {
165 | return $this->getError() instanceof Error;
166 | }
167 |
168 | /**
169 | * Set request ID
170 | *
171 | * @param mixed $name
172 | * @return self
173 | */
174 | public function setId($name)
175 | {
176 | $this->id = $name;
177 | return $this;
178 | }
179 |
180 | /**
181 | * Get request ID.
182 | *
183 | * @return mixed
184 | */
185 | public function getId()
186 | {
187 | return $this->id;
188 | }
189 |
190 | /**
191 | * Set JSON-RPC version.
192 | *
193 | * @param string $version
194 | * @return self
195 | */
196 | public function setVersion($version)
197 | {
198 | $version = (string) $version;
199 | if ('2.0' === $version) {
200 | $this->version = '2.0';
201 | return $this;
202 | }
203 |
204 | $this->version = null;
205 | return $this;
206 | }
207 |
208 | /**
209 | * Retrieve JSON-RPC version
210 | *
211 | * @return null|string
212 | */
213 | public function getVersion()
214 | {
215 | return $this->version;
216 | }
217 |
218 | /**
219 | * Cast to JSON
220 | *
221 | * @return string
222 | */
223 | public function toJson()
224 | {
225 | $response = ['id' => $this->getId()];
226 |
227 | if ($this->isError()) {
228 | $response['error'] = $this->getError()->toArray();
229 | } else {
230 | $response['result'] = $this->getResult();
231 | }
232 |
233 | if (null !== ($version = $this->getVersion())) {
234 | $response['jsonrpc'] = $version;
235 | }
236 |
237 | return Json::encode($response);
238 | }
239 |
240 | /**
241 | * Retrieve args.
242 | *
243 | * @return mixed
244 | */
245 | public function getArgs()
246 | {
247 | return $this->args;
248 | }
249 |
250 | /**
251 | * Set args.
252 | *
253 | * @param mixed $args
254 | * @return self
255 | */
256 | public function setArgs($args)
257 | {
258 | $this->args = $args;
259 | return $this;
260 | }
261 |
262 | /**
263 | * Set service map object.
264 | *
265 | * @param Smd $serviceMap
266 | * @return self
267 | */
268 | public function setServiceMap($serviceMap)
269 | {
270 | $this->serviceMap = $serviceMap;
271 | return $this;
272 | }
273 |
274 | /**
275 | * Retrieve service map.
276 | *
277 | * @return Smd|null
278 | */
279 | public function getServiceMap()
280 | {
281 | return $this->serviceMap;
282 | }
283 |
284 | /**
285 | * Cast to string (JSON).
286 | *
287 | * @return string
288 | */
289 | public function __toString()
290 | {
291 | return $this->toJson();
292 | }
293 | }
294 |
--------------------------------------------------------------------------------
/src/Response/Http.php:
--------------------------------------------------------------------------------
1 | sendHeaders();
26 |
27 | if (! $this->isError() && null === $this->getId()) {
28 | return '';
29 | }
30 |
31 | return parent::toJson();
32 | }
33 |
34 | /**
35 | * Send headers
36 | *
37 | * If headers are already sent, do nothing.
38 | *
39 | * If null ID, send HTTP 204 header.
40 | *
41 | * Otherwise, send content type header based on content type of service
42 | * map.
43 | *
44 | * @return void
45 | */
46 | public function sendHeaders()
47 | {
48 | if (headers_sent()) {
49 | return;
50 | }
51 |
52 | if (! $this->isError() && (null === $this->getId())) {
53 | header('HTTP/1.1 204 No Content');
54 | return;
55 | }
56 |
57 | if (null === ($smd = $this->getServiceMap())) {
58 | return;
59 | }
60 |
61 | $contentType = $smd->getContentType();
62 | if (! empty($contentType)) {
63 | header('Content-Type: ' . $contentType);
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Server.php:
--------------------------------------------------------------------------------
1 | getMethods();
116 | $found = false;
117 | foreach ($methods as $method) {
118 | if ($action === $method->getName()) {
119 | $found = true;
120 | break;
121 | }
122 | }
123 | if (! $found) {
124 | $this->fault('Method not found', Error::ERROR_INVALID_METHOD);
125 | return $this;
126 | }
127 | }
128 |
129 | $definition = $this->_buildSignature($method, $class);
130 | $this->addMethodServiceMap($definition);
131 |
132 | return $this;
133 | }
134 |
135 | /**
136 | * Register a class with the server.
137 | *
138 | * @param string $class
139 | * @param string $namespace Ignored
140 | * @param mixed $argv Ignored
141 | * @return Server
142 | */
143 | public function setClass($class, $namespace = '', $argv = null)
144 | {
145 | if (2 < func_num_args()) {
146 | $argv = func_get_args();
147 | $argv = array_slice($argv, 2);
148 | }
149 |
150 | $reflection = Reflection::reflectClass($class, $argv, $namespace);
151 |
152 | foreach ($reflection->getMethods() as $method) {
153 | $definition = $this->_buildSignature($method, $class);
154 | $this->addMethodServiceMap($definition);
155 | }
156 |
157 | return $this;
158 | }
159 |
160 | /**
161 | * Indicate fault response.
162 | *
163 | * @param string $fault
164 | * @param int $code
165 | * @param mixed $data
166 | * @return Error
167 | */
168 | public function fault($fault = null, $code = 404, $data = null)
169 | {
170 | $error = new Error($fault, $code, $data);
171 | $this->getResponse()->setError($error);
172 | return $error;
173 | }
174 |
175 | /**
176 | * Handle request.
177 | *
178 | * @param Request|false $request
179 | * @return null|Response
180 | * @throws Exception\InvalidArgumentException
181 | */
182 | public function handle($request = false)
183 | {
184 | if ((false !== $request) && ! $request instanceof Request) {
185 | throw new Exception\InvalidArgumentException('Invalid request type provided; cannot handle');
186 | }
187 |
188 | if ($request) {
189 | $this->setRequest($request);
190 | }
191 |
192 | // Handle request
193 | $this->handleRequest();
194 |
195 | // Get response
196 | $response = $this->getReadyResponse();
197 |
198 | // Emit response?
199 | if (! $this->returnResponse) {
200 | echo $response;
201 | return;
202 | }
203 |
204 | // or return it?
205 | return $response;
206 | }
207 |
208 | /**
209 | * Load function definitions
210 | *
211 | * @param array|Definition $definition
212 | * @throws Exception\InvalidArgumentException
213 | * @return void
214 | */
215 | public function loadFunctions($definition)
216 | {
217 | if (! is_array($definition) && ! $definition instanceof Definition) {
218 | throw new Exception\InvalidArgumentException('Invalid definition provided to loadFunctions()');
219 | }
220 |
221 | foreach ($definition as $key => $method) {
222 | $this->table->addMethod($method, $key);
223 | $this->addMethodServiceMap($method);
224 | }
225 | }
226 |
227 | /**
228 | * Cache/persist server (unused)
229 | *
230 | * @param int $mode
231 | * @return void
232 | */
233 | public function setPersistence($mode)
234 | {
235 | }
236 |
237 | /**
238 | * Set request object
239 | *
240 | * @return self
241 | */
242 | public function setRequest(Request $request)
243 | {
244 | $this->request = $request;
245 | return $this;
246 | }
247 |
248 | /**
249 | * Get JSON-RPC request object.
250 | *
251 | * Lazy-loads an instance if none previously available.
252 | *
253 | * @return Request
254 | */
255 | public function getRequest()
256 | {
257 | if (null === $this->request) {
258 | $this->setRequest(new Request\Http());
259 | }
260 |
261 | return $this->request;
262 | }
263 |
264 | /**
265 | * Set response object.
266 | *
267 | * @return self
268 | */
269 | public function setResponse(Response $response)
270 | {
271 | $this->response = $response;
272 | return $this;
273 | }
274 |
275 | /**
276 | * Get response object.
277 | *
278 | * Lazy-loads an instance if none previously available.
279 | *
280 | * @return Response
281 | */
282 | public function getResponse()
283 | {
284 | if (null === $this->response) {
285 | $this->setResponse(new Response\Http());
286 | }
287 |
288 | return $this->response;
289 | }
290 |
291 | /**
292 | * Set return response flag.
293 | *
294 | * If true, {@link handle()} will return the response instead of
295 | * automatically sending it back to the requesting client.
296 | *
297 | * The response is always available via {@link getResponse()}.
298 | *
299 | * @param bool $flag
300 | * @return self
301 | */
302 | public function setReturnResponse($flag = true)
303 | {
304 | $this->returnResponse = (bool) $flag;
305 | return $this;
306 | }
307 |
308 | /**
309 | * Retrieve return response flag.
310 | *
311 | * @return bool
312 | */
313 | public function getReturnResponse()
314 | {
315 | return $this->returnResponse;
316 | }
317 |
318 | /**
319 | * Overload to accessors of SMD object.
320 | *
321 | * @param string $method
322 | * @param array $args
323 | * @return mixed
324 | */
325 | public function __call($method, $args)
326 | {
327 | if (! preg_match('/^(set|get)/', $method, $matches)) {
328 | return;
329 | }
330 |
331 | if (! in_array($method, $this->getSmdMethods())) {
332 | return;
333 | }
334 |
335 | if ('set' === $matches[1]) {
336 | $value = array_shift($args);
337 | $this->getServiceMap()->$method($value);
338 | return $this;
339 | }
340 |
341 | return $this->getServiceMap()->$method();
342 | }
343 |
344 | /**
345 | * Retrieve SMD object.
346 | *
347 | * Lazy loads an instance if not previously set.
348 | *
349 | * @return Smd
350 | */
351 | public function getServiceMap()
352 | {
353 | if (null === $this->serviceMap) {
354 | $this->serviceMap = new Smd();
355 | }
356 | return $this->serviceMap;
357 | }
358 |
359 | /**
360 | * Add service method to service map.
361 | *
362 | * @return void
363 | */
364 | protected function addMethodServiceMap(Method\Definition $method)
365 | {
366 | $serviceInfo = [
367 | 'name' => $method->getName(),
368 | 'return' => $this->getReturnType($method),
369 | ];
370 |
371 | $params = $this->getParams($method);
372 | $serviceInfo['params'] = $params;
373 | $serviceMap = $this->getServiceMap();
374 |
375 | if (false !== $serviceMap->getService($serviceInfo['name'])) {
376 | $serviceMap->removeService($serviceInfo['name']);
377 | }
378 |
379 | $serviceMap->addService($serviceInfo);
380 | }
381 |
382 | // @codingStandardsIgnoreStart
383 | /**
384 | * Translate PHP type to JSON type.
385 | *
386 | * Method defined in AbstractServer as abstract and implemented here.
387 | *
388 | * @param string $type
389 | * @return string
390 | */
391 | protected function _fixType($type)
392 | {
393 | return $type;
394 | }
395 | // @codingStandardsIgnoreEnd
396 |
397 | /**
398 | * Get default params from signature.
399 | *
400 | * @param array $args
401 | * @param array $params
402 | * @return array
403 | */
404 | protected function getDefaultParams(array $args, array $params)
405 | {
406 | if (false === $this->isAssociative($args)) {
407 | $params = array_slice($params, count($args));
408 | }
409 |
410 | foreach ($params as $param) {
411 | if (isset($args[$param['name']]) || ! array_key_exists('default', $param)) {
412 | continue;
413 | }
414 |
415 | $args[$param['name']] = $param['default'];
416 | }
417 |
418 | return $args;
419 | }
420 |
421 | /**
422 | * Check whether array is associative or not.
423 | *
424 | * @param array $array
425 | * @return bool
426 | */
427 | private function isAssociative(array $array)
428 | {
429 | $keys = array_keys($array);
430 | return $keys !== array_keys($keys);
431 | }
432 |
433 | /**
434 | * Get method param type.
435 | *
436 | * @return string|array
437 | */
438 | protected function getParams(Method\Definition $method)
439 | {
440 | $params = [];
441 | foreach ($method->getPrototypes() as $prototype) {
442 | foreach ($prototype->getParameterObjects() as $key => $parameter) {
443 | if (! isset($params[$key])) {
444 | $params[$key] = [
445 | 'type' => $parameter->getType(),
446 | 'name' => $parameter->getName(),
447 | 'optional' => $parameter->isOptional(),
448 | ];
449 |
450 | if (null !== ($default = $parameter->getDefaultValue())) {
451 | $params[$key]['default'] = $default;
452 | }
453 |
454 | $description = $parameter->getDescription();
455 |
456 | if (! empty($description)) {
457 | $params[$key]['description'] = $description;
458 | }
459 |
460 | continue;
461 | }
462 |
463 | $newType = $parameter->getType();
464 |
465 | if (
466 | is_array($params[$key]['type'])
467 | && in_array($newType, $params[$key]['type'])
468 | ) {
469 | continue;
470 | }
471 |
472 | if (
473 | ! is_array($params[$key]['type'])
474 | && $params[$key]['type'] === $newType
475 | ) {
476 | continue;
477 | }
478 |
479 | if (! is_array($params[$key]['type'])) {
480 | $params[$key]['type'] = (array) $params[$key]['type'];
481 | }
482 |
483 | array_push($params[$key]['type'], $parameter->getType());
484 | }
485 | }
486 |
487 | return $params;
488 | }
489 |
490 | /**
491 | * Set response state.
492 | *
493 | * @return Response
494 | */
495 | protected function getReadyResponse()
496 | {
497 | $request = $this->getRequest();
498 | $response = $this->getResponse();
499 | $response->setServiceMap($this->getServiceMap());
500 |
501 | if (null !== ($id = $request->getId())) {
502 | $response->setId($id);
503 | }
504 |
505 | if (null !== ($version = $request->getVersion())) {
506 | $response->setVersion($version);
507 | }
508 |
509 | return $response;
510 | }
511 |
512 | /**
513 | * Get method return type.
514 | *
515 | * @return string|array
516 | */
517 | protected function getReturnType(Method\Definition $method)
518 | {
519 | $return = [];
520 | foreach ($method->getPrototypes() as $prototype) {
521 | $return[] = $prototype->getReturnType();
522 | }
523 |
524 | if (1 === count($return)) {
525 | return $return[0];
526 | }
527 |
528 | return $return;
529 | }
530 |
531 | /**
532 | * Retrieve list of allowed SMD methods for proxying.
533 | *
534 | * Lazy-loads the list on first retrieval.
535 | *
536 | * @return array
537 | */
538 | protected function getSmdMethods()
539 | {
540 | if (null !== $this->smdMethods) {
541 | return $this->smdMethods;
542 | }
543 |
544 | $this->smdMethods = [];
545 |
546 | foreach (get_class_methods(Smd::class) as $method) {
547 | if (! preg_match('/^(set|get)/', $method)) {
548 | continue;
549 | }
550 |
551 | if (strstr($method, 'Service')) {
552 | continue;
553 | }
554 |
555 | $this->smdMethods[] = $method;
556 | }
557 |
558 | return $this->smdMethods;
559 | }
560 |
561 | /**
562 | * Internal method for handling request.
563 | *
564 | * @return Error|null
565 | */
566 | protected function handleRequest()
567 | {
568 | $request = $this->getRequest();
569 |
570 | if ($request->isParseError()) {
571 | return $this->fault('Parse error', Error::ERROR_PARSE);
572 | }
573 |
574 | if (! $request->isMethodError() && null === $request->getMethod()) {
575 | return $this->fault('Invalid Request', Error::ERROR_INVALID_REQUEST);
576 | }
577 |
578 | if ($request->isMethodError()) {
579 | return $this->fault('Invalid Request', Error::ERROR_INVALID_REQUEST);
580 | }
581 |
582 | $method = $request->getMethod();
583 | if (! $this->table->hasMethod($method)) {
584 | return $this->fault('Method not found', Error::ERROR_INVALID_METHOD);
585 | }
586 |
587 | $invokable = $this->table->getMethod($method);
588 | $serviceMap = $this->getServiceMap();
589 | $service = $serviceMap->getService($method);
590 | $params = $this->validateAndPrepareParams(
591 | $request->getParams(),
592 | $service->getParams(),
593 | $invokable
594 | );
595 |
596 | if ($params instanceof Error) {
597 | return $params;
598 | }
599 |
600 | try {
601 | $result = $this->_dispatch($invokable, $params);
602 | } catch (PhpException $e) {
603 | return $this->fault($e->getMessage(), $e->getCode(), $e);
604 | }
605 |
606 | $this->getResponse()->setResult($result);
607 |
608 | return null;
609 | }
610 |
611 | /**
612 | * @param array $requestedParams
613 | * @param array $serviceParams
614 | * @return array|Error Array of parameters to use when calling the requested
615 | * method on success, Error if there is a mismatch between request
616 | * parameters and the method signature.
617 | */
618 | private function validateAndPrepareParams(
619 | array $requestedParams,
620 | array $serviceParams,
621 | Method\Definition $invokable
622 | ) {
623 | return is_string(key($requestedParams))
624 | ? $this->validateAndPrepareNamedParams($requestedParams, $serviceParams, $invokable)
625 | : $this->validateAndPrepareOrderedParams($requestedParams, $serviceParams);
626 | }
627 |
628 | /**
629 | * Ensures named parameters are passed in the correct order.
630 | *
631 | * @param array $requestedParams
632 | * @param array $serviceParams
633 | * @return array|Error Array of parameters to use when calling the requested
634 | * method on success, Error if any named request parameters do not match
635 | * those of the method requested.
636 | */
637 | private function validateAndPrepareNamedParams(
638 | array $requestedParams,
639 | array $serviceParams,
640 | Method\Definition $invokable
641 | ) {
642 | if (count($requestedParams) < count($serviceParams)) {
643 | $requestedParams = $this->getDefaultParams($requestedParams, $serviceParams);
644 | }
645 |
646 | $callback = $invokable->getCallback();
647 | $reflection = 'function' === $callback->getType()
648 | ? new ReflectionFunction($callback->getFunction())
649 | : new ReflectionMethod($callback->getClass(), $callback->getMethod());
650 |
651 | $orderedParams = [];
652 | foreach ($reflection->getParameters() as $refParam) {
653 | if (array_key_exists($refParam->getName(), $requestedParams)) {
654 | $orderedParams[$refParam->getName()] = $requestedParams[$refParam->getName()];
655 | continue;
656 | }
657 |
658 | if ($refParam->isOptional()) {
659 | $orderedParams[$refParam->getName()] = null;
660 | continue;
661 | }
662 |
663 | return $this->fault('Invalid params', Error::ERROR_INVALID_PARAMS);
664 | }
665 |
666 | return $orderedParams;
667 | }
668 |
669 | /**
670 | * @param array $requestedParams
671 | * @param array $serviceParams
672 | * @return array|Error Array of parameters to use when calling the requested
673 | * method on success, Error if the number of request parameters does not
674 | * match the number of parameters required by the requested method.
675 | */
676 | private function validateAndPrepareOrderedParams(array $requestedParams, array $serviceParams)
677 | {
678 | $requiredParamsCount = array_reduce($serviceParams, static function ($count, $param) {
679 | $count += $param['optional'] ? 0 : 1;
680 | return $count;
681 | }, 0);
682 |
683 | if (count($requestedParams) < $requiredParamsCount) {
684 | return $this->fault('Invalid params', Error::ERROR_INVALID_PARAMS);
685 | }
686 |
687 | return $requestedParams;
688 | }
689 | }
690 |
--------------------------------------------------------------------------------
/src/Smd.php:
--------------------------------------------------------------------------------
1 | $value) {
115 | $method = 'set' . ucfirst($key);
116 |
117 | if (method_exists($this, $method)) {
118 | $this->$method($value);
119 | }
120 | }
121 |
122 | return $this;
123 | }
124 |
125 | /**
126 | * Set transport.
127 | *
128 | * @param string $transport
129 | * @return self
130 | * @throws InvalidArgumentException
131 | */
132 | public function setTransport($transport)
133 | {
134 | if (! in_array($transport, $this->transportTypes)) {
135 | throw new InvalidArgumentException("Invalid transport '{$transport}' specified");
136 | }
137 |
138 | $this->transport = $transport;
139 | return $this;
140 | }
141 |
142 | /**
143 | * Get transport.
144 | *
145 | * @return string
146 | */
147 | public function getTransport()
148 | {
149 | return $this->transport;
150 | }
151 |
152 | /**
153 | * Set envelope.
154 | *
155 | * @param string $envelopeType
156 | * @return self
157 | * @throws InvalidArgumentException
158 | */
159 | public function setEnvelope($envelopeType)
160 | {
161 | if (! in_array($envelopeType, $this->envelopeTypes)) {
162 | throw new InvalidArgumentException("Invalid envelope type '{$envelopeType}'");
163 | }
164 |
165 | $this->envelope = $envelopeType;
166 | return $this;
167 | }
168 |
169 | /**
170 | * Retrieve envelope.
171 | *
172 | * @return string
173 | */
174 | public function getEnvelope()
175 | {
176 | return $this->envelope;
177 | }
178 |
179 | /**
180 | * Set content type
181 | *
182 | * @param string $type
183 | * @return self
184 | * @throws InvalidArgumentException
185 | */
186 | public function setContentType($type)
187 | {
188 | if (! preg_match($this->contentTypeRegex, $type)) {
189 | throw new InvalidArgumentException("Invalid content type '{$type}' specified");
190 | }
191 |
192 | $this->contentType = $type;
193 | return $this;
194 | }
195 |
196 | /**
197 | * Retrieve content type
198 | *
199 | * Content-Type of response; default to application/json.
200 | *
201 | * @return string
202 | */
203 | public function getContentType()
204 | {
205 | return $this->contentType;
206 | }
207 |
208 | /**
209 | * Set service target.
210 | *
211 | * @param string $target
212 | * @return self
213 | */
214 | public function setTarget($target)
215 | {
216 | $this->target = (string) $target;
217 | return $this;
218 | }
219 |
220 | /**
221 | * Retrieve service target.
222 | *
223 | * @return string
224 | */
225 | public function getTarget()
226 | {
227 | return $this->target;
228 | }
229 |
230 | /**
231 | * Set service ID.
232 | *
233 | * @param string $id
234 | * @return self
235 | */
236 | public function setId($id)
237 | {
238 | $this->id = (string) $id;
239 | return $this;
240 | }
241 |
242 | /**
243 | * Get service id.
244 | *
245 | * @return string
246 | */
247 | public function getId()
248 | {
249 | return $this->id;
250 | }
251 |
252 | /**
253 | * Set service description.
254 | *
255 | * @param string $description
256 | * @return self
257 | */
258 | public function setDescription($description)
259 | {
260 | $this->description = (string) $description;
261 | return $this;
262 | }
263 |
264 | /**
265 | * Get service description.
266 | *
267 | * @return string
268 | */
269 | public function getDescription()
270 | {
271 | return $this->description;
272 | }
273 |
274 | /**
275 | * Indicate whether or not to generate Dojo-compatible SMD.
276 | *
277 | * @param bool $flag
278 | * @return self
279 | */
280 | public function setDojoCompatible($flag)
281 | {
282 | $this->dojoCompatible = (bool) $flag;
283 | return $this;
284 | }
285 |
286 | /**
287 | * Is this a Dojo compatible SMD?
288 | *
289 | * @return bool
290 | */
291 | public function isDojoCompatible()
292 | {
293 | return $this->dojoCompatible;
294 | }
295 |
296 | /**
297 | * Add Service.
298 | *
299 | * @param Smd\Service|array $service
300 | * @return self
301 | * @throws RuntimeException
302 | * @throws InvalidArgumentException
303 | */
304 | public function addService($service)
305 | {
306 | if (is_array($service)) {
307 | $service = new Smd\Service($service);
308 | }
309 |
310 | if (! $service instanceof Smd\Service) {
311 | throw new InvalidArgumentException('Invalid service passed to addService()');
312 | }
313 |
314 | $name = $service->getName();
315 |
316 | if (array_key_exists($name, $this->services)) {
317 | throw new RuntimeException('Attempt to register a service already registered detected');
318 | }
319 |
320 | $this->services[$name] = $service;
321 | return $this;
322 | }
323 |
324 | /**
325 | * Add many services.
326 | *
327 | * @param array $services
328 | * @return self
329 | */
330 | public function addServices(array $services)
331 | {
332 | foreach ($services as $service) {
333 | $this->addService($service);
334 | }
335 |
336 | return $this;
337 | }
338 |
339 | /**
340 | * Overwrite existing services with new ones.
341 | *
342 | * @param array $services
343 | * @return self
344 | */
345 | public function setServices(array $services)
346 | {
347 | $this->services = [];
348 | return $this->addServices($services);
349 | }
350 |
351 | /**
352 | * Get service object.
353 | *
354 | * @param string $name
355 | * @return bool|Smd\Service
356 | */
357 | public function getService($name)
358 | {
359 | if (! array_key_exists($name, $this->services)) {
360 | return false;
361 | }
362 |
363 | return $this->services[$name];
364 | }
365 |
366 | /**
367 | * Return services.
368 | *
369 | * @return array
370 | */
371 | public function getServices()
372 | {
373 | return $this->services;
374 | }
375 |
376 | /**
377 | * Remove service.
378 | *
379 | * @param string $name
380 | * @return bool
381 | */
382 | public function removeService($name)
383 | {
384 | if (! array_key_exists($name, $this->services)) {
385 | return false;
386 | }
387 |
388 | unset($this->services[$name]);
389 | return true;
390 | }
391 |
392 | /**
393 | * Cast to array.
394 | *
395 | * @return array
396 | */
397 | public function toArray()
398 | {
399 | if ($this->isDojoCompatible()) {
400 | return $this->toDojoArray();
401 | }
402 |
403 | $description = $this->getDescription();
404 | $transport = $this->getTransport();
405 | $envelope = $this->getEnvelope();
406 | $contentType = $this->getContentType();
407 | $smdVersion = static::SMD_VERSION;
408 | assert(is_string($smdVersion));
409 | $service = [
410 | 'transport' => $transport,
411 | 'envelope' => $envelope,
412 | 'contentType' => $contentType,
413 | 'SMDVersion' => $smdVersion,
414 | 'description' => $description,
415 | ];
416 |
417 | if (null !== ($target = $this->getTarget())) {
418 | $service['target'] = $target;
419 | }
420 | if (null !== ($id = $this->getId())) {
421 | $service['id'] = $id;
422 | }
423 |
424 | $services = $this->getServices();
425 | if (empty($services)) {
426 | return $service;
427 | }
428 |
429 | $service['services'] = [];
430 | foreach ($services as $name => $svc) {
431 | $svc->setEnvelope($envelope);
432 | $service['services'][$name] = $svc->toArray();
433 | }
434 | $service['methods'] = $service['services'];
435 |
436 | return $service;
437 | }
438 |
439 | /**
440 | * Export to DOJO-compatible SMD array
441 | *
442 | * @return array
443 | */
444 | public function toDojoArray()
445 | {
446 | $smdVersion = '.1';
447 | $serviceType = 'JSON-RPC';
448 | $service = ['SMDVersion' => $smdVersion, 'serviceType' => $serviceType];
449 | $target = $this->getTarget();
450 | $services = $this->getServices();
451 |
452 | if (empty($services)) {
453 | return $service;
454 | }
455 |
456 | $service['methods'] = [];
457 | foreach ($services as $name => $svc) {
458 | $method = [
459 | 'name' => $name,
460 | 'serviceURL' => $target,
461 | ];
462 |
463 | $params = [];
464 | foreach ($svc->getParams() as $param) {
465 | $params[] = [
466 | 'name' => array_key_exists('name', $param) ? $param['name'] : $param['type'],
467 | 'type' => $param['type'],
468 | ];
469 | }
470 |
471 | if (! empty($params)) {
472 | $method['parameters'] = $params;
473 | }
474 |
475 | $service['methods'][] = $method;
476 | }
477 |
478 | return $service;
479 | }
480 |
481 | /**
482 | * Cast to JSON.
483 | *
484 | * @return string
485 | */
486 | public function toJson()
487 | {
488 | return Json::encode($this->toArray());
489 | }
490 |
491 | /**
492 | * Cast to string (JSON)
493 | *
494 | * @return string
495 | */
496 | public function __toString()
497 | {
498 | return $this->toJson();
499 | }
500 | }
501 |
--------------------------------------------------------------------------------
/src/Smd/Service.php:
--------------------------------------------------------------------------------
1 | 'is_string',
73 | 'optional' => 'is_bool',
74 | 'default' => null,
75 | 'description' => 'is_string',
76 | ];
77 |
78 | /**
79 | * Service params.
80 | *
81 | * @var array
82 | */
83 | protected $params = [];
84 |
85 | /**
86 | * Mapping of parameter types to JSON-RPC types.
87 | *
88 | * @var array
89 | */
90 | protected $paramMap = [
91 | 'any' => 'any',
92 | 'arr' => 'array',
93 | 'array' => 'array',
94 | 'assoc' => 'object',
95 | 'bool' => 'boolean',
96 | 'boolean' => 'boolean',
97 | 'dbl' => 'float',
98 | 'double' => 'float',
99 | 'false' => 'boolean',
100 | 'float' => 'float',
101 | 'hash' => 'object',
102 | 'integer' => 'integer',
103 | 'int' => 'integer',
104 | 'mixed' => 'any',
105 | 'nil' => 'null',
106 | 'null' => 'null',
107 | 'object' => 'object',
108 | 'string' => 'string',
109 | 'str' => 'string',
110 | 'struct' => 'object',
111 | 'true' => 'boolean',
112 | 'void' => 'null',
113 | ];
114 |
115 | /**
116 | * Allowed transport types.
117 | *
118 | * @var array
119 | */
120 | protected $transportTypes = [
121 | 'POST',
122 | ];
123 |
124 | /**
125 | * @param string|array $spec
126 | * @throws InvalidArgumentException If no name provided.
127 | */
128 | public function __construct($spec)
129 | {
130 | if (is_string($spec)) {
131 | $this->setName($spec);
132 | } elseif (is_array($spec)) {
133 | $this->setOptions($spec);
134 | }
135 |
136 | if ('' === $this->getName()) {
137 | throw new InvalidArgumentException('SMD service description requires a name; none provided');
138 | }
139 | }
140 |
141 | /**
142 | * Set object state.
143 | *
144 | * @param array $options
145 | * @return self
146 | */
147 | public function setOptions(array $options)
148 | {
149 | $methods = get_class_methods($this);
150 | foreach ($options as $key => $value) {
151 | if ('options' === strtolower($key)) {
152 | continue;
153 | }
154 |
155 | $method = 'set' . ucfirst($key);
156 | if (in_array($method, $methods)) {
157 | $this->$method($value);
158 | }
159 | }
160 |
161 | return $this;
162 | }
163 |
164 | /**
165 | * Set service name.
166 | *
167 | * @return self
168 | * @throws InvalidArgumentException
169 | */
170 | public function setName(string $name)
171 | {
172 | if (! preg_match($this->nameRegex, $name)) {
173 | throw new InvalidArgumentException(sprintf(
174 | 'Invalid name "%s" provided for service; must follow PHP method naming conventions',
175 | $name
176 | ));
177 | }
178 |
179 | $this->name = $name;
180 | return $this;
181 | }
182 |
183 | /**
184 | * Retrieve name.
185 | *
186 | * @return string
187 | */
188 | public function getName()
189 | {
190 | return $this->name;
191 | }
192 |
193 | /**
194 | * Set Transport.
195 | *
196 | * Currently limited to POST.
197 | *
198 | * @param string $transport
199 | * @return self
200 | * @throws InvalidArgumentException
201 | */
202 | public function setTransport($transport)
203 | {
204 | if (! in_array($transport, $this->transportTypes)) {
205 | throw new InvalidArgumentException(sprintf(
206 | 'Invalid transport "%s"; please select one of (%s)',
207 | $transport,
208 | implode(', ', $this->transportTypes)
209 | ));
210 | }
211 |
212 | $this->transport = $transport;
213 | return $this;
214 | }
215 |
216 | /**
217 | * Get transport.
218 | *
219 | * @return string
220 | */
221 | public function getTransport()
222 | {
223 | return $this->transport;
224 | }
225 |
226 | /**
227 | * Set service target.
228 | *
229 | * @param string $target
230 | * @return self
231 | */
232 | public function setTarget($target)
233 | {
234 | $this->target = (string) $target;
235 | return $this;
236 | }
237 |
238 | /**
239 | * Get service target.
240 | *
241 | * @return null|string
242 | */
243 | public function getTarget()
244 | {
245 | return $this->target;
246 | }
247 |
248 | /**
249 | * Set envelope type.
250 | *
251 | * @param string $envelopeType
252 | * @return self
253 | * @throws InvalidArgumentException
254 | */
255 | public function setEnvelope($envelopeType)
256 | {
257 | if (! in_array($envelopeType, $this->envelopeTypes)) {
258 | throw new InvalidArgumentException(sprintf(
259 | 'Invalid envelope type "%s"; please specify one of (%s)',
260 | $envelopeType,
261 | implode(', ', $this->envelopeTypes)
262 | ));
263 | }
264 |
265 | $this->envelope = $envelopeType;
266 | return $this;
267 | }
268 |
269 | /**
270 | * Get envelope type.
271 | *
272 | * @return string
273 | */
274 | public function getEnvelope()
275 | {
276 | return $this->envelope;
277 | }
278 |
279 | /**
280 | * Add a parameter to the service.
281 | *
282 | * @param string|array $type
283 | * @param array $options
284 | * @param int|null $order
285 | * @return self
286 | * @throws InvalidArgumentException
287 | */
288 | public function addParam($type, array $options = [], $order = null)
289 | {
290 | if (! is_string($type) && ! is_array($type)) {
291 | throw new InvalidArgumentException('Invalid param type provided');
292 | }
293 |
294 | if (is_string($type)) {
295 | $type = $this->validateParamType($type);
296 | }
297 |
298 | if (is_array($type)) {
299 | foreach ($type as $key => $paramType) {
300 | $type[$key] = $this->validateParamType($paramType);
301 | }
302 | }
303 |
304 | $paramOptions = ['type' => $type];
305 | foreach ($options as $key => $value) {
306 | if (in_array($key, array_keys($this->paramOptionTypes))) {
307 | if (null !== ($callback = $this->paramOptionTypes[$key])) {
308 | if (! $callback($value)) {
309 | continue;
310 | }
311 | }
312 | $paramOptions[$key] = $value;
313 | }
314 | }
315 |
316 | $this->params[] = [
317 | 'param' => $paramOptions,
318 | 'order' => $order,
319 | ];
320 |
321 | return $this;
322 | }
323 |
324 | /**
325 | * Add params.
326 | *
327 | * Each param should be an array, and should include the key 'type'.
328 | *
329 | * @param array $params
330 | * @return self
331 | */
332 | public function addParams(array $params)
333 | {
334 | ksort($params);
335 |
336 | foreach ($params as $options) {
337 | if (! is_array($options)) {
338 | continue;
339 | }
340 |
341 | if (! array_key_exists('type', $options)) {
342 | continue;
343 | }
344 |
345 | $type = $options['type'];
346 | $order = array_key_exists('order', $options) ? $options['order'] : null;
347 | $this->addParam($type, $options, $order);
348 | }
349 |
350 | return $this;
351 | }
352 |
353 | /**
354 | * Overwrite all parameters.
355 | *
356 | * @param array $params
357 | * @return self
358 | */
359 | public function setParams(array $params)
360 | {
361 | $this->params = [];
362 | return $this->addParams($params);
363 | }
364 |
365 | /**
366 | * Get all parameters.
367 | *
368 | * Returns all params in specified order.
369 | *
370 | * @return array
371 | */
372 | public function getParams()
373 | {
374 | $params = [];
375 | $index = 0;
376 |
377 | foreach ($this->params as $param) {
378 | if (null === $param['order']) {
379 | if (array_search($index, array_keys($params), true)) {
380 | ++$index;
381 | }
382 |
383 | $params[$index] = $param['param'];
384 | ++$index;
385 | continue;
386 | }
387 |
388 | $params[$param['order']] = $param['param'];
389 | }
390 |
391 | ksort($params);
392 | return $params;
393 | }
394 |
395 | /**
396 | * Set return type.
397 | *
398 | * @param string|array $type
399 | * @return self
400 | * @throws InvalidArgumentException
401 | */
402 | public function setReturn($type)
403 | {
404 | if (! is_string($type) && ! is_array($type)) {
405 | throw new InvalidArgumentException("Invalid param type provided ('" . gettype($type) . "')");
406 | }
407 |
408 | if (is_string($type)) {
409 | $type = $this->validateParamType($type, true);
410 | }
411 |
412 | if (is_array($type)) {
413 | foreach ($type as $key => $returnType) {
414 | $type[$key] = $this->validateParamType($returnType, true);
415 | }
416 | }
417 |
418 | $this->return = $type;
419 | return $this;
420 | }
421 |
422 | /**
423 | * Get return type.
424 | *
425 | * @return null|string|array
426 | */
427 | public function getReturn()
428 | {
429 | return $this->return;
430 | }
431 |
432 | /**
433 | * Cast service description to array.
434 | *
435 | * @return array
436 | */
437 | public function toArray()
438 | {
439 | $envelope = $this->getEnvelope();
440 | $target = $this->getTarget();
441 | $transport = $this->getTransport();
442 | $parameters = $this->getParams();
443 | $returns = $this->getReturn();
444 | $name = $this->getName();
445 |
446 | if (empty($target)) {
447 | return [
448 | 'envelope' => $envelope,
449 | 'transport' => $transport,
450 | 'name' => $name,
451 | 'parameters' => $parameters,
452 | 'returns' => $returns,
453 | ];
454 | }
455 |
456 | return [
457 | 'envelope' => $envelope,
458 | 'target' => $target,
459 | 'transport' => $transport,
460 | 'name' => $name,
461 | 'parameters' => $parameters,
462 | 'returns' => $returns,
463 | ];
464 | }
465 |
466 | /**
467 | * Return JSON encoding of service.
468 | *
469 | * @return string
470 | */
471 | public function toJson()
472 | {
473 | return Json::encode([
474 | $this->getName() => $this->toArray(),
475 | ]);
476 | }
477 |
478 | /**
479 | * Cast to string.
480 | *
481 | * @return string
482 | */
483 | public function __toString()
484 | {
485 | return $this->toJson();
486 | }
487 |
488 | /**
489 | * Validate parameter type.
490 | *
491 | * @param string $type
492 | * @param bool $isReturn
493 | * @return string
494 | * @throws InvalidArgumentException
495 | */
496 | protected function validateParamType($type, $isReturn = false)
497 | {
498 | if (! is_string($type)) {
499 | throw new InvalidArgumentException(sprintf(
500 | 'Invalid param type provided ("%s")',
501 | $type
502 | ));
503 | }
504 |
505 | if (! array_key_exists($type, $this->paramMap)) {
506 | $type = 'object';
507 | }
508 |
509 | $paramType = $this->paramMap[$type];
510 | if (! $isReturn && ('null' === $paramType)) {
511 | throw new InvalidArgumentException(sprintf(
512 | 'Invalid param type provided ("%s")',
513 | $type
514 | ));
515 | }
516 |
517 | return $paramType;
518 | }
519 | }
520 |
--------------------------------------------------------------------------------