#[RPC\Response]

Остання зміна 2024/05/19 21:13 автором Ashterix

Summary

Classname

Response

Namespace

Ufo\RpcObject\RPC

Targetmethod
Arguments:

$responseFormat

typearray
optionaltrue
default[]
$dtotypestring
optionaltrue
default''
$collectiontypebool
optionaltrue
defaultfalse

Розширений формат відповіді

За замовченням документація щодо відповіді кожного сервісу API базується на вказівці return type, що добре працює з примітивними типами, проте в разі якщо ваш метод повертає асоціативний масив, обʼєкт або колекцію обʼєктів інформації про тип даних відповіді стає недостатньо.

Щоб надати клієнту більше інформації про формат більш складних відповідей був створений атрибут #[RPC\Response].

Розглянемо приклад який продемонструє проблематику і її вирішення. У вас є клас, що містить API методи, які повертають інформацію про користувачів: 

  • getUserInfo(int $id) - повертає обʼєкт user що містить ключі id, email, name
  • getUserInfoAsArray(int $id) - повертає інформацію про користувача у вигляді масиву
  • getUsersList() - повертає колекцію обʼєктів user
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
namespace App\Api\Procedures;

use App\Services\UserService;
use Ufo\JsonRpcBundle\ApiMethod\Interfaces\IRpcService;

class ExampleApi implements IRpcService
{
   public function __construct(
       protected UserService $userService
    ) {}

   public function getUserInfo(int $id): User
    {
       return $this->userService->getById($id);
    }

   public function getUserInfoAsArray(int $id): array
    {
       $user = $this->getUserInfo($id);
       return [
           'id' => $user->getId(),
           'email' => $user->getEmail(),
           'name' => $user->getName(),
        ];
    }

   public function getUsersList(): array
    {
       return $this->userService->getAll();
    }
}

Поглянемо на документацію, що згенерована по цій інструкції

1
2
3
4
5
6
7
8
9
10
11
12
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
41
42
43
44
45
46
47
48
49
50
{
   "envelope": "JSON-RPC-2.0/UFO-RPC-6",
   "contentType": "application/json",
   "description": "",
   "transport": {
       "sync": {
           "scheme": "https",
           "host": "example.com",
           "path": "/api",
           "method": "POST"
        }
    },
   "methods": {
       "ExampleApi.getUserInfo": {
           "name": "ExampleApi.getUserInfo",
           "description": "",
           "parameters": {
               "id": {
                   "type": "integer",
                   "name": "id",
                   "description": "",
                   "optional": false
                }
            },
           "returns": "object",
           "responseFormat": "object"
        },
       "ExampleApi.getUserInfoAsArray": {
           "name": "ExampleApi.getUserInfoAsArray",
           "description": "",
           "parameters": {
               "id": {
                   "type": "integer",
                   "name": "id",
                   "description": "",
                   "optional": false
                }
            },
           "returns": "array",
           "responseFormat": "array"
        },
       "ExampleApi.getUsersList": {
           "name": "ExampleApi.getUsersList",
           "description": "",
           "parameters": [],
           "returns": "array",
           "responseFormat": "array"
        }
    }
}

Нас цікавлять returns та responseFormat (рядки 19-20 33-34 та 40-41), бачимо, що RPC Server інтерпретував User як object і ця інформація ніяким чином не говорить клієнту про те, як працювати з обʼєктом відповіді, які властивості в нього мають бути, так само як і немає інформації щодо відповідей двох інших методів, в обох випадках має повернутися масив і ми нічого не знаємо про його вміст.

Саме час додати атрибут Response. Тут є варіативність у використанні.

Варіант 1 - (одноразовий) підходить для методів, що повертають унікальний набір параметрів.

Передача параметру $responseFormat, в якому у вигляді масиву ключ=>значення потрібно перерахувати всі параметри, що буде мати обʼєкт відповіді. 

Ключ це назва параметру, а значення це тип даних.

1
2
3
4
5
6
7
8
9
10
11
12
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
<?php
namespace App\Api\Procedures;

use App\Services\UserService;
use Ufo\JsonRpcBundle\ApiMethod\Interfaces\IRpcService;
use Ufo\RpcObject\RPC;

class ExampleApi implements IRpcService
{
   public function __construct(
       protected UserService $userService
    ) {}

   #[RPC\Response(['id' => 'int', 'email' => 'string', 'name' => 'string'])]
   public function getUserInfo(int $id): User
    {
       return $this->userService->getById($id);
    }

   #[RPC\Response(['id' => 'int', 'email' => 'string', 'name' => 'string'])]
   public function getUserInfoAsArray(int $id): array
    {
       $user = $this->getUserInfo($id);
       return [
           'id' => $user->getId(),
           'email' => $user->getEmail(),
           'name' => $user->getName(),
        ];
    }

   #[RPC\Response([['id' => 'int', 'email' => 'string', 'name' => 'string'])]]
   public function getUsersList(): array
    {
       return $this->userService->getAll();
    }
}

Зверніть увагу на змінений формат документації праворуч, responseFormat тепер має деталізацію і надає чітке уявлення про структуру відповіді.

Для спрощення документації, в прикладі відповіді я приберу інші елементи окрім тих, що маю на меті продемонструвати 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
   "methods": {
       "ExampleApi.getUserInfo": {
           "name": "ExampleApi.getUserInfo",
           ...,
           "returns": "object",
           "responseFormat": {
               "id": "int",
               "email": "string",
               "name": "string"
            }
        },
       "ExampleApi.getUserInfoAsArray": {
           "name": "ExampleApi.getUserInfoAsArray",
           ...,
           "returns": "array",
           "responseFormat": {
               "id": "int",
               "email": "string",
               "name": "string"
            }
        },
       "ExampleApi.getUsersList": {
           "name": "ExampleApi.getUsersList",
           ...,
           "returns": "array",
           "responseFormat": [
                {
                   "id": "int",
                   "email": "string",
                   "name": "string"
                }
            ]
        }
    }
}

 

Думаю, що ви звернули увагу на те, що використання масиву в такому прикладі здається не зручним, бо навіть в рамках лише одного цього класу ми використали його тричі і якщо потрібно буде змінити структуру властивостей користувача цей масив доведеться змінювати в багатьох місцях. Іншими словами це порушує DRY. Тому я й вказав, що цей варіант підходить лише як одноразовий. В інших випадках вам потрібно використовувати інший підхід.

Варіант 2 - (рекомендований)

Цей варіант передбачає, що вам потрібно створити класи що будуть виступати в ролі DTO (Data Transfer Object), це має бути клас, що має ті самі властивості, що і обʼєкт, який повертає ваш API метод.

1
2
3
4
5
6
7
8
9
<?php
namespace App\Api\DTO;

readonly class UserDTO
{
   public int $id;  
   public string $email;  
   public string $name;  
}
1
2
3
4
5
6
7
8
9
10
11
12
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
<?php
namespace App\Api\Procedures;

use App\Services\UserService;
use Ufo\JsonRpcBundle\ApiMethod\Interfaces\IRpcService;
use Ufo\RpcObject\RPC;
use App\Api\DTO\UserDTO;

class ExampleApi implements IRpcService
{
   public function __construct(
       protected UserService $userService
    ) {}

   #[RPC\Response(dto: UserDTO::class)]
   public function getUserInfo(int $id): User
    {
       return $this->userService->getById($id);
    }

   #[RPC\Response(dto: UserDTO::class)]
   public function getUserInfoAsArray(int $id): array
    {
       $user = $this->getUserInfo($id);
       return [
           'id' => $user->getId(),
           'email' => $user->getEmail(),
           'name' => $user->getName(),
        ];
    }

   #[RPC\Response(dto: UserDTO::class, collection: true)]
   public function getUsersList(): array
    {
       return $this->userService->getAll();
    }
}

Для спрощення документації, в прикладі відповіді я приберу інші елементи окрім тих, що маю на меті продемонструвати 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
   "methods": {
       "ExampleApi.getUserInfo": {
           "name": "ExampleApi.getUserInfo",
           ...,
           "returns": "object",
           "responseFormat": {
               "id": "int",
               "email": "string",
               "name": "string"
            }
        },
       "ExampleApi.getUserInfoAsArray": {
           "name": "ExampleApi.getUserInfoAsArray",
           ...,
           "returns": "array",
           "responseFormat": {
               "id": "int",
               "email": "string",
               "name": "string"
            }
        },
       "ExampleApi.getUsersList": {
           "name": "ExampleApi.getUsersList",
           ...,
           "returns": "array",
           "responseFormat": [
                {
                   "id": "int",
                   "email": "string",
                   "name": "string"
                }
            ]
        }
    }
}

Формат відповіді не змінився, але підтримувати і розширяти ваші API методи стало набагато легше і комфортніше