From 578702ae74a4753a875527e09b717de1564bddca Mon Sep 17 00:00:00 2001 From: Kirill Astakhov Date: Thu, 17 Nov 2022 12:45:23 +0200 Subject: [PATCH] Improved generator of GRPC proto files --- README.md | 26 +++++-- src/Bootloader/GRPCBootloader.php | 4 ++ src/Config/GRPCConfig.php | 27 ++++++++ src/Console/Command/GRPC/GenerateCommand.php | 26 +++++-- src/GRPC/CommandExecutor.php | 28 ++++++++ src/GRPC/ProtoCompiler.php | 37 ++-------- src/GRPC/ProtocCommandBuilder.php | 58 ++++++++++++++++ tests/app/config/grpc.php | 4 ++ tests/src/Bootloader/GRPCBootloaderTest.php | 3 + tests/src/Config/GRPCConfigTest.php | 48 +++++++++++++ tests/src/GRPC/ProtocCommandBuilderTest.php | 72 ++++++++++++++++++++ 11 files changed, 293 insertions(+), 40 deletions(-) create mode 100644 src/GRPC/CommandExecutor.php create mode 100644 src/GRPC/ProtocCommandBuilder.php create mode 100644 tests/src/GRPC/ProtocCommandBuilderTest.php diff --git a/README.md b/README.md index 7945ec7..b205100 100644 --- a/README.md +++ b/README.md @@ -933,6 +933,24 @@ return [ 'binaryPath' => null, // 'binaryPath' => __DIR__.'/../../protoc-gen-php-grpc', + /** + * Path, where generated DTO files put. + * Default: null + */ + 'generatedPath' => null, + + /** + * Base namespace for generated proto files. + * Default: null + */ + 'namespace' => null, + + /** + * Root path for all proto files in which imports will be searched. + * Default: null + */ + 'servicesBasePath' => null + 'services' => [ __DIR__.'/../../proto/echo.proto', ], @@ -947,10 +965,10 @@ php app.php grpc:generate #### Console commands -| Command | Description | -|--------------------------------------------|---------------------------------------------------------| -| grpc:services | List available GRPC services | -| grpc:generate {path=auto} {namespace=auto} | Generate GPRC service code using protobuf specification | +| Command | Description | +|--------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| grpc:services | List available GRPC services | +| grpc:generate {path=auto} {namespace=auto} | Generate GPRC service code using protobuf specification. By default `path` and `namespace` options are `auto`. Defined values from config will be used in `auto` mode, by default. | #### Example GRPC service diff --git a/src/Bootloader/GRPCBootloader.php b/src/Bootloader/GRPCBootloader.php index e35d28f..cf03af0 100644 --- a/src/Bootloader/GRPCBootloader.php +++ b/src/Bootloader/GRPCBootloader.php @@ -63,6 +63,10 @@ private function initGrpcConfig(): void */ 'binaryPath' => null, + 'generatedPath' => null, + 'namespace' => null, + 'servicesBasePath' => null, + 'services' => [], 'interceptors' => [], diff --git a/src/Config/GRPCConfig.php b/src/Config/GRPCConfig.php index 65a5807..bf1f056 100644 --- a/src/Config/GRPCConfig.php +++ b/src/Config/GRPCConfig.php @@ -15,6 +15,9 @@ final class GRPCConfig extends InjectableConfig protected array $config = [ 'binaryPath' => null, + 'generatedPath' => null, + 'namespace' => null, + 'servicesBasePath' => null, 'services' => [], 'interceptors' => [], ]; @@ -24,6 +27,30 @@ public function getBinaryPath(): ?string return $this->config['binaryPath'] ?? null; } + /** + * Path, where generated DTO files put. + */ + public function getGeneratedPath(): ?string + { + return $this->config['generatedPath'] ?? null; + } + + /** + * Base namespace for generated proto files. + */ + public function getNamespace(): ?string + { + return $this->config['namespace'] ?? null; + } + + /** + * Root path for all proto files in which imports will be searched. + */ + public function getServicesBasePath(): ?string + { + return $this->config['servicesBasePath'] ?? null; + } + /** * @return array> */ diff --git a/src/Console/Command/GRPC/GenerateCommand.php b/src/Console/Command/GRPC/GenerateCommand.php index 6e704a8..41616ba 100644 --- a/src/Console/Command/GRPC/GenerateCommand.php +++ b/src/Console/Command/GRPC/GenerateCommand.php @@ -10,7 +10,9 @@ use Spiral\Console\Command; use Spiral\Files\FilesInterface; use Spiral\RoadRunnerBridge\Config\GRPCConfig; +use Spiral\RoadRunnerBridge\GRPC\CommandExecutor; use Spiral\RoadRunnerBridge\GRPC\Exception\CompileException; +use Spiral\RoadRunnerBridge\GRPC\ProtocCommandBuilder; use Spiral\RoadRunnerBridge\GRPC\ProtoCompiler; use Spiral\RoadRunnerBridge\GRPC\ProtoRepository\ProtoFilesRepositoryInterface; @@ -31,16 +33,20 @@ public function perform( $binaryPath = $config->getBinaryPath(); if ($binaryPath !== null && !\file_exists($binaryPath)) { - $this->sprintf('PHP Server plugin binary `%s` not found.', $binaryPath); + $this->sprintf( + 'Protoc plugin binary `%s` was not found. Use command `./vendor/bin/rr download-protoc-binary` to download it.`', + $binaryPath + ); return self::FAILURE; } $compiler = new ProtoCompiler( - $this->getPath($kernel), - $this->getNamespace($kernel), + $this->getPath($kernel, $config->getGeneratedPath()), + $this->getNamespace($kernel, $config->getNamespace()), $files, - $binaryPath + new ProtocCommandBuilder($files, $config, $binaryPath), + new CommandExecutor() ); foreach ($repository->getProtos() as $protoFile) { @@ -81,13 +87,17 @@ public function perform( /** * Get or detect base source code path. By default fallbacks to kernel location. */ - protected function getPath(KernelInterface $kernel): string + protected function getPath(KernelInterface $kernel, ?string $generatedPath): string { $path = $this->argument('path'); if ($path !== 'auto') { return $path; } + if ($generatedPath !== null) { + return $generatedPath; + } + $r = new \ReflectionObject($kernel); return \dirname($r->getFileName()); @@ -96,13 +106,17 @@ protected function getPath(KernelInterface $kernel): string /** * Get or detect base namespace. By default fallbacks to kernel namespace. */ - protected function getNamespace(KernelInterface $kernel): string + protected function getNamespace(KernelInterface $kernel, ?string $protoNamespace): string { $namespace = $this->argument('namespace'); if ($namespace !== 'auto') { return $namespace; } + if ($protoNamespace !== null) { + return $protoNamespace; + } + return (new \ReflectionObject($kernel))->getNamespaceName(); } } diff --git a/src/GRPC/CommandExecutor.php b/src/GRPC/CommandExecutor.php new file mode 100644 index 0000000..5233318 --- /dev/null +++ b/src/GRPC/CommandExecutor.php @@ -0,0 +1,28 @@ +baseNamespace = \str_replace('\\', '/', \rtrim($baseNamespace, '\\')); } @@ -30,25 +33,10 @@ public function compile(string $protoFile): array { $tmpDir = $this->tmpDir(); - \exec( - \sprintf( - 'protoc %s --php_out=%s --php-grpc_out=%s -I %s %s 2>&1', - $this->protocBinaryPath ? '--plugin=' . $this->protocBinaryPath : '', - \escapeshellarg($tmpDir), - \escapeshellarg($tmpDir), - \escapeshellarg(dirname($protoFile)), - \implode(' ', \array_map('escapeshellarg', $this->getProtoFiles($protoFile))) - ), - $output, - $exitCode + $output = $this->executor->execute( + $this->commandBuilder->build(\dirname($protoFile), $tmpDir) ); - if ($exitCode !== 0) { - throw new CompileException(\implode("\n", $output), $exitCode); - } - - $output = \trim(\implode("\n", $output), "\n ,"); - if ($output !== '') { $this->files->deleteDirectory($tmpDir); throw new CompileException($output); @@ -68,7 +56,7 @@ public function compile(string $protoFile): array private function copy(string $tmpDir, string $file): string { $source = \ltrim($this->files->relativePath($file, $tmpDir), '\\/'); - if (str_starts_with($source, $this->baseNamespace)) { + if (\str_starts_with($source, $this->baseNamespace)) { $source = \ltrim(\substr($source, \strlen($this->baseNamespace)), '\\/'); } @@ -87,15 +75,4 @@ private function tmpDir(): string return $this->files->normalizePath($directory, true); } - - /** - * Include all proto files from the directory. - */ - private function getProtoFiles(string $protoFile): array - { - return \array_filter( - $this->files->getFiles(\dirname($protoFile)), - static fn (string $file) => str_contains($file, '.proto') - ); - } } diff --git a/src/GRPC/ProtocCommandBuilder.php b/src/GRPC/ProtocCommandBuilder.php new file mode 100644 index 0000000..f67f50f --- /dev/null +++ b/src/GRPC/ProtocCommandBuilder.php @@ -0,0 +1,58 @@ +&1', + $this->protocBinaryPath ? '--plugin=' . $this->protocBinaryPath : '', + \escapeshellarg($tmpDir), + \escapeshellarg($tmpDir), + $this->buildDirs($protoDir), + \implode(' ', \array_map('escapeshellarg', $this->getProtoFiles($protoDir))) + ); + } + + /** + * Include all proto files from the directory. + */ + private function getProtoFiles(string $protoDir): array + { + return \array_filter( + $this->files->getFiles($protoDir), + static fn(string $file) => \str_ends_with($file, '.proto') + ); + } + + private function buildDirs(string $protoDir): string + { + $dirs = \array_filter([ + $this->config->getServicesBasePath(), + $protoDir, + ]); + + if ($dirs === []) { + return ''; + } + + return ' -I=' . \implode(' -I=', \array_map('escapeshellarg', $dirs)); + } +} diff --git a/tests/app/config/grpc.php b/tests/app/config/grpc.php index 340b98c..dc9bd65 100644 --- a/tests/app/config/grpc.php +++ b/tests/app/config/grpc.php @@ -9,6 +9,10 @@ */ 'binaryPath' => directory('app') . '../protoc-gen-php-grpc', + 'generatedPath' => null, + 'namespace' => null, + 'servicesBasePath' => directory('app') . 'proto', + 'services' => [ directory('app') . 'proto/echo.proto', directory('app') . 'proto/foo.proto', diff --git a/tests/src/Bootloader/GRPCBootloaderTest.php b/tests/src/Bootloader/GRPCBootloaderTest.php index cd9e9ca..f8e6640 100644 --- a/tests/src/Bootloader/GRPCBootloaderTest.php +++ b/tests/src/Bootloader/GRPCBootloaderTest.php @@ -73,6 +73,9 @@ public function testConfigShouldBeDefined(): void $this->assertSame([ 'binaryPath' => $this->getDirectoryByAlias('app') . '../protoc-gen-php-grpc', + 'generatedPath' => null, + 'namespace' => null, + 'servicesBasePath' => $this->getDirectoryByAlias('app') . 'proto', 'services' => [ $this->getDirectoryByAlias('app') . 'proto/echo.proto', $this->getDirectoryByAlias('app') . 'proto/foo.proto', diff --git a/tests/src/Config/GRPCConfigTest.php b/tests/src/Config/GRPCConfigTest.php index 910351b..b4b1f33 100644 --- a/tests/src/Config/GRPCConfigTest.php +++ b/tests/src/Config/GRPCConfigTest.php @@ -56,4 +56,52 @@ public function testGetNotExistsInterceptors(): void $this->assertSame([], $config->getInterceptors()); } + + public function testGetGeneratedPath(): void + { + $config = new GRPCConfig([ + 'generatedPath' => 'foo', + ]); + + $this->assertSame('foo', $config->getGeneratedPath()); + } + + public function testGetNonExistsGeneratedPath(): void + { + $config = new GRPCConfig(); + + $this->assertNull($config->getGeneratedPath()); + } + + public function testGetNamespace(): void + { + $config = new GRPCConfig([ + 'namespace' => 'foo', + ]); + + $this->assertSame('foo', $config->getNamespace()); + } + + public function testGetNonExistsNamespace(): void + { + $config = new GRPCConfig(); + + $this->assertNull($config->getNamespace()); + } + + public function testGetServicesBasePath(): void + { + $config = new GRPCConfig([ + 'servicesBasePath' => 'foo', + ]); + + $this->assertSame('foo', $config->getServicesBasePath()); + } + + public function testGetNonExistsServicesBasePath(): void + { + $config = new GRPCConfig(); + + $this->assertNull($config->getServicesBasePath()); + } } diff --git a/tests/src/GRPC/ProtocCommandBuilderTest.php b/tests/src/GRPC/ProtocCommandBuilderTest.php new file mode 100644 index 0000000..2a32901 --- /dev/null +++ b/tests/src/GRPC/ProtocCommandBuilderTest.php @@ -0,0 +1,72 @@ + 'path4', + ]), + 'path3' + ); + + $files->shouldReceive('ensureDirectory') + ->with($directory = \sys_get_temp_dir() . '/' . \spl_object_hash($builder)) + ->andReturn(); + + $files->shouldReceive('normalizePath')->with($directory, true)->andReturn('path5'); + + $files->shouldReceive('getFiles')->with('path1') + ->andReturn([ + 'message.proto.tmp', + 'service.proto.tmp', + 'message.proto', + 'service.proto', + '.gitignore', + '.gitattributes' + ]); + + $this->assertSame( + "protoc --plugin=path3 --php_out='path2' --php-grpc_out='path2' -I='path4' -I='path1' 'message.proto' 'service.proto' 2>&1", + $builder->build('path1', 'path2') + ); + } + + public function testBuildWithNullServicesBasePath(): void + { + $builder = new ProtocCommandBuilder( + $files = m::mock(FilesInterface::class), + new GRPCConfig([ + 'servicesBasePath' => null, + ]), + 'path3' + ); + + $files->shouldReceive('getFiles')->with('path1') + ->andReturn([ + 'message.proto.tmp', + 'service.proto.tmp', + 'message.proto', + 'service.proto', + '.gitignore', + '.gitattributes' + ]); + + $this->assertSame( + "protoc --plugin=path3 --php_out='path2' --php-grpc_out='path2' -I='path1' 'message.proto' 'service.proto' 2>&1", + $builder->build('path1', 'path2') + ); + } +}