Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ If you want to change the namespace under which the code will be generated, use
protoc \
--plugin=protoc-gen-php-plugin=/usr/local/bin/protoc-gen-php \
protos/*.proto \
--php-plugin_out=php_namespace="Thesis\\Api\\V1":src
--php-plugin_opt=php_namespace="Thesis\\Api\\V1" \
--php-plugin_out=src
```

### `src_path`
Expand All @@ -147,7 +148,8 @@ and configure autoloading via composer without creating unnecessary nesting. To
protoc \
--plugin=protoc-gen-php-plugin=/usr/local/bin/protoc-gen-php \
protos/*.proto \
--php-plugin_out=src_path=.:src
--php-plugin_opt=src_path=. \
--php-plugin_out=src
```

### `grpc`
Expand All @@ -158,15 +160,17 @@ If you do not want to generate `gRPC` code, you can disable this behavior using
protoc \
--plugin=protoc-gen-php-plugin=/usr/local/bin/protoc-gen-php \
protos/*.proto \
--php-plugin_out=grpc=none:genproto
--php-plugin_opt=grpc=none \
--php-plugin_out=genproto
```

If you want to generate only the client code, use `grpc=client`:
```shell
protoc \
--plugin=protoc-gen-php-plugin=/usr/local/bin/protoc-gen-php \
protos/*.proto \
--php-plugin_out=grpc=client:genproto
--php-plugin_opt=grpc=client \
--php-plugin_out=genproto
```

To generate only the server code, use `grpc=server`. By default, and when passing `grpc=client,grpc=server`, both the client and server will be generated.
Expand Down Expand Up @@ -199,12 +203,15 @@ docker run --rm \

### Multiple options

You can pass multiple options separated by commas. For example, if you want to specify a different namespace, place all the code in the root directory, and generate only the `gRPC` client, you can write the following:
You can pass multiple options by repeating `--php-plugin_opt`. For example, if you want to specify a different namespace, place all the code in the root directory, and generate only the `gRPC` client, you can write the following:
```shell
protoc \
--plugin=protoc-gen-php-plugin=/usr/local/bin/protoc-gen-php \
protos/*.proto \
--php-plugin_out=php_namespace="Thesis\\Api\\V1",src_path=.,grpc=client:genproto
--php-plugin_opt=php_namespace="Thesis\\Api\\V1" \
--php-plugin_opt=src_path=. \
--php-plugin_opt=grpc=client \
--php-plugin_out=genproto
```

### Generated code guide
Expand Down
3 changes: 2 additions & 1 deletion docker/php/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ FROM alpine:3.20 AS protoc

ARG PROTOC_VERSION=32.1

RUN apk add --no-cache curl unzip && \
RUN apk add --no-cache bash curl unzip && \
curl -LO "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip" && \
unzip "protoc-${PROTOC_VERSION}-linux-x86_64.zip" -d /usr/local && \
rm "protoc-${PROTOC_VERSION}-linux-x86_64.zip"

FROM ghcr.io/phpyh/php:8.4

COPY --from=protoc /bin/bash /bin/bash
COPY --from=protoc /usr/local/bin/protoc /usr/local/bin/protoc
COPY --from=protoc /usr/local/include /usr/local/include
2 changes: 1 addition & 1 deletion src/Plugin/Dependency/Index.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
final readonly class Index
{
/**
* @param array<string, 1> $types
* @param array<string, ?string> $types map of fully-qualified type name to default value (zero case for enums or null for messages)
*/
public function __construct(
public array $types,
Expand Down
19 changes: 7 additions & 12 deletions src/Plugin/Dependency/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,6 @@ private function createIndex(Parser\Request $request, CompilerOptions $options):
proto: $descriptor,
package: $descriptor->package === null ? '.' : ".{$descriptor->package}.",
),
preserve_keys: false,
);

$types = array_combine(
$types,
array_fill(0, \count($types), 1),
);

$package = $descriptor->package ?? '.';
Expand Down Expand Up @@ -152,12 +146,12 @@ private function createServiceDependencyGraph(Parser\ServiceDescriptor $descript
}

/**
* @return iterable<string>
* @return iterable<string, ?string>
*/
private function createFileIndex(Parser\FileDescriptor $proto, string $package): iterable
{
foreach ($proto->enums as $enum) {
yield "{$package}{$enum->name}";
yield "{$package}{$enum->name}" => $enum->cases[0]->name ?? null;
}

foreach ($proto->messages as $message) {
Expand All @@ -166,18 +160,18 @@ private function createFileIndex(Parser\FileDescriptor $proto, string $package):
}

/**
* @return iterable<string>
* @return iterable<string, ?string>
*/
private function createDescriptorIndex(Parser\MessageDescriptor $descriptor, string $name): iterable
{
yield $name = "{$name}{$descriptor->name}";
yield $name = "{$name}{$descriptor->name}" => null;

foreach ($descriptor->messages as $it) {
yield from $this->createDescriptorIndex($it, "{$name}.");
}

foreach ($descriptor->enums as $it) {
yield "{$name}.{$it->name}";
yield "{$name}.{$it->name}" => $it->cases[0]->name ?? null;
}
}

Expand All @@ -189,7 +183,7 @@ private function extractTypes(array $typeNames, string $proto): iterable
{
foreach ($typeNames as $typeName) {
foreach ($this->fileIndexes[$proto] ?? [] as $index) {
if (!isset($index->types[$typeName])) {
if (!\array_key_exists($typeName, $index->types)) {
continue;
}

Expand All @@ -201,6 +195,7 @@ private function extractTypes(array $typeNames, string $proto): iterable
yield $typeName => new Type(
$fqcn,
$class,
$index->types[$typeName],
);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Plugin/Dependency/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
public function __construct(
public string $fqcn,
public string $class,
public ?string $default = null,
) {}
}
18 changes: 16 additions & 2 deletions src/Plugin/Generator/ComplexTypeDeclarationFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Google\Protobuf\FieldDescriptorProto\Type;
use Nette\PhpGenerator\Literal;
use Thesis\Protoc\Plugin\Dependency;
use Thesis\Protoc\Plugin\Naming;
use Thesis\Protoc\Plugin\Parser;

/**
Expand All @@ -22,13 +23,26 @@ public function create(Parser\FieldDescriptor $field, string $typeName): TypeDec
{
$fieldType = $this->graph->get($typeName);

$isEnum = $field->type === Type::TYPE_ENUM;

$default = null;

if ($isEnum && $fieldType->default !== null) {
$default = new Literal(\sprintf(
'%s::%s',
$fieldType->fqcn,
Naming::secureEnumCase($fieldType->default),
));
}

return new TypeDeclaration(
phpType: $fieldType->fqcn,
reflectionType: Literal::new('Reflection\\' . ($field->type === Type::TYPE_MESSAGE ? 'ObjectT' : 'EnumT'), [
reflectionType: Literal::new('Reflection\\' . ($isEnum ? 'EnumT' : 'ObjectT'), [
new Literal("{$fieldType->fqcn}::class"),
]),
nullable: true,
nullable: !$isEnum,
docType: $fieldType->fqcn,
default: $default,
);
}
}
42 changes: 37 additions & 5 deletions src/Plugin/Generator/ProtoGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Nette\PhpGenerator\EnumType;
use Nette\PhpGenerator\InterfaceType;
use Nette\PhpGenerator\Literal;
use Nette\PhpGenerator\Method;
use Nette\PhpGenerator\PhpNamespace;
use Thesis\Protobuf\Registry\File;
use Thesis\Protoc\Exception\CodeCannotBeGenerated;
Expand Down Expand Up @@ -166,7 +167,7 @@ private function generateMessage(PhpNamespace $namespace, string $className, Par
continue;
}

$features = $field->options?->features;
$features = $field->features;

if ($features?->messageEncoding === FeatureSet\MessageEncoding::DELIMITED) {
throw new CodeCannotBeGenerated('DELIMITED message encoding are not supported');
Expand Down Expand Up @@ -198,12 +199,13 @@ private function generateMessage(PhpNamespace $namespace, string $className, Par
}

$repeated = $field->label === FieldDescriptorProto\Label::LABEL_REPEATED && !$type->isMap;
$required = $field->label === FieldDescriptorProto\Label::LABEL_REQUIRED || $features?->fieldPresence === FeatureSet\FieldPresence::LEGACY_REQUIRED;

$presence = $this->edition !== null
? $features?->fieldPresence === FeatureSet\FieldPresence::EXPLICIT
: $field->optional;

$nullable = ($type->nullable || $presence) && !$repeated && !($field->type === FieldDescriptorProto\Type::TYPE_ENUM && $field->defaultValue !== null);
$nullable = ($type->nullable || $presence) && !$repeated && !$required && !($field->type === FieldDescriptorProto\Type::TYPE_ENUM && $field->defaultValue !== null);

$phpType = ($nullable ? '?' : '') . ($repeated ? 'array' : $type->phpType);

Expand All @@ -225,7 +227,10 @@ private function generateMessage(PhpNamespace $namespace, string $className, Par

$default = $nullable ? null : $type->default;

if ($field->defaultValue !== null && $field->type !== null) {
// Only bake the proto-defined default into the constructor when the field
// is non-nullable. For nullable fields we keep `null` so that presence
// round-trips: the user can distinguish "field unset" from "field set to its proto default".
if (!$nullable && $field->defaultValue !== null && $field->type !== null) {
$default = $this->parseDefaultValue(
$field->type,
$field->defaultValue,
Expand All @@ -239,8 +244,11 @@ private function generateMessage(PhpNamespace $namespace, string $className, Par
$field->number,
$reflectionType,
])
->setNullable($nullable)
->setDefaultValue($repeated ? [] : $default);
->setNullable($nullable);

if (!$required) {
$parameter->setDefaultValue($repeated ? [] : $default);
}

if ($field->options?->deprecated === true) {
$parameter->addComment('@deprecated');
Expand Down Expand Up @@ -300,9 +308,33 @@ private function generateMessage(PhpNamespace $namespace, string $className, Par
]);
}

$this->reorderRequiredFirst($constructor);

return $namespace;
}

private function reorderRequiredFirst(Method $constructor): void
{
[$required, $optional] = [[], []];

foreach ($constructor->getParameters() as $parameter) {
if ($parameter->hasDefaultValue()) {
$optional[] = $parameter;
} else {
$required[] = $parameter;
}
}

if ($required === [] || $optional === []) {
return;
}

$constructor->setParameters([
...$required,
...$optional,
]);
}

/**
* @param list<Parser\FieldDescriptor> $variants
* @return iterable<string, PhpNamespace>
Expand Down
Loading