You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+338Lines changed: 338 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,6 +11,14 @@ For this reason, we have written this plugin, which — in addition to addressin
11
11
-[Plugin options](#plugin-options)
12
12
-[src_path](#src_path)
13
13
-[grpc](#grpc)
14
+
-[Generated code guide](#generated-code-guide)
15
+
-[numbers](#numbers)
16
+
-[repeated](#repeated)
17
+
-[maps](#maps)
18
+
-[oneof](#oneof)
19
+
-[precedence](#precedence)
20
+
-[grpc client](#grpc-client)
21
+
-[grpc server](#grpc-server)
14
22
-[Generated libraries](#generated-libraries)
15
23
-[Feature matrix](#feature-matrix)
16
24
@@ -130,6 +138,336 @@ protoc \
130
138
131
139
To generate only the server code, use `grpc=server`. By default, and when passing `grpc=client,server`, both the client and server will be generated.
132
140
141
+
### Generated code guide
142
+
143
+
As mentioned above, the plugin generates simple DTOs without setters, getters, or inheritance. All metadata for protobuf serialization is stored in attribute `Thesis\Protobuf\Reflection\*`, and the generated DTOs only have a constructor with promoted properties.
144
+
145
+
#### numbers
146
+
147
+
To avoid issues with integer overflow when using `int64/uint64` types, `\BcMath\Number` will be used.
148
+
For other numeric scalars, the `int` and `float` types will be used respectively.
149
+
150
+
#### repeated
151
+
152
+
When using `proto2`, it will be explicitly specified whether lists are packed. This is necessary because in `proto2` only lists with the corresponding option explicitly set could be packed.
153
+
Meanwhile, in `proto3`, this rule is applied implicitly, but only for types for which it was also possible in `proto2`.
154
+
In other words, for a list of `int32` in `proto2` code will be generated with an attribute like this:
155
+
```php
156
+
final readonly class Request
157
+
{
158
+
/**
159
+
* @param list<int> $ids
160
+
*/
161
+
public function __construct(
162
+
#[Reflection\Field(1, new Reflection\ListT(Reflection\Int32T::T, true))]
163
+
public array $ids = [],
164
+
) {}
165
+
}
166
+
```
167
+
168
+
Note the second argument of the `ListT` attribute: it will be set according to the `[packed = bool]` option.
169
+
For `proto3` this argument will be omitted. Also note that lists will always have `[]` as their default value, because in protobuf all values are optional.
170
+
171
+
#### maps
172
+
173
+
Since maps can have `int64/uint64` types as keys, for which we use `\BcMath\Number`, we cannot use the native `array` type, as its keys cannot be objects.
174
+
A typical solution to this problem is a list of pairs, where the key is the result of applying a hash function, which ensures fast lookup in such a `map` similar to a regular `array`.
175
+
176
+
Therefore, for all fields of type `map<K, V>`, regardless of the key type, the `\Thesis\Protobuf\Map<K, V>` type will be used for consistency.
177
+
It implements `\ArrayAccess`, `\Countable`, and `\IteratorAggregate` to smooth over the inconvenience of not being able to use a regular `array`.
178
+
179
+
```php
180
+
use Thesis\Protobuf;
181
+
use Thesis\Protobuf\Reflection;
182
+
183
+
final readonly class Request
184
+
{
185
+
/**
186
+
* @param Protobuf\Map<string,string> $options
187
+
*/
188
+
public function __construct(
189
+
#[Reflection\Field(1, new Reflection\MapT(Reflection\StringT::T, Reflection\StringT::T))]
190
+
public Protobuf\Map $options = new Protobuf\Map(),
191
+
) {}
192
+
}
193
+
```
194
+
195
+
Such fields will never be nullable (especially since maps in protobuf cannot be `required` or `optional`) and will have an empty `Map` object as its default value.
196
+
This will simplify interaction with this type.
197
+
198
+
#### oneof
199
+
200
+
Since `oneof` in protobuf can contain variants with the same data types, we cannot use a native union.
201
+
For this reason, an object is created for each variant, each of which implements a sealed interface (enabled through static analysis).
202
+
203
+
Consider the following protobuf schema:
204
+
```protobuf
205
+
syntax = "proto3";
206
+
207
+
package thesis.api;
208
+
209
+
message Request {
210
+
oneof contact {
211
+
string phone = 1;
212
+
string email = 2;
213
+
int64 chat_id = 3;
214
+
}
215
+
}
216
+
```
217
+
218
+
First of all, an `Request` object will be generated:
219
+
```php
220
+
namespace Thesis\Api\Request;
221
+
222
+
final readonly class Request
223
+
{
224
+
public function __construct(
225
+
#[Reflection\OneOf([
226
+
\Thesis\Api\Request\ContactPhone::class,
227
+
\Thesis\Api\Request\ContactEmail::class,
228
+
\Thesis\Api\Request\ContactChatId::class,
229
+
])]
230
+
public ?\Thesis\Api\Request\Contact $contact = null,
231
+
) {}
232
+
}
233
+
```
234
+
235
+
Then, an interface `Contact` will be generated:
236
+
```php
237
+
namespace Thesis\Api\Request;
238
+
239
+
/**
240
+
* @api
241
+
* @phpstan-sealed (
242
+
* ContactPhone |
243
+
* ContactEmail |
244
+
* ContactChatId
245
+
* )
246
+
*/
247
+
interface Contact {}
248
+
```
249
+
250
+
Note the namespace: this interface and all its implementations will be generated in a namespace nested relative to the object, just like all nested types of this message.
251
+
252
+
And for each variant, the following objects will be generated:
253
+
254
+
```php
255
+
namespace Thesis\Api\Request;
256
+
257
+
/**
258
+
* @api
259
+
*/
260
+
final readonly class ContactChatId implements \Thesis\Api\Request\Contact
261
+
{
262
+
public function __construct(
263
+
#[Reflection\Field(3, Reflection\Int64T::T)]
264
+
public \BcMath\Number $chatId = new \BcMath\Number(0),
265
+
) {}
266
+
}
267
+
268
+
/**
269
+
* @api
270
+
*/
271
+
final readonly class ContactEmail implements \Thesis\Api\Request\Contact
272
+
{
273
+
public function __construct(
274
+
#[Reflection\Field(2, Reflection\StringT::T)]
275
+
public string $email = '',
276
+
) {}
277
+
}
278
+
279
+
/**
280
+
* @api
281
+
*/
282
+
final readonly class ContactPhone implements \Thesis\Api\Request\Contact
283
+
{
284
+
public function __construct(
285
+
#[Reflection\Field(1, Reflection\StringT::T)]
286
+
public string $phone = '',
287
+
) {}
288
+
}
289
+
```
290
+
291
+
#### precedence
292
+
293
+
By default, all fields with scalar data types will have corresponding default values (0 for numbers, false for booleans, and so on).
294
+
If proto2 is used and the field is marked as `optional`, scalar types will become nullable, with null as the default value.
295
+
The same applies to `optional` in proto3. Lists and maps, however, will always be non-nullable (especially since they cannot be `required` or `optional`) but will have empty default values.
296
+
Meanwhile, all objects will always be nullable, regardless of `required`/`optional` labels, which allows the serializer to quickly skip such fields and avoid writing unnecessary data.
297
+
298
+
#### grpc client
299
+
300
+
Our plugin supports generating all types of communication between client and server, including client-side, server-side, and bidirectional streaming.
yield new Server\Service('thesis.api.v1.QueueService', [
450
+
new Server\Rpc(
451
+
new Server\Handle('State', \Google\Protobuf\Empty_::class),
452
+
new Server\UnaryHandler($this->server->state(...)),
453
+
),
454
+
new Server\Rpc(
455
+
new Server\Handle('Push', \Thesis\Api\V1\Message::class),
456
+
new Server\ClientStreamHandler($this->server->push(...)),
457
+
),
458
+
new Server\Rpc(
459
+
new Server\Handle('Pull', \Google\Protobuf\Empty_::class),
460
+
new Server\ServerStreamHandler($this->server->pull(...)),
461
+
),
462
+
new Server\Rpc(
463
+
new Server\Handle('Heartbeats', \Thesis\Api\V1\Heartbeat::class),
464
+
new Server\BidirectionalStreamHandler($this->server->heartbeats(...)),
465
+
),
466
+
]);
467
+
}
468
+
}
469
+
```
470
+
133
471
### Generated libraries
134
472
135
473
The protobuf ecosystem has many well-defined types for various tasks, including the so-called [well-known types](https://protobuf.dev/reference/protobuf/google.protobuf/), which include `Timestamp`, `Duration`, `Empty`, and many others.
0 commit comments