Skip to content

Commit ca7ee13

Browse files
committed
chore: generate code guide
1 parent 1b0404b commit ca7ee13

1 file changed

Lines changed: 338 additions & 0 deletions

File tree

README.md

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ For this reason, we have written this plugin, which — in addition to addressin
1111
- [Plugin options](#plugin-options)
1212
- [src_path](#src_path)
1313
- [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)
1422
- [Generated libraries](#generated-libraries)
1523
- [Feature matrix](#feature-matrix)
1624

@@ -130,6 +138,336 @@ protoc \
130138

131139
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.
132140

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.
301+
302+
Consider the following service:
303+
```protobuf
304+
syntax = "proto3";
305+
306+
package thesis.api.v1;
307+
308+
import "google/protobuf/empty.proto";
309+
310+
message Message {}
311+
312+
message Heartbeat {}
313+
314+
message Queue {}
315+
316+
service QueueService {
317+
rpc State(google.protobuf.Empty) returns (Queue);
318+
rpc Push(stream Message) returns (google.protobuf.Empty);
319+
rpc Pull(google.protobuf.Empty) returns (stream Message);
320+
rpc Heartbeats(stream Heartbeat) returns (stream Heartbeat);
321+
}
322+
```
323+
324+
A client of the following code will be generated (method bodies are intentionally omitted for simplicity):
325+
```php
326+
namespace Thesis\Api\V1;
327+
328+
use Amp\Cancellation;
329+
use Amp\NullCancellation;
330+
use Thesis\Grpc\Client;
331+
use Thesis\Grpc\Metadata;
332+
333+
/**
334+
* @api
335+
*/
336+
final readonly class QueueServiceClient
337+
{
338+
public function __construct(
339+
private Client $client,
340+
) {}
341+
342+
public function state(
343+
\Google\Protobuf\Empty_ $request,
344+
Metadata $md = new Metadata(),
345+
Cancellation $cancellation = new NullCancellation(),
346+
): \Thesis\Api\V1\Queue {}
347+
348+
/**
349+
* @return Client\ClientStreamChannel<\Thesis\Api\V1\Message, \Google\Protobuf\Empty_>
350+
*/
351+
public function push(
352+
Metadata $md = new Metadata(),
353+
Cancellation $cancellation = new NullCancellation(),
354+
): Client\ClientStreamChannel {}
355+
356+
/**
357+
* @return Client\ServerStreamChannel<\Google\Protobuf\Empty_, \Thesis\Api\V1\Message>
358+
*/
359+
public function pull(
360+
\Google\Protobuf\Empty_ $request,
361+
Metadata $md = new Metadata(),
362+
Cancellation $cancellation = new NullCancellation(),
363+
): Client\ServerStreamChannel {}
364+
365+
/**
366+
* @return Client\BidirectionalStreamChannel<\Thesis\Api\V1\Heartbeat, \Thesis\Api\V1\Heartbeat>
367+
*/
368+
public function heartbeats(
369+
Metadata $md = new Metadata(),
370+
Cancellation $cancellation = new NullCancellation(),
371+
): Client\BidirectionalStreamChannel {}
372+
}
373+
```
374+
375+
See the [thesis/grpc](https://github.com/thesis-php/grpc) for details of how to use streams and `Metadata`.
376+
377+
To use `gRPC` after generation you should install `thesis/grpc` and `amphp/amp` packages, if you haven't done so already.
378+
379+
#### grpc server
380+
381+
When generating the server, two types will be generated: first, the server interface itself, which you need to implement,
382+
and second, a registrar class that registers your implementation with the `gRPC` server, adapting the interface methods through intermediate handlers.
383+
384+
Let's take a look at the server interface:
385+
```php
386+
387+
namespace Thesis\Api\V1;
388+
389+
use Amp\Cancellation;
390+
use Thesis\Grpc\Metadata;
391+
use Thesis\Grpc\Server;
392+
393+
/**
394+
* @api
395+
*/
396+
interface QueueServiceServer
397+
{
398+
public function state(
399+
\Google\Protobuf\Empty_ $request,
400+
Metadata $md,
401+
Cancellation $cancellation,
402+
): \Thesis\Api\V1\Queue;
403+
404+
/**
405+
* @param Server\ClientStreamChannel<\Thesis\Api\V1\Message, \Google\Protobuf\Empty_> $stream
406+
*/
407+
public function push(Server\ClientStreamChannel $stream, Metadata $md, Cancellation $cancellation): void;
408+
409+
/**
410+
* @param Server\ServerStreamChannel<\Google\Protobuf\Empty_, \Thesis\Api\V1\Message> $stream
411+
*/
412+
public function pull(
413+
\Google\Protobuf\Empty_ $request,
414+
Server\ServerStreamChannel $stream,
415+
Metadata $md,
416+
Cancellation $cancellation,
417+
): void;
418+
419+
/**
420+
* @param Server\BidirectionalStreamChannel<\Thesis\Api\V1\Heartbeat, \Thesis\Api\V1\Heartbeat> $stream
421+
*/
422+
public function heartbeats(
423+
Server\BidirectionalStreamChannel $stream,
424+
Metadata $md,
425+
Cancellation $cancellation,
426+
): void;
427+
}
428+
```
429+
430+
And at the registrar class:
431+
```php
432+
namespace Thesis\Api\V1;
433+
434+
use Override;
435+
use Thesis\Grpc\Server;
436+
437+
/**
438+
* @api
439+
*/
440+
final readonly class QueueServiceServerRegistry implements Server\ServiceRegistry
441+
{
442+
public function __construct(
443+
private \Thesis\Api\V1\QueueServiceServer $server,
444+
) {}
445+
446+
#[Override]
447+
public function services(): iterable
448+
{
449+
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+
133471
### Generated libraries
134472

135473
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

Comments
 (0)