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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -598,11 +598,11 @@ final readonly class QueueServiceServerRegistry implements Server\ServiceRegistr

#### autoload.metadata.php

After generating the code, you may notice a strange file called `autoload.metadata.php` and classes named `*DescriptorRegistry.php`, which contain the original proto files stored as base64-encoded protobuf messages that the plugin used to generate your code.
After generating the code, you may notice a strange file called `autoload.metadata.php` and classes named `DescriptorRegistry.php`, which contain the original proto files stored as base64-encoded protobuf messages that the plugin used to generate your code.
Do not scare. These files are necessary for implementing [server-side reflection](https://github.com/grpc/grpc/tree/master/src/proto/grpc/reflection/v1) and (de) serialization of the [google.protobuf.Any](https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/any.proto) type,
which contains the full message path within the schema. This approach is also used in other ecosystems.

To avoid manually registering `*DescriptorRegistry.php` classes in the descriptor pool, it is recommended to add the path to `autoload.metadata.php` in your `composer.json` file.
To avoid manually registering `DescriptorRegistry.php` classes in the descriptor pool, it is recommended to add the path to `autoload.metadata.php` in your `composer.json` file.
In long-running applications for which the [thesis](https://github.com/thesis-php) project is designed, such a file will be loaded by `Composer` only once:
```json
"autoload": {
Expand Down
27 changes: 0 additions & 27 deletions src/Plugin/ClassLikeGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@

private Generator\ProtoGenerator $proto;

private Generator\DescriptorMetadataRegistryGenerator $metadata;

public function __construct(
string $namespace,
private FileFactory $files,
Expand All @@ -41,9 +39,6 @@ public function __construct(
$syntax,
$edition,
);
$this->metadata = new Generator\DescriptorMetadataRegistryGenerator(
$namespacer,
);
}

/**
Expand Down Expand Up @@ -77,26 +72,4 @@ public function generateGrpcServer(Parser\ServiceDescriptor $service): iterable
yield $this->files->create($this->grpc->generateServerRegistry($service), "{$service->path}ServerRegistry");
}
}

/**
* @param list<string> $dependencies
*/
public function generateDescriptorMetadataRegistry(
NameIndex $index,
string $filename,
array $dependencies,
string $descriptorName,
string $buffer,
): CodeGeneratorResponse\File {
return $this->files->create(
$this->metadata->generate(
$index,
$filename,
$dependencies,
$descriptorName,
$buffer,
),
$descriptorName,
);
}
}
74 changes: 40 additions & 34 deletions src/Plugin/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
use Thesis\Protobuf\Registry\File;
use Thesis\Protoc\Exception\CodeCannotBeGenerated;
use Thesis\Protoc\Plugin\Generator\AutoloadFunctionGenerator;
use Thesis\Protoc\Plugin\Generator\DescriptorFile;
use Thesis\Protoc\Plugin\Generator\DescriptorMetadataRegistryGenerator;
use Thesis\Protoc\Plugin\Generator\FileFactory;
use Thesis\Protoc\Plugin\Generator\PhpNamespacer;
use Thesis\Protoc\Plugin\Parser\FileDescriptor;
use Thesis\Protoc\Plugin\Parser\MessageDescriptor;
use Thesis\Protoc\Plugin\Parser\ServiceMethodDescriptor;
Expand Down Expand Up @@ -61,16 +64,20 @@ private function doGenerate(

$descriptorPaths = new PathTable();

$registries = new DescriptorTable();

foreach ($request as $source => $proto) {
$phpNamespace = self::determinePhpNamespace($proto, $options);

$index = new NameIndex();

$path = $options->srcPath ?? str_replace('\\', '/', $phpNamespace);

$generator = new ClassLikeGenerator(
namespace: $phpNamespace,
files: new FileFactory(
self::createClassLikeGeneratedDoc($request, $source),
$path = $options->srcPath ?? str_replace('\\', '/', $phpNamespace),
$path,
),
graph: $registry->graph($source),
index: $index,
Expand Down Expand Up @@ -114,22 +121,42 @@ static function (ServiceMethodDescriptor $method): File\MethodDescriptor {
}

if ($options->emitMetadata && !$index->empty()) {
// A user message/enum may already occupy "DescriptorRegistry" in
// this namespace — that is not their fault, so rename ours instead.
$descriptorName = Naming::descriptorName(self::topLevelClassNames($proto));

yield $generator->generateDescriptorMetadataRegistry(
$index,
$proto->name,
$proto->dependencies,
$descriptorName,
$this->encoder->encode($proto->file),
$group = $registries->add($path, $phpNamespace);

$group->add(
$source,
$proto->topLevelClassNames(),
new DescriptorFile(
constant: Naming::descriptorBuffer($proto->name),
name: $proto->name,
dependencies: $proto->dependencies,
index: $index,
buffer: $this->encoder->encode($proto->file),
),
);

$descriptorPaths->addRelation("{$phpNamespace}\\{$descriptorName}", $path);
}
}

foreach ($registries as $group) {
// A user message/enum may already occupy "DescriptorRegistry" in this
// namespace. That is not their fault, so rename ours instead.
$descriptorName = Naming::descriptorName($group->taken);

$descriptorPaths->addRelation("{$group->namespace}\\{$descriptorName}", $group->path);

$factory = new FileFactory(
self::createClassLikeGeneratedDoc($request, implode(', ', $group->sources)),
$group->path,
);

$registryGenerator = new DescriptorMetadataRegistryGenerator(new PhpNamespacer($group->namespace));

yield $factory->create(
$registryGenerator->generate($descriptorName, $group->files),
$descriptorName,
);
}

foreach ($options->emitMetadata ? $descriptorPaths->groupByNamespace() : [] as $ns => $descriptors) {
$generator = new AutoloadFunctionGenerator(
new FileFactory(
Expand Down Expand Up @@ -158,27 +185,6 @@ private function doGenerateMessages(ClassLikeGenerator $generator, MessageDescri
}
}

/**
* The class names generated directly in the file's namespace — the top-level
* messages and enums the descriptor registry must not clash with.
*
* @return array<string, true>
*/
private static function topLevelClassNames(FileDescriptor $proto): array
{
$names = [];

foreach ($proto->messages as $message) {
$names[Naming::pascalCase($message->name)] = true;
}

foreach ($proto->enums as $enum) {
$names[Naming::pascalCase($enum->name)] = true;
}

return $names;
}

/**
* @return non-empty-string
* @throws CodeCannotBeGenerated
Expand Down
40 changes: 40 additions & 0 deletions src/Plugin/DescriptorRegistryGroup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Thesis\Protoc\Plugin;

use Thesis\Protoc\Plugin\Generator\DescriptorFile;

/**
* Accumulates every .proto file of a single package (PHP namespace) so the
* compiler can emit one DescriptorRegistry per package instead of one per file.
*
* @api
*/
final class DescriptorRegistryGroup
{
/** @var list<string> */
public private(set) array $sources = [];

/** @var array<string, true> */
public private(set) array $taken = [];

/** @var list<DescriptorFile> */
public private(set) array $files = [];

public function __construct(
public string $namespace,
public string $path,
) {}

/**
* @param array<string, true> $taken class names already used in the namespace
*/
public function add(string $source, array $taken, DescriptorFile $file): void
{
$this->sources[] = $source;
$this->taken += $taken;
$this->files[] = $file;
}
}
26 changes: 26 additions & 0 deletions src/Plugin/DescriptorTable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Thesis\Protoc\Plugin;

/**
* @api
* @template-implements \IteratorAggregate<array-key, DescriptorRegistryGroup>
*/
final class DescriptorTable implements \IteratorAggregate
{
/** @var array<string, DescriptorRegistryGroup> */
private array $groups = [];

public function add(string $path, string $namespace): DescriptorRegistryGroup
{
return $this->groups[$path] ??= new DescriptorRegistryGroup($namespace, $path);
}

#[\Override]
public function getIterator(): \Traversable
{
yield from array_values($this->groups);
}
}
28 changes: 28 additions & 0 deletions src/Plugin/Generator/DescriptorFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Thesis\Protoc\Plugin\Generator;

use Thesis\Protoc\Plugin\NameIndex;

/**
* One .proto file's contribution to its package descriptor registry: a private
* buffer constant plus the messages/enums/services it registers.
*
* @api
*/
final readonly class DescriptorFile
{
/**
* @param non-empty-string $constant name of the private buffer constant, e.g. CODE_DESCRIPTOR_BUFFER
* @param list<string> $dependencies
*/
public function __construct(
public string $constant,
public string $name,
public array $dependencies,
public NameIndex $index,
public string $buffer,
) {}
}
46 changes: 28 additions & 18 deletions src/Plugin/Generator/DescriptorMetadataRegistryGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use Nette\PhpGenerator\Parameter;
use Nette\PhpGenerator\PhpNamespace;
use Thesis\Protobuf\Registry\File;
use Thesis\Protoc\Plugin\NameIndex;

/**
* @api
Expand All @@ -23,14 +22,11 @@ public function __construct(
) {}

/**
* @param list<string> $dependencies
* @param list<DescriptorFile> $files the .proto files that share this package
*/
public function generate(
NameIndex $index,
string $filename,
array $dependencies,
string $className,
string $buffer,
array $files,
): PhpNamespace {
$namespace = $this->namespacer->create($className);

Expand All @@ -40,13 +36,16 @@ public function generate(
->addComment('@api')
->setImplements([
'Registry\Registrar',
])
->addMember(
new Constant('DESCRIPTOR_BUFFER')
]);

foreach ($files as $file) {
$classType->addMember(
new Constant($file->constant)
->setPrivate()
->setType('string')
->setValue(base64_encode($buffer)),
->setValue(base64_encode($file->buffer)),
);
}

$namespace->add($classType);

Expand All @@ -63,25 +62,38 @@ public function generate(
->setReturnType('void')
->addAttribute(\Override::class);

$method->addBody('$pool->add(Registry\Descriptor::base64(self::DESCRIPTOR_BUFFER), new File(');
$method->addBody(' name: ?,', [$filename]);
foreach ($files as $idx => $file) {
if ($idx > 0) {
$method->addBody('');
}

self::appendFile($method, $file);
}

return $namespace;
}

private static function appendFile(Method $method, DescriptorFile $file): void
{
$method->addBody(\sprintf('$pool->add(Registry\Descriptor::base64(self::%s), new File(', $file->constant));
$method->addBody(' name: ?,', [$file->name]);

self::pushParameter($method, 'dependencies', $dependencies);
self::pushParameter($method, 'dependencies', $file->dependencies);
self::pushParameter(
$method,
'messages',
$index->messageTypes,
$file->index->messageTypes,
static fn(File\MessageDescriptor $descriptor) => new Literal(
\sprintf("new File\\MessageDescriptor('%s', \\%s::class)", $descriptor->name, $descriptor->fqcn),
),
);
self::pushParameter($method, 'enums', $index->enumTypes, static fn(File\EnumDescriptor $descriptor) => new Literal(
self::pushParameter($method, 'enums', $file->index->enumTypes, static fn(File\EnumDescriptor $descriptor) => new Literal(
\sprintf("new File\\EnumDescriptor('%s', \\%s::class)", $descriptor->name, $descriptor->fqcn),
));
self::pushParameter(
$method,
'services',
$index->services,
$file->index->services,
static fn(File\ServiceDescriptor $descriptor) => new Literal(
<<<'PHP'
new File\ServiceDescriptor(
Expand Down Expand Up @@ -110,8 +122,6 @@ public function generate(
);

$method->addBody('));');

return $namespace;
}

/**
Expand Down
15 changes: 15 additions & 0 deletions src/Plugin/Naming.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,21 @@ public static function descriptorName(array $taken = []): string
return $base . $suffix;
}

/**
* Name of the private constant holding a single .proto file's descriptor
* buffer inside the per-package registry, e.g. "google/rpc/code.proto" =>
* "CODE_DESCRIPTOR_BUFFER". File names are unique within a package, so the
* constants never collide.
*
* @return non-empty-string
*/
public static function descriptorBuffer(string $file): string
{
$name = strtoupper(trim((string) preg_replace('/[^a-zA-Z0-9]+/', '_', pathinfo($file, PATHINFO_FILENAME)), '_'));

return "{$name}_DESCRIPTOR_BUFFER";
}

public static function camelCase(string $name): string
{
return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $name))));
Expand Down
Loading