#[RPC\Response]

Last modified by Ashterix on 2024/05/19 21:13

Summary

Classname

Response

Namespace

Ufo\RpcObject\RPC

Targetmethod
Arguments:

$responseFormat

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

Enhanced Response Format

By default, the documentation for the response of each API service is based on the return type, which works well with primitive types. However, if your method returns an associative array, an object, or a collection of objects, the information about the data type of the response becomes insufficient.

To provide clients with more information about the format of more complex responses, the attribute #[RPC\Response] was created.

Let's consider an example that demonstrates the problem and its solution. You have a class containing API methods that return information about users:

  • getUserInfo(int $id) - returns a user object containing keys id, email, name
  • getUserInfoAsArray(int $id) - returns information about a user in the form of an array
  • getUsersList() - returns a collection of user objects

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();
    }
}

Let's look at the documentation generated by this instruction

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"
        }
    }
}

We are interested in returns and responseFormat (lines 19-20, 33-34, and 40-41), we see that the RPC Server interpreted User as an object and this information does not tell the client how to work with the response object, what properties it should have, nor is there information about the responses of the other two methods, in both cases an array should be returned and we know nothing about its contents.

It's time to add the Response attribute. There are variations in its use.

Option 1 - (one-time) suitable for methods that return a unique set of parameters.

Passing the $responseFormat parameter, in which as a key=>value array you need to enumerate all the parameters that the response object will have.

The key is the name of the parameter, and the value is the type of data.

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's->getName(),
        ];
    }

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

Note the changed documentation format on the right, responseFormat now has detail and provides a clear idea of the response structure.

To simplify the documentation, in the response example I will remove other elements except those intended to demonstrate 

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"
                }
            ]
        }
    }
}

 

You might notice that using an array in such an example seems inconvenient because even within just this one class we used it three times and if you need to change the structure of the user properties this array will have to be changed in many places. In other words, this violates DRY. That's why I mentioned that this option is only suitable as a one-time. In other cases, you need to use a different approach.

Option 2 - (recommended)

This option assumes that you need to create classes that will act as DTOs (Data Transfer Object), it should be a class that has the same properties as the object returned by your API method.

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's->getName(),
        ];
    }

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

To simplify the documentation, in the response example I will remove other elements except those intended to demonstrate 

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"
                }
            ]
        }
    }
}

The response format has not changed, but maintaining and expanding your API methods has become much easier and more comfortable