├── tests ├── Protos │ └── .gitignore ├── Fixtures │ ├── compiler │ │ └── generator-request-simple.bin │ ├── Service │ │ └── SearchService.tpl │ ├── Options │ │ ├── ParentMessage │ │ │ └── InnerMessage │ │ │ │ └── InnerMessageEnum.tpl │ │ └── SimpleMessage.tpl │ ├── Person │ │ └── PhoneType.tpl │ └── Extension │ │ └── Extension.tpl ├── Resources │ ├── proto2 │ │ ├── repeated.proto │ │ ├── simple.proto │ │ ├── addressbook.proto │ │ └── options.proto │ └── proto3 │ │ └── route_guide.proto ├── travis │ └── install-protobuf.sh ├── OptionsTest.php ├── Descriptor │ ├── ToStreamTest.php │ ├── MessageDescriptorTest.php │ ├── MergeTest.php │ ├── ClearTest.php │ └── SerializedSizeTest.php ├── Generator │ └── Message │ │ ├── SerializedSizeGeneratorTest.php │ │ ├── WriteToGeneratorTest.php │ │ ├── ReadFromGeneratorTest.php │ │ └── SerializedSizeFieldStatementGeneratorTest.php ├── Command │ ├── ApplicationTest.php │ ├── PluginCommandTest.php │ └── GenerateCommandTest.php ├── CompilerTest.php ├── EntityTest.php └── EntityBuilderTest.php ├── src ├── Generator │ ├── EntityVisitor.php │ ├── GeneratorVisitor.php │ ├── Message │ │ ├── ClearGenerator.php │ │ ├── FromStreamGenerator.php │ │ ├── ToStreamGenerator.php │ │ ├── MergeGenerator.php │ │ ├── ConstructGenerator.php │ │ ├── SerializedSizeGenerator.php │ │ ├── SerializedSizeFieldStatementGenerator.php │ │ ├── WriteToGenerator.php │ │ ├── DescriptorGenerator.php │ │ ├── ReadFromGenerator.php │ │ ├── WriteFieldStatementGenerator.php │ │ ├── FromArrayGenerator.php │ │ └── ReadFieldStatementGenerator.php │ ├── MessageGenerator.php │ ├── ServiceGenerator.php │ ├── EnumGenerator.php │ └── ExtensionGenerator.php ├── Options.php ├── Context.php ├── Generator.php ├── Command │ ├── Application.php │ ├── PluginCommand.php │ └── GenerateCommand.php ├── Compiler.php ├── Entity.php └── Protoc │ └── ProcessBuilder.php ├── ruleset.xml ├── .travis.yml ├── .gitignore ├── bin └── protobuf ├── LICENSE ├── phpunit.xml.dist ├── composer.json ├── Makefile └── README.md /tests/Protos/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /tests/Fixtures/compiler/generator-request-simple.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protobuf-php/protobuf-plugin/HEAD/tests/Fixtures/compiler/generator-request-simple.bin -------------------------------------------------------------------------------- /src/Generator/EntityVisitor.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface EntityVisitor 13 | { 14 | /** 15 | * @param \Protobuf\Compiler\Entity $entity 16 | */ 17 | public function visit(Entity $entity); 18 | } 19 | -------------------------------------------------------------------------------- /ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Coding Standard 4 | 5 | */Resources/* 6 | 7 | 8 | 9 | 10 | 11 | 12 | 0 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/Resources/proto2/repeated.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package ProtobufCompilerTest.Protos; 4 | 5 | message Repeated { 6 | 7 | message Nested { 8 | optional int32 id = 1; 9 | } 10 | 11 | enum Enum { 12 | FOO = 0; 13 | BAR = 1; 14 | } 15 | 16 | repeated string string = 1; 17 | repeated int32 int = 2; 18 | repeated Nested nested = 3; 19 | repeated int32 packed = 4 [packed=true]; 20 | repeated bytes bytes = 5; 21 | repeated Enum enum = 6; 22 | } 23 | -------------------------------------------------------------------------------- /src/Generator/GeneratorVisitor.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface GeneratorVisitor 14 | { 15 | /** 16 | * @param \Protobuf\Compiler\Entity $entity 17 | * @param \Zend\Code\Generator\GeneratorInterface $class 18 | */ 19 | public function visit(Entity $entity, GeneratorInterface $class); 20 | } 21 | -------------------------------------------------------------------------------- /tests/Fixtures/Service/SearchService.tpl: -------------------------------------------------------------------------------- 1 | run(); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | ./tests 16 | ./tests/Resources/* 17 | 18 | 19 | 20 | 21 | 22 | benchmark 23 | deprecated 24 | 25 | 26 | 27 | 28 | 29 | ./vendor/* 30 | 31 | 32 | ./src 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/OptionsTest.php: -------------------------------------------------------------------------------- 1 | 1, 13 | 'verbose' => 1, 14 | 'psr4' => ['MyPackage'], 15 | ]); 16 | 17 | $options2 = Options::fromArray([ 18 | 'generate-imported' => 0, 19 | 'verbose' => 0 20 | ]); 21 | 22 | $this->assertTrue($options1->getVerbose()); 23 | $this->assertTrue($options1->getGenerateImported()); 24 | $this->assertEquals(['MyPackage'], $options1->getPsr4()); 25 | 26 | $this->assertFalse($options2->getVerbose()); 27 | $this->assertFalse($options2->getGenerateImported()); 28 | } 29 | 30 | public function testDefaults() 31 | { 32 | $options = Options::fromArray([]); 33 | 34 | $this->assertFalse($options->getVerbose()); 35 | $this->assertFalse($options->getGenerateImported()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Fixtures/Options/ParentMessage/InnerMessage/InnerMessageEnum.tpl: -------------------------------------------------------------------------------- 1 | markTestIncompleteIfProtoClassNotFound(); 20 | 21 | parent::setUp(); 22 | } 23 | 24 | public function testAddressBookToStream() 25 | { 26 | $config = $this->getMock(Configuration::CLASS); 27 | $stream = $this->getMock(Stream::CLASS, [], [], '', false); 28 | $context = $this->getMock(WriteContext::CLASS, [], [], '', false); 29 | $message = $this->getMockBuilder(AddressBook::CLASS) 30 | ->setMethods(['writeTo']) 31 | ->getMock(); 32 | 33 | $stream->expects($this->once()) 34 | ->method('seek') 35 | ->with($this->equalTo(0)); 36 | 37 | $config->expects($this->once()) 38 | ->method('createWriteContext') 39 | ->willReturn($context); 40 | 41 | $context->expects($this->once()) 42 | ->method('getStream') 43 | ->willReturn($stream); 44 | 45 | $message->expects($this->once()) 46 | ->method('writeTo') 47 | ->with($this->equalTo($context)); 48 | 49 | $this->assertSame($stream, $message->toStream($config)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Options.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class Options 11 | { 12 | /** 13 | * @var string[] 14 | */ 15 | protected $psr4; 16 | 17 | /** 18 | * @var bool 19 | */ 20 | protected $verbose = false; 21 | 22 | /** 23 | * @var bool 24 | */ 25 | protected $generateImported = false; 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function getVerbose() 31 | { 32 | return $this->verbose; 33 | } 34 | 35 | /** 36 | * @return bool 37 | */ 38 | public function getGenerateImported() 39 | { 40 | return $this->generateImported; 41 | } 42 | 43 | /** 44 | * @return array 45 | */ 46 | public function getPsr4() 47 | { 48 | return $this->psr4; 49 | } 50 | 51 | /** 52 | * @param array $params 53 | * 54 | * @return \Protobuf\Compiler\Generator\Options 55 | */ 56 | public static function fromArray(array $params) 57 | { 58 | $options = new Options(); 59 | 60 | if (isset($params['verbose'])) { 61 | $options->verbose = (bool) $params['verbose']; 62 | } 63 | 64 | if (isset($params['generate-imported'])) { 65 | $options->generateImported = (bool) $params['generate-imported']; 66 | } 67 | 68 | if (isset($params['psr4'])) { 69 | $options->psr4 = $params['psr4']; 70 | } 71 | 72 | return $options; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Generator/Message/SerializedSizeGeneratorTest.php: -------------------------------------------------------------------------------- 1 | createContext([ 17 | [ 18 | 'name' => 'simple.proto', 19 | 'package' => 'ProtobufCompilerTest.Protos', 20 | 'values' => [ 21 | 'messages' => [ 22 | [ 23 | 'name' => 'Simple', 24 | 'fields' => [ 25 | 1 => ['lines', Field::TYPE_INT32, Field::LABEL_REPEATED] 26 | ] 27 | ] 28 | ] 29 | ] 30 | ] 31 | ]); 32 | 33 | $entity = $context->getEntity('ProtobufCompilerTest.Protos.Simple'); 34 | $generator = new SerializedSizeGenerator($context); 35 | $descritor = $entity->getDescriptor(); 36 | $field = $descritor->getFieldList()[0]; 37 | 38 | $actual = $this->invokeMethod($generator, 'generateBody', [$entity]); 39 | $expected = <<<'CODE' 40 | $calculator = $context->getSizeCalculator(); 41 | $size = 0; 42 | 43 | if ($this->lines !== null) { 44 | foreach ($this->lines as $val) { 45 | $size += 1; 46 | $size += $calculator->computeVarintSize($val); 47 | } 48 | } 49 | 50 | if ($this->extensions !== null) { 51 | $size += $this->extensions->serializedSize($context); 52 | } 53 | 54 | return $size; 55 | CODE; 56 | 57 | $this->assertEquals($expected, implode(PHP_EOL, $actual)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Generator/Message/ClearGenerator.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ClearGenerator extends BaseGenerator implements GeneratorVisitor 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function visit(Entity $entity, GeneratorInterface $class) 23 | { 24 | $class->addMethodFromGenerator($this->generateClearMethod($entity)); 25 | } 26 | 27 | /** 28 | * @param \Protobuf\Compiler\Entity $entity 29 | * 30 | * @return \Zend\Code\Generator\GeneratorInterface 31 | */ 32 | protected function generateClearMethod(Entity $entity) 33 | { 34 | $lines = $this->generateBody($entity); 35 | $body = implode(PHP_EOL, $lines); 36 | $method = MethodGenerator::fromArray([ 37 | 'name' => 'clear', 38 | 'body' => $body, 39 | 'docblock' => [ 40 | 'shortDescription' => "{@inheritdoc}" 41 | ] 42 | ]); 43 | 44 | return $method; 45 | } 46 | 47 | /** 48 | * @param \Protobuf\Compiler\Entity $entity 49 | * 50 | * @return string[] 51 | */ 52 | public function generateBody(Entity $entity) 53 | { 54 | $body = []; 55 | $descriptor = $entity->getDescriptor(); 56 | $fields = $descriptor->getFieldList() ?: []; 57 | 58 | foreach ($fields as $field) { 59 | $name = $field->getName(); 60 | $value = $this->getDefaultFieldValue($field); 61 | 62 | $body[] = sprintf('$this->%s = %s;', $name, $value); 63 | } 64 | 65 | return $body; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Command/ApplicationTest.php: -------------------------------------------------------------------------------- 1 | input = $this->getMock('Symfony\Component\Console\Input\InputInterface'); 30 | 31 | $this->generateCommand = $this->getMockBuilder(GenerateCommand::CLASS) 32 | ->disableOriginalConstructor() 33 | ->getMock(); 34 | 35 | $this->pluginCommand = $this->getMockBuilder(PluginCommand::CLASS) 36 | ->disableOriginalConstructor() 37 | ->getMock(); 38 | } 39 | 40 | public function testSingleCommandApplication() 41 | { 42 | $this->generateCommand->expects($this->once()) 43 | ->method('getName') 44 | ->willReturn('my-command'); 45 | 46 | $application = new Application($this->generateCommand, $this->pluginCommand); 47 | $definition = $this->invokeMethod($application, 'getDefinition'); 48 | $defaultCommands = $this->invokeMethod($application, 'getDefaultCommands'); 49 | $commandName = $this->invokeMethod($application, 'getCommandName', [$this->input]); 50 | 51 | $this->assertEquals('my-command', $commandName); 52 | $this->assertEquals([], $definition->getArguments()); 53 | $this->assertSame($this->generateCommand, $defaultCommands[2]); 54 | $this->assertSame($this->pluginCommand, $defaultCommands[3]); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Generator/Message/WriteToGeneratorTest.php: -------------------------------------------------------------------------------- 1 | createContext([ 17 | [ 18 | 'name' => 'simple.proto', 19 | 'package' => 'ProtobufCompilerTest.Protos', 20 | 'values' => [ 21 | 'messages' => [ 22 | [ 23 | 'name' => 'Simple', 24 | 'fields' => [ 25 | 1 => ['lines', Field::TYPE_INT32, Field::LABEL_REPEATED] 26 | ] 27 | ] 28 | ] 29 | ] 30 | ] 31 | ]); 32 | 33 | $entity = $context->getEntity('ProtobufCompilerTest.Protos.Simple'); 34 | $generator = new WriteToGenerator($context); 35 | $descritor = $entity->getDescriptor(); 36 | $field = $descritor->getFieldList()[0]; 37 | 38 | $actual = $this->invokeMethod($generator, 'generateBody', [$entity]); 39 | $expected = <<<'CODE' 40 | $stream = $context->getStream(); 41 | $writer = $context->getWriter(); 42 | $sizeContext = $context->getComputeSizeContext(); 43 | 44 | if ($this->lines !== null) { 45 | foreach ($this->lines as $val) { 46 | $writer->writeVarint($stream, 8); 47 | $writer->writeVarint($stream, $val); 48 | } 49 | } 50 | 51 | if ($this->extensions !== null) { 52 | $this->extensions->writeTo($context); 53 | } 54 | 55 | return $stream; 56 | CODE; 57 | 58 | $this->assertEquals($expected, implode(PHP_EOL, $actual)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "protobuf-php/protobuf-plugin", 3 | "description" : "PHP Code generator plugin from Google's Protocol Buffers", 4 | "keywords" : ["protobuf", "protocol buffer", "serializing"], 5 | "homepage" : "https://github.com/protobuf-php/protobuf-plugin", 6 | "type" : "library", 7 | "license" : "MIT", 8 | "prefer-stable" : true, 9 | "authors" : [ 10 | { 11 | "name" : "Fabio B. Silva", 12 | "email" : "fabio.bat.silva@gmail.com", 13 | "homepage": "https://github.com/FabioBatSilva" 14 | } 15 | ], 16 | "bin" : ["bin/protobuf"], 17 | "require" : { 18 | "php" : ">=5.5.0", 19 | "protobuf-php/protobuf" : ">=0.1", 20 | "protobuf-php/google-protobuf-proto" : ">=0.1", 21 | "zendframework/zend-code" : "^2.6", 22 | "psr/log" : "^1.0", 23 | "symfony/console" : "^2.5|^3.0", 24 | "symfony/process" : "^2.5|^3.0", 25 | "doctrine/inflector" : "^1.0" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit" : "~4", 29 | "mikey179/vfsstream" : "~1.6", 30 | "instaclick/coding-standard" : "~1.1", 31 | "squizlabs/php_codesniffer" : "~1.5", 32 | "satooshi/php-coveralls" : "~0.6", 33 | "php-mock/php-mock-phpunit" : "^0.2", 34 | "instaclick/object-calisthenics-sniffs" : "dev-master", 35 | "instaclick/symfony2-coding-standard" : "dev-remaster" 36 | }, 37 | "autoload" : { 38 | "psr-4": { 39 | "Protobuf\\Compiler\\": "src/" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "ProtobufCompilerTest\\": "tests" 45 | } 46 | }, 47 | "extra": { 48 | "branch-alias": { 49 | "dev-master": "0.1.x-dev" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Generator/Message/FromStreamGenerator.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class FromStreamGenerator extends BaseGenerator implements GeneratorVisitor 21 | { 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function visit(Entity $entity, GeneratorInterface $class) 26 | { 27 | $class->addMethodFromGenerator($this->generateMethod($entity)); 28 | } 29 | 30 | /** 31 | * @param \Protobuf\Compiler\Entity $entity 32 | * 33 | * @return string 34 | */ 35 | protected function generateMethod(Entity $entity) 36 | { 37 | $lines = $this->generateBody($entity); 38 | $body = implode(PHP_EOL, $lines); 39 | $method = MethodGenerator::fromArray([ 40 | 'name' => 'fromStream', 41 | 'body' => $body, 42 | 'static' => true, 43 | 'parameters' => [ 44 | [ 45 | 'name' => 'stream', 46 | 'type' => 'mixed', 47 | ], 48 | [ 49 | 'name' => 'configuration', 50 | 'type' => '\Protobuf\Configuration', 51 | 'defaultValue' => null 52 | ] 53 | ], 54 | 'docblock' => [ 55 | 'shortDescription' => "{@inheritdoc}" 56 | ] 57 | ]); 58 | 59 | return $method; 60 | } 61 | 62 | /** 63 | * @return string[] 64 | */ 65 | public function generateBody() 66 | { 67 | return [ 68 | 'return new self($stream, $configuration);' 69 | ]; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # vim: ts=4:sw=4:noexpandtab!: 2 | 3 | BASEDIR := $(shell pwd) 4 | COMPOSER := $(shell which composer) 5 | PROTOC_VERSION := $(shell protoc --version | grep -oEi '([0-9]).*' | cut -d '.' -f 1) 6 | 7 | help: 8 | @echo "---------------------------------------------" 9 | @echo "List of available targets:" 10 | @echo " composer-install - Installs composer dependencies." 11 | @echo " phpcs - Runs PHP Code Sniffer." 12 | @echo " phpunit - Runs tests." 13 | @echo " phpunit-coverage-clover - Runs tests to genereate coverage clover." 14 | @echo " phpunit-coverage-html - Runs tests to genereate coverage html." 15 | @echo " help - Shows this dialog." 16 | @exit 0 17 | 18 | all: install phpunit 19 | 20 | install: composer-install proto-generate 21 | 22 | test: proto-generate phpcs phpunit 23 | 24 | composer-install: 25 | ifdef COMPOSER 26 | php $(COMPOSER) install --prefer-source --no-interaction; 27 | else 28 | @echo "Composer not found !!" 29 | @echo 30 | @echo "curl -sS https://getcomposer.org/installer | php" 31 | @echo "mv composer.phar /usr/local/bin/composer" 32 | endif 33 | 34 | proto-clean: 35 | rm -rf $(BASEDIR)/tests/Protos/*; 36 | 37 | proto-generate: proto-clean 38 | ifeq ($(PROTOC_VERSION), 3) 39 | php $(BASEDIR)/bin/protobuf --include-descriptors \ 40 | --psr4 ProtobufCompilerTest\\Protos \ 41 | -o $(BASEDIR)/tests/Protos \ 42 | -i $(BASEDIR)/tests/Resources \ 43 | $(BASEDIR)/tests/Resources/proto3/*.proto 44 | endif 45 | php $(BASEDIR)/bin/protobuf --include-descriptors \ 46 | --psr4 ProtobufCompilerTest\\Protos \ 47 | -o $(BASEDIR)/tests/Protos \ 48 | -i $(BASEDIR)/tests/Resources \ 49 | $(BASEDIR)/tests/Resources/proto2/*.proto 50 | 51 | phpunit: 52 | php $(BASEDIR)/vendor/bin/phpunit -v; 53 | 54 | phpunit-coverage-clover: 55 | php $(BASEDIR)/vendor/bin/phpunit -v --coverage-clover ./build/logs/clover.xml; 56 | 57 | phpunit-coverage-html: 58 | php $(BASEDIR)/vendor/bin/phpunit -v --coverage-html ./build/coverage; 59 | 60 | phpcs: 61 | php $(BASEDIR)/vendor/bin/phpcs -p --extensions=php --standard=ruleset.xml src; 62 | 63 | .PHONY: composer-install phpunit phpcs help -------------------------------------------------------------------------------- /src/Context.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Context 13 | { 14 | /** 15 | * @var \Protobuf\Entity[] 16 | */ 17 | protected $entities; 18 | 19 | /** 20 | * @var \Protobuf\Compiler\Options 21 | */ 22 | protected $options; 23 | 24 | /** 25 | * @var \Protobuf\Configuration 26 | */ 27 | protected $configuration; 28 | 29 | /** 30 | * @param array $entities 31 | * @param \Protobuf\Compiler\Options $options 32 | * @param \Protobuf\Configuration $config 33 | */ 34 | public function __construct(array $entities, Options $options, Configuration $config) 35 | { 36 | $this->configuration = $config; 37 | $this->options = $options; 38 | $this->entities = $entities; 39 | } 40 | 41 | /** 42 | * @return bool 43 | */ 44 | public function hasProtobufExtension() 45 | { 46 | foreach ($this->entities as $entity) { 47 | if ($entity->isProtobufExtension()) { 48 | return true; 49 | } 50 | } 51 | 52 | return false; 53 | } 54 | 55 | /** 56 | * @return \Protobuf\Compiler\Options 57 | */ 58 | public function getOptions() 59 | { 60 | return $this->options; 61 | } 62 | 63 | /** 64 | * @return array 65 | */ 66 | public function getEntities() 67 | { 68 | return $this->entities; 69 | } 70 | 71 | /** 72 | * @return \Protobuf\Configuration 73 | */ 74 | public function getConfiguration() 75 | { 76 | return $this->configuration; 77 | } 78 | 79 | /** 80 | * @param string $fqcn 81 | * 82 | * @return \Protobuf\Entity 83 | */ 84 | public function getEntity($fqcn) 85 | { 86 | $class = trim($fqcn, '.'); 87 | 88 | if ( ! isset($this->entities[$class])) { 89 | throw new \LogicException("Unable to find class : $class"); 90 | } 91 | 92 | return $this->entities[$class]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Generator/Message/ToStreamGenerator.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class ToStreamGenerator extends BaseGenerator implements GeneratorVisitor 21 | { 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function visit(Entity $entity, GeneratorInterface $class) 26 | { 27 | $class->addMethodFromGenerator($this->generateToStreamMethod($entity)); 28 | } 29 | 30 | /** 31 | * @param \Protobuf\Compiler\Entity $entity 32 | * 33 | * @return \Zend\Code\Generator\GeneratorInterface 34 | */ 35 | protected function generateToStreamMethod(Entity $entity) 36 | { 37 | $lines = $this->generateBody($entity); 38 | $body = implode(PHP_EOL, $lines); 39 | $method = MethodGenerator::fromArray([ 40 | 'name' => 'toStream', 41 | 'body' => $body, 42 | 'parameters' => [ 43 | [ 44 | 'name' => 'configuration', 45 | 'type' => '\Protobuf\Configuration', 46 | 'defaultValue' => null 47 | ] 48 | ], 49 | 'docblock' => [ 50 | 'shortDescription' => "{@inheritdoc}" 51 | ] 52 | ]); 53 | 54 | return $method; 55 | } 56 | 57 | /** 58 | * @return string[] 59 | */ 60 | public function generateBody() 61 | { 62 | $body[] = '$config = $configuration ?: \Protobuf\Configuration::getInstance();'; 63 | $body[] = '$context = $config->createWriteContext();'; 64 | $body[] = '$stream = $context->getStream();'; 65 | $body[] = null; 66 | $body[] = '$this->writeTo($context);'; 67 | $body[] = '$stream->seek(0);'; 68 | $body[] = null; 69 | $body[] = 'return $stream;'; 70 | 71 | return $body; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Fixtures/Person/PhoneType.tpl: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class Generator extends BaseGenerator 20 | { 21 | /** 22 | * @var array 23 | */ 24 | protected $generators; 25 | /** 26 | * @param \Protobuf\Compiler\Context $context 27 | */ 28 | public function __construct(Context $context) 29 | { 30 | $this->context = $context; 31 | $this->generators = [ 32 | Entity::TYPE_ENUM => new EnumGenerator($context), 33 | Entity::TYPE_SERVICE => new ServiceGenerator($context), 34 | Entity::TYPE_MESSAGE => new MessageGenerator($context), 35 | Entity::TYPE_EXTENSION => new ExtensionGenerator($context) 36 | ]; 37 | } 38 | 39 | /** 40 | * @param Entity $entity 41 | */ 42 | public function visit(Entity $entity) 43 | { 44 | $type = $entity->getType(); 45 | $class = $entity->getNamespacedName(); 46 | $path = $this->getPsr4ClassPath($class); 47 | 48 | $entity->setPath($path); 49 | 50 | if ( ! isset($this->generators[$type])) { 51 | return; 52 | } 53 | 54 | $this->generators[$type]->visit($entity); 55 | } 56 | 57 | /** 58 | * @param string $class 59 | * 60 | * @return string 61 | */ 62 | protected function getPsr4ClassPath($class) 63 | { 64 | $options = $this->context->getOptions(); 65 | $psr4 = $options->getPsr4() ?: []; 66 | $class = trim($class, '\\'); 67 | 68 | foreach ($psr4 as $item) { 69 | $prefix = trim($item, '\\') . '\\'; 70 | $length = strlen($prefix); 71 | $start = substr($class, 0, $length); 72 | 73 | if ($start !== $prefix) { 74 | continue; 75 | } 76 | 77 | $name = trim(str_replace($prefix, '', $class), '\\'); 78 | $path = $this->getClassPath($name); 79 | 80 | return $path; 81 | } 82 | 83 | return $this->getClassPath($class); 84 | } 85 | 86 | /** 87 | * @param string $class 88 | * 89 | * @return string 90 | */ 91 | protected function getClassPath($class) 92 | { 93 | return str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php'; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/Descriptor/MessageDescriptorTest.php: -------------------------------------------------------------------------------- 1 | markTestIncompleteIfProtoClassNotFound(); 25 | 26 | parent::setUp(); 27 | } 28 | 29 | public function testComplexMessageDescriptor() 30 | { 31 | $descriptor = AddressBook::descriptor(); 32 | 33 | $this->assertInstanceOf(DescriptorProto::CLASS, $descriptor); 34 | $this->assertEquals('AddressBook', $descriptor->getName()); 35 | 36 | $this->assertCount(1, $descriptor->getFieldList()); 37 | 38 | $this->assertInstanceOf(FieldDescriptorProto::CLASS, $descriptor->getFieldList()[0]); 39 | $this->assertEquals(1, $descriptor->getFieldList()[0]->getNumber()); 40 | $this->assertEquals('person', $descriptor->getFieldList()[0]->getName()); 41 | $this->assertSame(Type::TYPE_MESSAGE(), $descriptor->getFieldList()[0]->getType()); 42 | $this->assertSame(Label::LABEL_REPEATED(), $descriptor->getFieldList()[0]->getLabel()); 43 | $this->assertEquals('.ProtobufCompilerTest.Protos.Person', $descriptor->getFieldList()[0]->getTypeName()); 44 | } 45 | 46 | public function testMessageDescriptorOptionsExtensions() 47 | { 48 | $descriptor = Options\MyMessage::descriptor(); 49 | 50 | $this->assertInstanceOf(DescriptorProto::CLASS, $descriptor); 51 | $this->assertEquals('MyMessage', $descriptor->getName()); 52 | 53 | $this->assertInstanceOf(MessageOptions::CLASS, $descriptor->getOptions()); 54 | 55 | $extensions = $descriptor->getOptions()->extensions(); 56 | 57 | $this->assertCount(2, $extensions); 58 | 59 | $this->assertContains(Options\Extension::myMessageOption(), $extensions); 60 | $this->assertContains(Options\Extension::myMessageOptionMsg(), $extensions); 61 | 62 | $myMessageOption = $extensions->get(Options\Extension::myMessageOption()); 63 | $myMessageOptionMsg = $extensions->get(Options\Extension::myMessageOptionMsg()); 64 | 65 | $this->assertSame(1234, $myMessageOption); 66 | $this->assertInstanceOf(Options\MyOption::CLASS, $myMessageOptionMsg); 67 | $this->assertSame('one,two,three,four', $myMessageOptionMsg->getLabel()); 68 | $this->assertSame('1234', $myMessageOptionMsg->getValue()); 69 | $this->assertSame(0, $myMessageOptionMsg->getVal()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Generator/MessageGenerator.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class MessageGenerator extends BaseGenerator implements EntityVisitor 29 | { 30 | /** 31 | * @var array 32 | */ 33 | protected $generators; 34 | 35 | /** 36 | * @param \Protobuf\Compiler\Context $context 37 | */ 38 | public function __construct(Context $context) 39 | { 40 | parent::__construct($context); 41 | 42 | $this->generators = [ 43 | new ConstructGenerator($context), 44 | new FieldsGenerator($context), 45 | new ExtensionsGenerator($context), 46 | new FromStreamGenerator($context), 47 | new FromArrayGenerator($context), 48 | new DescriptorGenerator($context), 49 | new ToStreamGenerator($context), 50 | new WriteToGenerator($context), 51 | new ReadFromGenerator($context), 52 | new SerializedSizeGenerator($context), 53 | new ClearGenerator($context), 54 | new MergeGenerator($context), 55 | ]; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function visit(Entity $entity) 62 | { 63 | $name = $entity->getName(); 64 | $namespace = $entity->getNamespace(); 65 | $descriptor = $entity->getDescriptor(); 66 | $shortDescription = 'Protobuf message : ' . $entity->getClass(); 67 | $class = ClassGenerator::fromArray([ 68 | 'name' => $name, 69 | 'namespacename' => $namespace, 70 | 'extendedClass' => '\Protobuf\AbstractMessage', 71 | 'docblock' => [ 72 | 'shortDescription' => $shortDescription 73 | ] 74 | ]); 75 | 76 | foreach ($this->generators as $generator) { 77 | $generator->visit($entity, $class); 78 | } 79 | 80 | $entity->setContent($this->generateFileContent($class, $entity)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/Resources/proto3/route_guide.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package ProtobufCompilerTest.Protos.RouteGuide; 4 | 5 | // Interface exported by the server. 6 | service RouteGuide { 7 | // A simple RPC. 8 | // 9 | // Obtains the feature at a given position. 10 | rpc GetFeature(Point) returns (Feature) {} 11 | 12 | // A server-to-client streaming RPC. 13 | // 14 | // Obtains the Features available within the given Rectangle. Results are 15 | // streamed rather than returned at once (e.g. in a response message with a 16 | // repeated field), as the rectangle may cover a large area and contain a 17 | // huge number of features. 18 | rpc ListFeatures(Rectangle) returns (stream Feature) {} 19 | 20 | // A client-to-server streaming RPC. 21 | // 22 | // Accepts a stream of Points on a route being traversed, returning a 23 | // RouteSummary when traversal is completed. 24 | rpc RecordRoute(stream Point) returns (RouteSummary) {} 25 | 26 | // A Bidirectional streaming RPC. 27 | // 28 | // Accepts a stream of RouteNotes sent while a route is being traversed, 29 | // while receiving other RouteNotes (e.g. from other users). 30 | rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} 31 | } 32 | 33 | // Points are represented as latitude-longitude pairs in the E7 representation 34 | // (degrees multiplied by 10**7 and rounded to the nearest integer). 35 | // Latitudes should be in the range +/- 90 degrees and longitude should be in 36 | // the range +/- 180 degrees (inclusive). 37 | message Point { 38 | int32 latitude = 1; 39 | int32 longitude = 2; 40 | } 41 | 42 | // A latitude-longitude rectangle, represented as two diagonally opposite 43 | // points "lo" and "hi". 44 | message Rectangle { 45 | // One corner of the rectangle. 46 | Point lo = 1; 47 | 48 | // The other corner of the rectangle. 49 | Point hi = 2; 50 | } 51 | 52 | // A feature names something at a given point. 53 | // 54 | // If a feature could not be named, the name is empty. 55 | message Feature { 56 | // The name of the feature. 57 | string name = 1; 58 | 59 | // The point where the feature is detected. 60 | Point location = 2; 61 | } 62 | 63 | // A RouteNote is a message sent while at a given point. 64 | message RouteNote { 65 | // The location from which the message is sent. 66 | Point location = 1; 67 | 68 | // The message to be sent. 69 | string message = 2; 70 | } 71 | 72 | // A RouteSummary is received in response to a RecordRoute rpc. 73 | // 74 | // It contains the number of individual points received, the number of 75 | // detected features, and the total distance covered as the cumulative sum of 76 | // the distance between each point. 77 | message RouteSummary { 78 | // The number of points received. 79 | int32 point_count = 1; 80 | 81 | // The number of known features passed while traversing the route. 82 | int32 feature_count = 2; 83 | 84 | // The distance covered in metres. 85 | int32 distance = 3; 86 | 87 | // The duration of the traversal in seconds. 88 | int32 elapsed_time = 4; 89 | } -------------------------------------------------------------------------------- /tests/Generator/Message/ReadFromGeneratorTest.php: -------------------------------------------------------------------------------- 1 | createContext([ 17 | [ 18 | 'name' => 'simple.proto', 19 | 'package' => 'ProtobufCompilerTest.Protos', 20 | 'values' => [ 21 | 'messages' => [ 22 | [ 23 | 'name' => 'Simple', 24 | 'fields' => [ 25 | 1 => ['lines', Field::TYPE_INT32, Field::LABEL_REPEATED] 26 | ] 27 | ] 28 | ] 29 | ] 30 | ] 31 | ]); 32 | 33 | $entity = $context->getEntity('ProtobufCompilerTest.Protos.Simple'); 34 | $generator = new ReadFromGenerator($context); 35 | $descritor = $entity->getDescriptor(); 36 | $field = $descritor->getFieldList()[0]; 37 | 38 | $actual = $this->invokeMethod($generator, 'generateBody', [$entity]); 39 | $expected = <<<'CODE' 40 | $reader = $context->getReader(); 41 | $length = $context->getLength(); 42 | $stream = $context->getStream(); 43 | 44 | $limit = ($length !== null) 45 | ? ($stream->tell() + $length) 46 | : null; 47 | 48 | while ($limit === null || $stream->tell() < $limit) { 49 | 50 | if ($stream->eof()) { 51 | break; 52 | } 53 | 54 | $key = $reader->readVarint($stream); 55 | $wire = \Protobuf\WireFormat::getTagWireType($key); 56 | $tag = \Protobuf\WireFormat::getTagFieldNumber($key); 57 | 58 | if ($stream->eof()) { 59 | break; 60 | } 61 | 62 | if ($tag === 1) { 63 | \Protobuf\WireFormat::assertWireType($wire, 5); 64 | 65 | if ($this->lines === null) { 66 | $this->lines = new \Protobuf\ScalarCollection(); 67 | } 68 | 69 | $this->lines->add($reader->readVarint($stream)); 70 | 71 | continue; 72 | } 73 | 74 | $extensions = $context->getExtensionRegistry(); 75 | $extension = $extensions ? $extensions->findByNumber(__CLASS__, $tag) : null; 76 | 77 | if ($extension !== null) { 78 | $this->extensions()->add($extension, $extension->readFrom($context, $wire)); 79 | 80 | continue; 81 | } 82 | 83 | if ($this->unknownFieldSet === null) { 84 | $this->unknownFieldSet = new \Protobuf\UnknownFieldSet(); 85 | } 86 | 87 | $data = $reader->readUnknown($stream, $wire); 88 | $unknown = new \Protobuf\Unknown($tag, $wire, $data); 89 | 90 | $this->unknownFieldSet->add($unknown); 91 | 92 | } 93 | CODE; 94 | 95 | $this->assertEquals($expected, implode(PHP_EOL, $actual)); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Command/Application.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class Application extends SymfonyApplication 16 | { 17 | /** 18 | * @var \Symfony\Component\Console\Command\Command 19 | */ 20 | private $generateCommand; 21 | 22 | /** 23 | * @var \Symfony\Component\Console\Command\Command 24 | */ 25 | private $pluginCommand; 26 | 27 | /** 28 | * Constructor. 29 | * 30 | * @param \Symfony\Component\Console\Command\Command $generateCommand 31 | * @param \Symfony\Component\Console\Command\Command $pluginCommand 32 | */ 33 | public function __construct(Command $generateCommand, Command $pluginCommand) 34 | { 35 | $this->generateCommand = $generateCommand; 36 | $this->pluginCommand = $pluginCommand; 37 | 38 | parent::__construct('protobuf'); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function getCommandName(InputInterface $input) 45 | { 46 | $stream = $this->getStdinStream(); 47 | $hasStdin = $stream->getSize() > 0; 48 | 49 | $this->pluginCommand->setStream($stream); 50 | 51 | return $hasStdin 52 | ? $this->pluginCommand->getName() 53 | : $this->generateCommand->getName(); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | protected function getDefaultCommands() 60 | { 61 | $commands = parent::getDefaultCommands(); 62 | $commands[] = $this->generateCommand; 63 | $commands[] = $this->pluginCommand; 64 | 65 | return $commands; 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function getDefinition() 72 | { 73 | $definition = parent::getDefinition(); 74 | 75 | $definition->setArguments(); 76 | 77 | return $definition; 78 | } 79 | 80 | /** 81 | * @return \Protobuf\Stream 82 | */ 83 | protected function getStdinStream() 84 | { 85 | $handle = fopen('php://stdin', 'r'); 86 | $stream = Stream::create(); 87 | $counter = 0; 88 | 89 | stream_set_blocking($handle, false); 90 | 91 | while ( ! feof($handle) && ($counter++ < 10)) { 92 | 93 | $buffer = fread($handle, 1024); 94 | $length = mb_strlen($buffer, '8bit'); 95 | 96 | if ($length > 0) { 97 | 98 | $stream->write($buffer, $length); 99 | $counter = 0; 100 | 101 | continue; 102 | } 103 | 104 | usleep(1000); 105 | } 106 | 107 | $stream->seek(0); 108 | fclose($handle); 109 | 110 | return $stream; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/Command/PluginCommandTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('Protobuf\Compiler\Compiler') 16 | ->disableOriginalConstructor() 17 | ->getMock(); 18 | 19 | $streamIn = $this->getMockBuilder('Protobuf\Stream') 20 | ->disableOriginalConstructor() 21 | ->getMock(); 22 | 23 | $streamOut = $this->getMockBuilder('Protobuf\Stream') 24 | ->disableOriginalConstructor() 25 | ->getMock(); 26 | $mock = $this->getMockBuilder(PluginCommand::CLASS) 27 | ->setMethods(['createCompiler', 'writeStream']) 28 | ->getMock(); 29 | 30 | $mock->setStream($streamIn); 31 | 32 | $mock->expects($this->once()) 33 | ->method('createCompiler') 34 | ->willReturn($compiler); 35 | 36 | $mock->expects($this->once()) 37 | ->method('writeStream') 38 | ->with($streamOut); 39 | 40 | $compiler->expects($this->once()) 41 | ->method('compile') 42 | ->willReturn($streamOut) 43 | ->with($this->equalTo($streamIn)); 44 | 45 | $application = new Application(); 46 | 47 | $application->add($mock); 48 | 49 | $command = $application->find('protobuf:plugin'); 50 | $commandTester = new CommandTester($command); 51 | 52 | $commandTester->execute([]); 53 | } 54 | 55 | /** 56 | * @expectedException RuntimeException 57 | * @expectedExceptionMessage Unable to read standard input 58 | */ 59 | public function testExecuteException() 60 | { 61 | $application = new Application(); 62 | $mock = $this->getMockBuilder(PluginCommand::CLASS) 63 | ->setMethods(['createCompiler', 'createStream']) 64 | ->getMock(); 65 | 66 | $application->add($mock); 67 | 68 | $command = $application->find('protobuf:plugin'); 69 | $commandTester = new CommandTester($command); 70 | 71 | $commandTester->execute([]); 72 | } 73 | 74 | public function testCreateProcessBuilder() 75 | { 76 | $command = new PluginCommand(); 77 | $output = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); 78 | $compiler = $this->invokeMethod($command, 'createCompiler', [$output]); 79 | 80 | $this->assertInstanceOf('Protobuf\Compiler\Compiler', $compiler); 81 | } 82 | 83 | public function testCreateConsoleLogger() 84 | { 85 | $command = new PluginCommand(); 86 | $output = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); 87 | $compiler = $this->invokeMethod($command, 'createConsoleLogger', [$output]); 88 | 89 | $this->assertInstanceOf('Symfony\Component\Console\Logger\ConsoleLogger', $compiler); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Generator/Message/MergeGenerator.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class MergeGenerator extends BaseGenerator implements GeneratorVisitor 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function visit(Entity $entity, GeneratorInterface $class) 27 | { 28 | $class->addMethodFromGenerator($this->generateMethod($entity)); 29 | } 30 | 31 | /** 32 | * @param \Protobuf\Compiler\Entity $entity 33 | * 34 | * @return MethodGenerator 35 | */ 36 | protected function generateMethod(Entity $entity) 37 | { 38 | $lines = $this->generateBody($entity); 39 | $body = implode(PHP_EOL, $lines); 40 | $method = MethodGenerator::fromArray([ 41 | 'name' => 'merge', 42 | 'body' => $body, 43 | 'parameters' => [ 44 | [ 45 | 'name' => 'message', 46 | 'type' => '\Protobuf\Message' 47 | ] 48 | ], 49 | 'docblock' => [ 50 | 'shortDescription' => "{@inheritdoc}" 51 | ] 52 | ]); 53 | 54 | return $method; 55 | } 56 | 57 | /** 58 | * @param \Protobuf\Compiler\Entity $entity 59 | * 60 | * @return string[] 61 | */ 62 | public function generateBody(Entity $entity) 63 | { 64 | $lines = []; 65 | $descriptor = $entity->getDescriptor(); 66 | $class = $entity->getNamespacedName(); 67 | $fields = $descriptor->getFieldList() ?: []; 68 | $message = var_export("Argument 1 passed to %s must be a %s, %s given", true); 69 | $exception = 'sprintf(' . $message . ', __METHOD__, __CLASS__, get_class($message))'; 70 | 71 | $lines[] = 'if ( ! $message instanceof ' . $class . ') {'; 72 | $lines[] = ' throw new \InvalidArgumentException(' . $exception . ');'; 73 | $lines[] = '}'; 74 | $lines[] = null; 75 | 76 | foreach ($fields as $field) { 77 | $item = $this->generateFieldMerge($entity, $field); 78 | $lines = array_merge($lines, $item); 79 | } 80 | 81 | return $lines; 82 | } 83 | 84 | /** 85 | * @param \Protobuf\Compiler\Entity $entity 86 | * @param \google\protobuf\FieldDescriptorProto $field 87 | * 88 | * @return array 89 | */ 90 | protected function generateFieldMerge(Entity $entity, FieldDescriptorProto $field) 91 | { 92 | $lines = []; 93 | $fieldName = $field->getName(); 94 | $format = '$this->%s = ($message->%s !== null) ? $message->%s : $this->%s;'; 95 | $lines[] = sprintf($format, $fieldName, $fieldName, $fieldName, $fieldName); 96 | 97 | return $lines; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Generator/Message/ConstructGenerator.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ConstructGenerator extends BaseGenerator implements GeneratorVisitor 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function visit(Entity $entity, GeneratorInterface $class) 23 | { 24 | if ( ! $this->hasDefaultValue($entity)) { 25 | return; 26 | } 27 | 28 | $class->addMethodFromGenerator($this->generateConstructorMethod($entity)); 29 | } 30 | 31 | /** 32 | * @param \Protobuf\Compiler\Entity $entity 33 | * 34 | * @return \Zend\Code\Generator\GeneratorInterface 35 | */ 36 | protected function generateConstructorMethod(Entity $entity) 37 | { 38 | $lines = $this->generateBody($entity); 39 | $body = implode(PHP_EOL, $lines); 40 | $method = MethodGenerator::fromArray([ 41 | 'name' => '__construct', 42 | 'body' => $body, 43 | 'parameters' => [ 44 | [ 45 | 'name' => 'stream', 46 | 'type' => 'mixed', 47 | 'defaultValue' => null 48 | ], 49 | [ 50 | 'name' => 'configuration', 51 | 'type' => '\Protobuf\Configuration', 52 | 'defaultValue' => null 53 | ] 54 | ], 55 | 'docblock' => [ 56 | 'shortDescription' => '{@inheritdoc}' 57 | ] 58 | ]); 59 | 60 | return $method; 61 | } 62 | 63 | /** 64 | * @param \Protobuf\Compiler\Entity $entity 65 | * 66 | * @return string[] 67 | */ 68 | public function generateBody(Entity $entity) 69 | { 70 | $body = []; 71 | $descriptor = $entity->getDescriptor(); 72 | $fields = $descriptor->getFieldList() ?: []; 73 | 74 | foreach ($fields as $field) { 75 | if ( ! $field->hasDefaultValue()) { 76 | continue; 77 | } 78 | 79 | $name = $field->getName(); 80 | $value = $this->getDefaultFieldValue($field); 81 | 82 | $body[] = sprintf('$this->%s = %s;', $name, $value); 83 | } 84 | 85 | $body[] = null; 86 | $body[] = 'parent::__construct($stream, $configuration);'; 87 | 88 | return $body; 89 | } 90 | 91 | /** 92 | * @param \Protobuf\Compiler\Entity $entity 93 | * 94 | * @return bool 95 | */ 96 | public function hasDefaultValue(Entity $entity) 97 | { 98 | $descriptor = $entity->getDescriptor(); 99 | $fields = $descriptor->getFieldList() ?: []; 100 | 101 | foreach ($fields as $field) { 102 | if ( ! $field->hasDefaultValue()) { 103 | continue; 104 | } 105 | 106 | return true; 107 | } 108 | 109 | return false; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Command/PluginCommand.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class PluginCommand extends Command 23 | { 24 | 25 | /** 26 | * @var \Protobuf\Stream 27 | */ 28 | private $stream; 29 | 30 | /** 31 | * @param \Protobuf\Stream $stream 32 | */ 33 | public function setStream(Stream $stream) 34 | { 35 | $this->stream = $stream; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function configure() 42 | { 43 | $this 44 | ->setName('protobuf:plugin') 45 | ->setDescription('Plugin to generate PHP classes from stdin generated by protoc'); 46 | } 47 | 48 | /** 49 | * @param \Protobuf\Stream $stream 50 | */ 51 | protected function writeStream(Stream $stream) 52 | { 53 | // OutputInterface#write messes with the content 54 | fwrite(STDOUT, $stream); 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | protected function execute(InputInterface $input, OutputInterface $output) 61 | { 62 | if ($this->stream === null) { 63 | throw new RuntimeException("Unable to read standard input."); 64 | } 65 | 66 | $compiler = $this->createCompiler($output); 67 | $response = $compiler->compile($this->stream); 68 | 69 | $this->writeStream($response); 70 | } 71 | 72 | /** 73 | * @param \Symfony\Component\Console\Output\OutputInterface $output 74 | * 75 | * @return \Protobuf\Compiler\Compiler 76 | */ 77 | protected function createCompiler(OutputInterface $output) 78 | { 79 | $logger = $this->createConsoleLogger($output); 80 | $compiler = new Compiler($logger); 81 | 82 | return $compiler; 83 | } 84 | 85 | /** 86 | * @param \Symfony\Component\Console\Output\OutputInterface $output 87 | * 88 | * @return \Symfony\Component\Console\Logger\ConsoleLogger 89 | */ 90 | protected function createConsoleLogger(OutputInterface $output) 91 | { 92 | return new ConsoleLogger( 93 | $output, 94 | [ 95 | // aways output notice, info and debug 96 | LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL, 97 | LogLevel::INFO => OutputInterface::VERBOSITY_NORMAL, 98 | LogLevel::DEBUG => OutputInterface::VERBOSITY_NORMAL 99 | ], 100 | [ 101 | // redirect messages to stderr 102 | LogLevel::WARNING => ConsoleLogger::INFO, 103 | LogLevel::NOTICE => ConsoleLogger::ERROR, 104 | LogLevel::INFO => ConsoleLogger::ERROR, 105 | LogLevel::DEBUG => ConsoleLogger::ERROR 106 | ] 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/CompilerTest.php: -------------------------------------------------------------------------------- 1 | logger = $this->getMock('Psr\Log\LoggerInterface'); 23 | } 24 | 25 | public function testGenerateSimpleMessage() 26 | { 27 | $binaryRequest = $this->getFixtureFileContent('compiler/generator-request-simple.bin'); 28 | $expectedContent = $this->getFixtureFileContent('Simple.tpl'); 29 | 30 | $compiler = new Compiler($this->logger); 31 | $binaryResponse = $compiler->compile(Stream::wrap($binaryRequest)); 32 | $response = CodeGeneratorResponse::fromStream($binaryResponse); 33 | 34 | $this->assertInstanceOf('google\protobuf\compiler\CodeGeneratorResponse', $response); 35 | $this->assertInstanceOf('Protobuf\Collection', $response->getFileList()); 36 | $this->assertCount(1, $response->getFileList()); 37 | 38 | $file = $response->getFileList()[0]; 39 | 40 | $this->assertInstanceOf('google\protobuf\compiler\CodeGeneratorResponse\File', $file); 41 | $this->assertEquals('ProtobufCompilerTest/Protos/Simple.php', $file->getName()); 42 | $this->assertEquals($expectedContent, $file->getContent()); 43 | } 44 | 45 | public function testLoadEntityClassIgnoreExistinClass() 46 | { 47 | $compiler = new Compiler($this->logger); 48 | $entity = $this->getMock(Entity::CLASS, [], [], '', false); 49 | 50 | $entity->expects($this->once()) 51 | ->method('getType') 52 | ->willReturn(Entity::TYPE_MESSAGE); 53 | 54 | $entity->expects($this->once()) 55 | ->method('getContent') 56 | ->willReturn('expects($this->once()) 59 | ->method('getNamespacedName') 60 | ->willReturn('\\Iterator'); 61 | 62 | $this->assertFalse($this->invokeMethod($compiler, 'loadEntityClass', [$entity])); 63 | } 64 | 65 | public function testLoadEntityExtensionClass() 66 | { 67 | $unique = uniqid(); 68 | $compiler = new Compiler($this->logger); 69 | $entity = $this->getMock(Entity::CLASS, [], [], '', false); 70 | $class = "ProtobufCompilerTest\CompilerTest$unique\Extension"; 71 | $code = <<expects($this->once()) 82 | ->method('getType') 83 | ->willReturn(Entity::TYPE_EXTENSION); 84 | 85 | $entity->expects($this->once()) 86 | ->method('getContent') 87 | ->willReturn($code); 88 | 89 | $entity->expects($this->once()) 90 | ->method('getNamespacedName') 91 | ->willReturn($class); 92 | 93 | $this->assertFalse(class_exists($class)); 94 | $this->assertTrue($this->invokeMethod($compiler, 'loadEntityClass', [$entity])); 95 | $this->assertTrue(class_exists($class)); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/Descriptor/MergeTest.php: -------------------------------------------------------------------------------- 1 | markTestIncompleteIfProtoClassNotFound(); 22 | 23 | parent::setUp(); 24 | } 25 | 26 | public function testSimpleMessageMerge() 27 | { 28 | $simple1 = new Simple(); 29 | $simple2 = new Simple(); 30 | $bytes = Stream::wrap("bar"); 31 | 32 | $simple1->setBool(true); 33 | $simple1->setString("foo"); 34 | $simple1->setBytes($bytes); 35 | $simple1->setFloat(12345.123); 36 | $simple1->setUint32(123456789); 37 | $simple1->setInt32(-123456789); 38 | $simple1->setFixed32(123456789); 39 | $simple1->setSint32(-123456789); 40 | $simple1->setSfixed32(-123456789); 41 | $simple1->setDouble(123456789.12345); 42 | $simple1->setInt64(-123456789123456789); 43 | $simple1->setUint64(123456789123456789); 44 | $simple1->setFixed64(123456789123456789); 45 | $simple1->setSint64(-123456789123456789); 46 | $simple1->setSfixed64(-123456789123456789); 47 | 48 | $simple2->merge($simple1); 49 | 50 | $this->assertSame(true, $simple2->getBool()); 51 | $this->assertSame("foo", $simple2->getString()); 52 | $this->assertSame($bytes, $simple2->getBytes()); 53 | $this->assertSame(12345.123, $simple2->getFloat()); 54 | $this->assertSame(123456789, $simple2->getUint32()); 55 | $this->assertSame(-123456789, $simple2->getInt32()); 56 | $this->assertSame(123456789, $simple2->getFixed32()); 57 | $this->assertSame(-123456789, $simple2->getSint32()); 58 | $this->assertSame(-123456789, $simple2->getSfixed32()); 59 | $this->assertSame(123456789.12345, $simple2->getDouble()); 60 | $this->assertSame(-123456789123456789, $simple2->getInt64()); 61 | $this->assertSame(123456789123456789, $simple2->getUint64()); 62 | $this->assertSame(123456789123456789, $simple2->getFixed64()); 63 | $this->assertSame(-123456789123456789, $simple2->getSint64()); 64 | $this->assertSame(-123456789123456789, $simple2->getSfixed64()); 65 | } 66 | 67 | public function testSimpleMessageMergeNullComparison() 68 | { 69 | $simple1 = new Simple(); 70 | $simple2 = new Simple(); 71 | $bytes = Stream::wrap("bar"); 72 | 73 | $simple1->setBool(false); 74 | $simple1->setFloat(0.0); 75 | $simple1->setUint32(0); 76 | 77 | $simple2->merge($simple1); 78 | 79 | $this->assertSame(false, $simple2->getBool()); 80 | $this->assertSame(0.0, $simple2->getFloat()); 81 | $this->assertSame(0, $simple2->getUint32()); 82 | } 83 | 84 | /** 85 | * @expectedException \InvalidArgumentException 86 | * @expectedExceptionMessage Argument 1 passed to ProtobufCompilerTest\Protos\Simple::merge must be a ProtobufCompilerTest\Protos\Simple, ProtobufCompilerTest\Protos\Person given 87 | */ 88 | public function testMergeException() 89 | { 90 | $simple = new Simple(); 91 | $person = new Person(); 92 | 93 | $simple->merge($person); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/Descriptor/ClearTest.php: -------------------------------------------------------------------------------- 1 | markTestIncompleteIfProtoClassNotFound(); 23 | 24 | parent::setUp(); 25 | } 26 | 27 | public function testSimpleMessageClear() 28 | { 29 | $simple = new Simple(); 30 | 31 | $simple->setBool(true); 32 | $simple->setString("foo"); 33 | $simple->setFloat(12345.123); 34 | $simple->setUint32(123456789); 35 | $simple->setInt32(-123456789); 36 | $simple->setFixed32(123456789); 37 | $simple->setSint32(-123456789); 38 | $simple->setSfixed32(-123456789); 39 | $simple->setDouble(123456789.12345); 40 | $simple->setInt64(-123456789123456789); 41 | $simple->setUint64(123456789123456789); 42 | $simple->setFixed64(123456789123456789); 43 | $simple->setSint64(-123456789123456789); 44 | $simple->setBytes(Stream::wrap("bar")); 45 | $simple->setSfixed64(-123456789123456789); 46 | 47 | $this->assertSame(true, $simple->getBool()); 48 | $this->assertSame("foo", $simple->getString()); 49 | $this->assertSame(12345.123, $simple->getFloat()); 50 | $this->assertSame(123456789, $simple->getUint32()); 51 | $this->assertSame(-123456789, $simple->getInt32()); 52 | $this->assertSame(123456789, $simple->getFixed32()); 53 | $this->assertSame(-123456789, $simple->getSint32()); 54 | $this->assertSame(-123456789, $simple->getSfixed32()); 55 | $this->assertSame(123456789.12345, $simple->getDouble()); 56 | $this->assertSame(-123456789123456789, $simple->getInt64()); 57 | $this->assertSame(123456789123456789, $simple->getUint64()); 58 | $this->assertSame(123456789123456789, $simple->getFixed64()); 59 | $this->assertSame(-123456789123456789, $simple->getSint64()); 60 | $this->assertSame(-123456789123456789, $simple->getSfixed64()); 61 | $this->assertInstanceOf('Protobuf\Stream', $simple->getBytes()); 62 | 63 | $simple->clear(); 64 | 65 | $this->assertNull($simple->getBool()); 66 | $this->assertNull($simple->getString()); 67 | $this->assertNull($simple->getFloat()); 68 | $this->assertNull($simple->getUint32()); 69 | $this->assertNull($simple->getInt32()); 70 | $this->assertNull($simple->getFixed32()); 71 | $this->assertNull($simple->getSint32()); 72 | $this->assertNull($simple->getSfixed32()); 73 | $this->assertNull($simple->getDouble()); 74 | $this->assertNull($simple->getInt64()); 75 | $this->assertNull($simple->getUint64()); 76 | $this->assertNull($simple->getFixed64()); 77 | $this->assertNull($simple->getSint64()); 78 | $this->assertNull($simple->getSfixed64()); 79 | $this->assertNull($simple->getBytes()); 80 | } 81 | 82 | public function testClearMessageWithDefaultValue() 83 | { 84 | $phone = new PhoneNumber(); 85 | 86 | $this->assertNull($phone->getNumber()); 87 | $this->assertSame(PhoneType::HOME(), $phone->getType()); 88 | 89 | $phone->setNumber('1231231212'); 90 | $phone->setType(PhoneType::MOBILE()); 91 | 92 | $this->assertEquals('1231231212', $phone->getNumber()); 93 | $this->assertSame(PhoneType::MOBILE(), $phone->getType()); 94 | 95 | $phone->clear(); 96 | 97 | $this->assertNull($phone->getNumber()); 98 | $this->assertSame(PhoneType::HOME(), $phone->getType()); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/Fixtures/Extension/Extension.tpl: -------------------------------------------------------------------------------- 1 | add(\ProtobufCompilerTest\Protos\Extension\Dog::animal()); 39 | $registry->add(self::habitat()); 40 | $registry->add(self::verbose()); 41 | } 42 | 43 | /** 44 | * Extension field : habitat 45 | * 46 | * @return \Protobuf\Extension 47 | */ 48 | public static function habitat() 49 | { 50 | if (self::$habitat !== null) { 51 | return self::$habitat; 52 | } 53 | 54 | $readCallback = function (\Protobuf\ReadContext $context, $wire) { 55 | $reader = $context->getReader(); 56 | $length = $context->getLength(); 57 | $stream = $context->getStream(); 58 | 59 | \Protobuf\WireFormat::assertWireType($wire, 9); 60 | 61 | $value = $reader->readString($stream); 62 | 63 | return $value; 64 | }; 65 | 66 | $writeCallback = function (\Protobuf\WriteContext $context, $value) { 67 | $stream = $context->getStream(); 68 | $writer = $context->getWriter(); 69 | $sizeContext = $context->getComputeSizeContext(); 70 | 71 | $writer->writeVarint($stream, 1602); 72 | $writer->writeString($stream, $value); 73 | }; 74 | 75 | $sizeCallback = function (\Protobuf\ComputeSizeContext $context, $value) { 76 | $calculator = $context->getSizeCalculator(); 77 | $size = 0; 78 | 79 | $size += 2; 80 | $size += $calculator->computeStringSize($value); 81 | 82 | return $size; 83 | }; 84 | 85 | return self::$habitat = new \Protobuf\Extension\ExtensionField('\\ProtobufCompilerTest\\Protos\\Extension\\Animal', 'habitat', 200, $readCallback, $writeCallback, $sizeCallback, __METHOD__); 86 | } 87 | 88 | /** 89 | * Extension field : verbose 90 | * 91 | * @return \Protobuf\Extension 92 | */ 93 | public static function verbose() 94 | { 95 | if (self::$verbose !== null) { 96 | return self::$verbose; 97 | } 98 | 99 | $readCallback = function (\Protobuf\ReadContext $context, $wire) { 100 | $reader = $context->getReader(); 101 | $length = $context->getLength(); 102 | $stream = $context->getStream(); 103 | 104 | \Protobuf\WireFormat::assertWireType($wire, 8); 105 | 106 | $value = $reader->readBool($stream); 107 | 108 | return $value; 109 | }; 110 | 111 | $writeCallback = function (\Protobuf\WriteContext $context, $value) { 112 | $stream = $context->getStream(); 113 | $writer = $context->getWriter(); 114 | $sizeContext = $context->getComputeSizeContext(); 115 | 116 | $writer->writeVarint($stream, 1608); 117 | $writer->writeBool($stream, $value); 118 | }; 119 | 120 | $sizeCallback = function (\Protobuf\ComputeSizeContext $context, $value) { 121 | $calculator = $context->getSizeCalculator(); 122 | $size = 0; 123 | 124 | $size += 2; 125 | $size += 1; 126 | 127 | return $size; 128 | }; 129 | 130 | return self::$verbose = new \Protobuf\Extension\ExtensionField('\\ProtobufCompilerTest\\Protos\\Extension\\Command', 'verbose', 201, $readCallback, $writeCallback, $sizeCallback, __METHOD__); 131 | } 132 | 133 | 134 | } 135 | 136 | -------------------------------------------------------------------------------- /src/Generator/Message/SerializedSizeGenerator.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class SerializedSizeGenerator extends BaseGenerator implements GeneratorVisitor 21 | { 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function visit(Entity $entity, GeneratorInterface $class) 26 | { 27 | $class->addMethodFromGenerator($this->generateSerializedSizeMethod($entity)); 28 | } 29 | 30 | /** 31 | * @param \Protobuf\Compiler\Entity $entity 32 | * 33 | * @return string 34 | */ 35 | protected function generateSerializedSizeMethod(Entity $entity) 36 | { 37 | $lines = $this->generateBody($entity); 38 | $body = implode(PHP_EOL, $lines); 39 | $method = MethodGenerator::fromArray([ 40 | 'name' => 'serializedSize', 41 | 'body' => $body, 42 | 'parameters' => [ 43 | [ 44 | 'name' => 'context', 45 | 'type' => '\Protobuf\ComputeSizeContext' 46 | ] 47 | ], 48 | 'docblock' => [ 49 | 'shortDescription' => "{@inheritdoc}" 50 | ] 51 | ]); 52 | 53 | return $method; 54 | } 55 | 56 | /** 57 | * @param \Protobuf\Compiler\Entity $entity 58 | * 59 | * @return string[] 60 | */ 61 | protected function generateBody(Entity $entity) 62 | { 63 | $descriptor = $entity->getDescriptor(); 64 | $fields = $descriptor->getFieldList() ?: []; 65 | $extLines = $this->generateExtensionsSerializedSize($entity); 66 | 67 | $body[] = '$calculator = $context->getSizeCalculator();'; 68 | $body[] = '$size = 0;'; 69 | $body[] = null; 70 | 71 | foreach ($fields as $field) { 72 | $lines = $this->generateFieldCondition($entity, $field); 73 | $body = array_merge($body, $lines, [null]); 74 | } 75 | 76 | $body = array_merge($body, $extLines); 77 | $body[] = null; 78 | $body[] = 'return $size;'; 79 | 80 | return $body; 81 | } 82 | 83 | /** 84 | * @param \Protobuf\Compiler\Entity $entity 85 | * 86 | * @return string[] 87 | */ 88 | protected function generateExtensionsSerializedSize(Entity $entity) 89 | { 90 | $descriptor = $entity->getDescriptor(); 91 | $extensionsField = $this->getUniqueFieldName($descriptor, 'extensions'); 92 | 93 | $body[] = 'if ($this->' . $extensionsField . ' !== null) {'; 94 | $body[] = ' $size += $this->' . $extensionsField . '->serializedSize($context);'; 95 | $body[] = '}'; 96 | 97 | return $body; 98 | } 99 | 100 | /** 101 | * @param \Protobuf\Compiler\Entity $entity 102 | * @param \google\protobuf\FieldDescriptorProto $field 103 | * 104 | * @return string[] 105 | */ 106 | protected function generateFieldCondition(Entity $entity, FieldDescriptorProto $field) 107 | { 108 | $sttm = $this->generateFieldSizeStatement($entity, $field); 109 | $lines = $this->addIndentation($sttm, 1); 110 | $name = $field->getName(); 111 | 112 | $body[] = 'if ($this->' . $name . ' !== null) {'; 113 | $body = array_merge($body, $lines); 114 | $body[] = '}'; 115 | 116 | return $body; 117 | } 118 | 119 | /** 120 | * @param \Protobuf\Compiler\Entity $entity 121 | * @param \google\protobuf\FieldDescriptorProto $field 122 | * 123 | * @return string[] 124 | */ 125 | protected function generateFieldSizeStatement(Entity $entity, FieldDescriptorProto $field) 126 | { 127 | $generator = new SerializedSizeFieldStatementGenerator($this->context); 128 | $statement = $generator->generateFieldSizeStatement($entity, $field); 129 | 130 | return $statement; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Protobuf for PHP 2 | ================ 3 | 4 | [![Build Status](https://travis-ci.org/protobuf-php/protobuf-plugin.svg?branch=master)](https://travis-ci.org/protobuf-php/protobuf-plugin) 5 | [![Coverage Status](https://coveralls.io/repos/protobuf-php/protobuf-plugin/badge.svg?branch=master&service=github)](https://coveralls.io/github/protobuf-php/protobuf-plugin?branch=master) 6 | 7 | Protobuf for PHP is an implementation of Google's Protocol Buffers for the PHP 8 | language, supporting its binary data serialization and including a `protoc` 9 | plugin to generate PHP classes from ```.proto``` files. 10 | 11 | 12 | ## Installation 13 | 14 | If you wish to compile ```.proto``` definitions to PHP, 15 | you will need to install [Google's Protocol Buffers](https://github.com/google/protobuf) from your favorite package manager or from source. 16 | This plugin currently supports protobuf 2.3.0. or later. 17 | 18 | **Note**: *Google's Protocol Buffers and ```proto``` is not a runtime requirement for [protobuf-php/protobuf](https://github.com/protobuf-php/protobuf), It is only necessary if you wish to compile your definitions to PHP using [protobuf-php/protobuf-plugin](https://github.com/protobuf-php/protobuf-plugin).* 19 | 20 | 21 | #### Installing Google's Protocol Buffers 22 | 23 | * **OSX Install** 24 | 25 | ```console 26 | $ brew install protobuf 27 | ``` 28 | 29 | * **Ubuntu** 30 | 31 | ```console 32 | $ sudo apt-get install -y protobuf 33 | ``` 34 | 35 | Make sure you have ```protoc``` available in the user's path: 36 | ```console 37 | $ protoc --version 38 | $ # libprotoc 2.6.1 39 | ``` 40 | 41 | **Note**: *For more information on how to install/compile Google's Protocol Buffers see : https://github.com/google/protobuf* 42 | 43 | 44 | #### Composer install 45 | 46 | To install the PHP plugin run the following `composer` commands: 47 | 48 | ```console 49 | $ composer require "protobuf-php/protobuf-plugin" 50 | ``` 51 | 52 | #### Defining Your Protocol Format 53 | 54 | To create your address book application, you'll need to start with a ```.proto``` file. The definitions in a ```.proto``` file are simple: you add a message for each data structure you want to serialize, then specify a name and a type for each field in the message. Here is the ```.proto``` file that defines your messages, ```addressbook.proto```. 55 | 56 | ```proto 57 | package tutorial; 58 | import "php.proto"; 59 | option (php.package) = "Tutorial.AddressBookProtos"; 60 | 61 | message Person { 62 | required string name = 1; 63 | required int32 id = 2; 64 | optional string email = 3; 65 | 66 | enum PhoneType { 67 | MOBILE = 0; 68 | HOME = 1; 69 | WORK = 2; 70 | } 71 | 72 | message PhoneNumber { 73 | required string number = 1; 74 | optional PhoneType type = 2 [default = HOME]; 75 | } 76 | 77 | repeated PhoneNumber phone = 4; 78 | } 79 | 80 | message AddressBook { 81 | repeated Person person = 1; 82 | } 83 | ``` 84 | 85 | As you can see, the syntax is similar to C++ or Java. Let's go through each part of the file and see what it does. 86 | The ```.proto``` file starts with a package declaration, which helps to prevent naming conflicts between different projects. 87 | In PHP, the package name is used as the PHP namespace unless you have explicitly specified a ```(php.package)```, as we have here. 88 | Even if you do provide a ```(php.package)```, you should still define a normal package as well to avoid name collisions in the Protocol Buffers name space as well as in non PHP languages. 89 | 90 | You'll find a complete guide to writing ```.proto``` files – including all the possible field types – in the [Protocol Buffer Language Guide](https://developers.google.com/protocol-buffers/docs/proto). Don't go looking for facilities similar to class inheritance, though – protocol buffers don't do that. 91 | 92 | 93 | #### Compiling Your Protocol Buffers 94 | 95 | Now that you have a ```.proto```, the next thing you need to do is generate the classes you'll need to read and write ```AddressBook``` (and hence ```Person``` and ```PhoneNumber```) messages. To do this, you need to run the protocol buffer plugin on your ```.proto```. 96 | 97 | In this case: 98 | 99 | ```console 100 | php ./vendor/bin/protobuf --include-descriptors -i . -o ./src/ ./addressbook.proto 101 | ``` 102 | 103 | This generates the following PHP classes in your specified destination directory 104 | 105 | ```console 106 | src/ 107 | └── Tutorial 108 | └── AddressBookProtos 109 | ├── AddressBook.php 110 | ├── Person 111 | │   ├── PhoneNumber.php 112 | │   └── PhoneType.php 113 | └── Person.php 114 | ``` 115 | 116 | **Note**: *For more information on how to use the generated code see : [protobuf-php/protobuf](https://github.com/protobuf-php/protobuf)* 117 | -------------------------------------------------------------------------------- /src/Generator/Message/SerializedSizeFieldStatementGenerator.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class SerializedSizeFieldStatementGenerator extends BaseGenerator 21 | { 22 | /** 23 | * @var string 24 | */ 25 | protected $targetVar; 26 | 27 | /** 28 | * @param string $variable 29 | */ 30 | public function setTargetVar($variable) 31 | { 32 | $this->targetVar = $variable; 33 | } 34 | 35 | /** 36 | * @param \Protobuf\Compiler\Entity $entity 37 | * @param \google\protobuf\FieldDescriptorProto $field 38 | * 39 | * @return string[] 40 | */ 41 | public function generateFieldSizeStatement(Entity $entity, FieldDescriptorProto $field) 42 | { 43 | $body = []; 44 | $name = $field->getName(); 45 | $type = $field->getType(); 46 | $rule = $field->getLabel(); 47 | $tag = $field->getNumber(); 48 | $options = $field->getOptions(); 49 | $variable = $this->targetVar ?: '$this->' . $name; 50 | $isPack = $options ? $options->getPacked() : false; 51 | 52 | $wire = $isPack ? WireFormat::WIRE_LENGTH : WireFormat::getWireType($type->value(), null); 53 | $key = WireFormat::getFieldKey($tag, $wire); 54 | $keySize = $this->getSizeCalculator()->computeVarintSize($key); 55 | 56 | if ($rule === Label::LABEL_REPEATED() && $isPack) { 57 | $itemValSttm = ($type === Type::TYPE_ENUM()) 58 | ? '$val->value()' 59 | : '$val'; 60 | 61 | $body[] = '$innerSize = 0;'; 62 | $body[] = null; 63 | $body[] = 'foreach (' . $variable . ' as $val) {'; 64 | $body[] = ' $innerSize += ' . $this->generateValueSizeStatement($type->value(), $itemValSttm) . ';'; 65 | $body[] = '}'; 66 | $body[] = null; 67 | $body[] = '$size += ' . $keySize . ';'; 68 | $body[] = '$size += $innerSize;'; 69 | $body[] = '$size += $calculator->computeVarintSize($innerSize);'; 70 | 71 | return $body; 72 | } 73 | 74 | if ($type === Type::TYPE_MESSAGE() && $rule === Label::LABEL_REPEATED()) { 75 | $body[] = 'foreach (' . $variable . ' as $val) {'; 76 | $body[] = ' $innerSize = $val->serializedSize($context);'; 77 | $body[] = null; 78 | $body[] = ' $size += ' . $keySize . ';'; 79 | $body[] = ' $size += $innerSize;'; 80 | $body[] = ' $size += $calculator->computeVarintSize($innerSize);'; 81 | $body[] = '}'; 82 | 83 | return $body; 84 | } 85 | 86 | if ($type === Type::TYPE_ENUM() && $rule === Label::LABEL_REPEATED()) { 87 | $body[] = 'foreach (' . $variable . ' as $val) {'; 88 | $body[] = ' $size += ' . $keySize . ';'; 89 | $body[] = ' $size += ' . $this->generateValueSizeStatement($type->value(), '$val->value()') . ';'; 90 | $body[] = '}'; 91 | 92 | return $body; 93 | } 94 | 95 | if ($rule === Label::LABEL_REPEATED()) { 96 | $body[] = 'foreach (' . $variable . ' as $val) {'; 97 | $body[] = ' $size += ' . $keySize . ';'; 98 | $body[] = ' $size += ' . $this->generateValueSizeStatement($type->value(), '$val') . ';'; 99 | $body[] = '}'; 100 | 101 | return $body; 102 | } 103 | 104 | if ($type === Type::TYPE_ENUM()) { 105 | $body[] = '$size += ' . $keySize . ';'; 106 | $body[] = '$size += ' . $this->generateValueSizeStatement($type->value(), $variable . '->value()') . ';'; 107 | 108 | return $body; 109 | } 110 | 111 | if ($type !== Type::TYPE_MESSAGE()) { 112 | $body[] = '$size += ' . $keySize . ';'; 113 | $body[] = '$size += ' . $this->generateValueSizeStatement($type->value(), $variable) . ';'; 114 | 115 | return $body; 116 | } 117 | 118 | $body[] = '$innerSize = ' . $variable . '->serializedSize($context);'; 119 | $body[] = null; 120 | $body[] = '$size += ' . $keySize . ';'; 121 | $body[] = '$size += $innerSize;'; 122 | $body[] = '$size += $calculator->computeVarintSize($innerSize);'; 123 | 124 | return $body; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Command/GenerateCommand.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class GenerateCommand extends Command 19 | { 20 | /** 21 | * plugin command 22 | * 23 | * @var string 24 | */ 25 | protected $plugin; 26 | 27 | /** 28 | * Constructor. 29 | * 30 | * @param string $plugin The plugin command 31 | */ 32 | public function __construct($plugin) 33 | { 34 | $this->plugin = $plugin; 35 | 36 | parent::__construct('protobuf:generate'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function configure() 43 | { 44 | $this 45 | ->setName('protobuf:generate') 46 | ->setDescription('Executes protoc to generate PHP classes') 47 | ->addArgument('protos', InputArgument::IS_ARRAY|InputArgument::REQUIRED, 'proto files') 48 | ->addOption('generate-imported', null, InputOption::VALUE_NONE, 'Generate imported proto files') 49 | ->addOption('protoc', null, InputOption::VALUE_REQUIRED, 'protoc compiler executable path', 'protoc') 50 | ->addOption('out', 'o', InputOption::VALUE_REQUIRED, 'destination directory for generated files', './') 51 | ->addOption('psr4', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_REQUIRED, 'psr-4 base directory') 52 | ->addOption('include', 'i', InputOption::VALUE_IS_ARRAY|InputOption::VALUE_REQUIRED, 'define an include path') 53 | ->addOption('include-descriptors', null, InputOption::VALUE_NONE, 'add google-protobuf-proto descriptors to include path'); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | protected function execute(InputInterface $input, OutputInterface $output) 60 | { 61 | $args = []; 62 | $out = $input->getOption('out'); 63 | $psr4 = $input->getOption('psr4'); 64 | $protoc = $input->getOption('protoc'); 65 | $protos = $input->getArgument('protos'); 66 | $include = $input->getOption('include') ?: []; 67 | $builder = $this->createProcessBuilder($this->plugin, $protoc); 68 | 69 | if ($output->isVerbose()) { 70 | $args['verbose'] = 1; 71 | } 72 | 73 | if ($input->getOption('generate-imported')) { 74 | $args['generate-imported'] = 1; 75 | } 76 | 77 | if ($psr4) { 78 | $args['psr4'] = $psr4; 79 | } 80 | 81 | if ($input->getOption('include-descriptors')) { 82 | $builder->setIncludeDescriptors(true); 83 | $builder->setDescriptorPaths([ 84 | __DIR__ . '/../../../google-protobuf-proto/src', 85 | __DIR__ . '/../../vendor/protobuf-php/google-protobuf-proto/src' 86 | ]); 87 | } 88 | 89 | $builder->assertVersion(); 90 | 91 | $process = $builder->createProcess($out, $protos, $include, $args); 92 | $command = $process->getCommandLine(); 93 | 94 | if ($output->isVerbose()) { 95 | $output->writeln("Generating protos with protoc -- $command"); 96 | } 97 | 98 | // Run command 99 | $process->run(function ($type, $buffer) use ($output) { 100 | if ( ! $output->isVerbose() || ! $buffer) { 101 | return; 102 | } 103 | 104 | $output->writeln($buffer); 105 | }); 106 | 107 | $return = $process->getExitCode(); 108 | $result = $process->getOutput(); 109 | 110 | if ($return === 0) { 111 | $output->writeln("PHP classes successfully generate."); 112 | 113 | return $return; 114 | } 115 | 116 | $output->writeln('protoc exited with an error ('.$return.') when executed with: '); 117 | $output->writeln(''); 118 | $output->writeln(' ' . $command); 119 | $output->writeln(''); 120 | $output->writeln($result); 121 | $output->writeln(''); 122 | $output->writeln($process->getErrorOutput()); 123 | $output->writeln(''); 124 | 125 | return $return; 126 | } 127 | 128 | /** 129 | * @param string $plugin 130 | * @param string $protoc 131 | * 132 | * @return \Protobuf\Compiler\Protoc\ProcessBuilder 133 | */ 134 | protected function createProcessBuilder($plugin, $protoc) 135 | { 136 | return new ProcessBuilder($plugin, $protoc); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /tests/Fixtures/Options/SimpleMessage.tpl: -------------------------------------------------------------------------------- 1 | extensions !== null) { 33 | return $this->extensions; 34 | } 35 | 36 | return $this->extensions = new \Protobuf\Extension\ExtensionFieldMap(__CLASS__); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function unknownFieldSet() 43 | { 44 | return $this->unknownFieldSet; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public static function fromStream($stream, \Protobuf\Configuration $configuration = null) 51 | { 52 | return new self($stream, $configuration); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public static function fromArray(array $values) 59 | { 60 | $message = new self(); 61 | $values = array_merge([ 62 | ], $values); 63 | 64 | return $message; 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public static function descriptor() 71 | { 72 | return \google\protobuf\DescriptorProto::fromArray([ 73 | 'name' => 'SimpleMessage', 74 | ]); 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function toStream(\Protobuf\Configuration $configuration = null) 81 | { 82 | $config = $configuration ?: \Protobuf\Configuration::getInstance(); 83 | $context = $config->createWriteContext(); 84 | $stream = $context->getStream(); 85 | 86 | $this->writeTo($context); 87 | $stream->seek(0); 88 | 89 | return $stream; 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function writeTo(\Protobuf\WriteContext $context) 96 | { 97 | $stream = $context->getStream(); 98 | $writer = $context->getWriter(); 99 | $sizeContext = $context->getComputeSizeContext(); 100 | 101 | if ($this->extensions !== null) { 102 | $this->extensions->writeTo($context); 103 | } 104 | 105 | return $stream; 106 | } 107 | 108 | /** 109 | * {@inheritdoc} 110 | */ 111 | public function readFrom(\Protobuf\ReadContext $context) 112 | { 113 | $reader = $context->getReader(); 114 | $length = $context->getLength(); 115 | $stream = $context->getStream(); 116 | 117 | $limit = ($length !== null) 118 | ? ($stream->tell() + $length) 119 | : null; 120 | 121 | while ($limit === null || $stream->tell() < $limit) { 122 | 123 | if ($stream->eof()) { 124 | break; 125 | } 126 | 127 | $key = $reader->readVarint($stream); 128 | $wire = \Protobuf\WireFormat::getTagWireType($key); 129 | $tag = \Protobuf\WireFormat::getTagFieldNumber($key); 130 | 131 | if ($stream->eof()) { 132 | break; 133 | } 134 | 135 | $extensions = $context->getExtensionRegistry(); 136 | $extension = $extensions ? $extensions->findByNumber(__CLASS__, $tag) : null; 137 | 138 | if ($extension !== null) { 139 | $this->extensions()->add($extension, $extension->readFrom($context, $wire)); 140 | 141 | continue; 142 | } 143 | 144 | if ($this->unknownFieldSet === null) { 145 | $this->unknownFieldSet = new \Protobuf\UnknownFieldSet(); 146 | } 147 | 148 | $data = $reader->readUnknown($stream, $wire); 149 | $unknown = new \Protobuf\Unknown($tag, $wire, $data); 150 | 151 | $this->unknownFieldSet->add($unknown); 152 | 153 | } 154 | } 155 | 156 | /** 157 | * {@inheritdoc} 158 | */ 159 | public function serializedSize(\Protobuf\ComputeSizeContext $context) 160 | { 161 | $calculator = $context->getSizeCalculator(); 162 | $size = 0; 163 | 164 | if ($this->extensions !== null) { 165 | $size += $this->extensions->serializedSize($context); 166 | } 167 | 168 | return $size; 169 | } 170 | 171 | /** 172 | * {@inheritdoc} 173 | */ 174 | public function clear() 175 | { 176 | } 177 | 178 | /** 179 | * {@inheritdoc} 180 | */ 181 | public function merge(\Protobuf\Message $message) 182 | { 183 | if ( ! $message instanceof \ProtobufCompilerTest\Protos\Options\SimpleMessage) { 184 | throw new \InvalidArgumentException(sprintf('Argument 1 passed to %s must be a %s, %s given', __METHOD__, __CLASS__, get_class($message))); 185 | } 186 | } 187 | 188 | 189 | } 190 | 191 | -------------------------------------------------------------------------------- /src/Generator/ServiceGenerator.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class ServiceGenerator extends BaseGenerator implements EntityVisitor 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function visit(Entity $entity) 25 | { 26 | $name = $entity->getName(); 27 | $namespace = $entity->getNamespace(); 28 | $shortDescription = 'Protobuf service : ' . $entity->getClass(); 29 | $class = InterfaceGenerator::fromArray([ 30 | 'name' => $name, 31 | 'namespacename' => $namespace, 32 | 'methods' => $this->generateMethods($entity), 33 | 'docblock' => [ 34 | 'shortDescription' => $shortDescription, 35 | ] 36 | ]); 37 | 38 | $entity->setContent($this->generateFileContent($class, $entity)); 39 | } 40 | 41 | /** 42 | * @param \Protobuf\Compiler\Entity $entity 43 | * 44 | * @return string[] 45 | */ 46 | protected function generateMethods(Entity $entity) 47 | { 48 | $result = []; 49 | $descriptor = $entity->getDescriptor(); 50 | $methods = $descriptor->getMethodList() ?: []; 51 | 52 | foreach ($methods as $method) { 53 | $result[] = $this->generateMethod($entity, $method); 54 | } 55 | 56 | return $result; 57 | } 58 | 59 | /** 60 | * @param \Protobuf\Compiler\Entity $entity 61 | * @param \google\protobuf\MethodDescriptorProto $method 62 | * 63 | * @return string 64 | */ 65 | protected function generateMethod(Entity $entity, MethodDescriptorProto $method) 66 | { 67 | $inputClass = $this->getMethodInputTypeHint($method); 68 | $inputDoc = $this->getMethodInputDocblock($method); 69 | $outputDoc = $this->getMethodOutputDocblock($method); 70 | $methodName = $this->getCamelizedValue($method->getName()); 71 | $method = MethodGenerator::fromArray([ 72 | 'name' => $methodName, 73 | 'parameters' => [ 74 | [ 75 | 'name' => 'input', 76 | 'type' => $inputClass 77 | ] 78 | ], 79 | 'docblock' => [ 80 | 'tags' => [ 81 | [ 82 | 'name' => 'param', 83 | 'description' => $inputDoc . ' $input' 84 | ], 85 | [ 86 | 'name' => 'return', 87 | 'description' => $outputDoc 88 | ] 89 | ] 90 | ] 91 | ]); 92 | 93 | return $method; 94 | } 95 | 96 | /** 97 | * @param \google\protobuf\MethodDescriptorProto $method 98 | * 99 | * @return string 100 | */ 101 | protected function getMethodInputTypeHint(MethodDescriptorProto $method) 102 | { 103 | $refType = $method->getInputType(); 104 | $refEntity = $this->getEntity($refType); 105 | 106 | if ($method->getClientStreaming()) { 107 | return '\Iterator'; 108 | } 109 | 110 | return $refEntity->getNamespacedName(); 111 | } 112 | 113 | /** 114 | * @param \google\protobuf\MethodDescriptorProto $method 115 | * 116 | * @return string 117 | */ 118 | protected function getMethodOutputTypeHint(MethodDescriptorProto $method) 119 | { 120 | $refType = $method->getOutputType(); 121 | $refEntity = $this->getEntity($refType); 122 | 123 | if ($method->getServerStreaming()) { 124 | return '\Iterator'; 125 | } 126 | 127 | return $refEntity->getNamespacedName(); 128 | } 129 | 130 | /** 131 | * @param \google\protobuf\MethodDescriptorProto $method 132 | * 133 | * @return string 134 | */ 135 | protected function getMethodInputDocblock(MethodDescriptorProto $method) 136 | { 137 | $refType = $method->getInputType(); 138 | $refEntity = $this->getEntity($refType); 139 | $refClass = $this->getMethodInputTypeHint($method); 140 | 141 | if ($method->getClientStreaming()) { 142 | return sprintf('\Iterator<%s>', $refEntity->getNamespacedName()); 143 | } 144 | 145 | return $refClass; 146 | } 147 | 148 | /** 149 | * @param \google\protobuf\MethodDescriptorProto $method 150 | * 151 | * @return string 152 | */ 153 | protected function getMethodOutputDocblock(MethodDescriptorProto $method) 154 | { 155 | $refType = $method->getOutputType(); 156 | $refEntity = $this->getEntity($refType); 157 | $refClass = $this->getMethodOutputTypeHint($method); 158 | 159 | if ($method->getServerStreaming()) { 160 | return sprintf('\Iterator<%s>', $refEntity->getNamespacedName()); 161 | } 162 | 163 | return $refClass; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /tests/Command/GenerateCommandTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('Protobuf\Compiler\Protoc\ProcessBuilder') 16 | ->disableOriginalConstructor() 17 | ->getMock(); 18 | 19 | $process = $this->getMockBuilder('Symfony\Component\Process\Process') 20 | ->disableOriginalConstructor() 21 | ->getMock(); 22 | 23 | $mock = $this->getMockBuilder(GenerateCommand::CLASS) 24 | ->setMethods(['createProcessBuilder']) 25 | ->setConstructorArgs(['./bin/protobuf']) 26 | ->getMock(); 27 | 28 | $mock->expects($this->once()) 29 | ->method('createProcessBuilder') 30 | ->willReturn($builder); 31 | 32 | $builder->expects($this->once()) 33 | ->method('createProcess') 34 | ->willReturn($process) 35 | ->with( 36 | $this->equalTo('./src'), 37 | $this->equalTo(['./file.proto']), 38 | $this->equalTo(['path-to-include']), 39 | $this->equalTo([ 40 | 'generate-imported' => 1, 41 | 'verbose' => 1, 42 | 'psr4' => ['ProtobufTest\Protos'] 43 | ]) 44 | ); 45 | 46 | $process->expects($this->once()) 47 | ->method('run'); 48 | 49 | $process->expects($this->once()) 50 | ->method('getExitCode') 51 | ->willReturn(0); 52 | 53 | $process->expects($this->once()) 54 | ->method('getOutput') 55 | ->willReturn('OK'); 56 | 57 | $process->expects($this->once()) 58 | ->method('getCommandLine') 59 | ->willReturn('"protoc command"'); 60 | 61 | $application = new Application(); 62 | 63 | $application->add($mock); 64 | 65 | $command = $application->find('protobuf:generate'); 66 | $commandTester = new CommandTester($command); 67 | 68 | $commandTester->execute([ 69 | '--generate-imported' => true, 70 | '--out' => './src', 71 | 'protos' => ['./file.proto'], 72 | '--protoc' => '/usr/bin/protoc', 73 | '--include' => ['path-to-include'], 74 | '--psr4' => ['ProtobufTest\Protos'], 75 | ], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]); 76 | 77 | $this->assertContains('Generating protos with protoc -- "protoc command"', $commandTester->getDisplay()); 78 | $this->assertContains('PHP classes successfully generate.', $commandTester->getDisplay()); 79 | } 80 | 81 | public function testExecuteFail() 82 | { 83 | $builder = $this->getMockBuilder('Protobuf\Compiler\Protoc\ProcessBuilder') 84 | ->disableOriginalConstructor() 85 | ->getMock(); 86 | 87 | $process = $this->getMockBuilder('Symfony\Component\Process\Process') 88 | ->disableOriginalConstructor() 89 | ->getMock(); 90 | 91 | $mock = $this->getMockBuilder(GenerateCommand::CLASS) 92 | ->setMethods(['createProcessBuilder']) 93 | ->setConstructorArgs(['./bin/protobuf-plugin']) 94 | ->getMock(); 95 | 96 | $mock->expects($this->once()) 97 | ->method('createProcessBuilder') 98 | ->willReturn($builder); 99 | 100 | $builder->expects($this->once()) 101 | ->method('createProcess') 102 | ->willReturn($process) 103 | ->with( 104 | $this->equalTo('./'), 105 | $this->equalTo(['./file.proto']), 106 | $this->equalTo([]), 107 | $this->equalTo([]) 108 | ); 109 | 110 | $process->expects($this->once()) 111 | ->method('run'); 112 | 113 | $process->expects($this->once()) 114 | ->method('getExitCode') 115 | ->willReturn(255); 116 | 117 | $process->expects($this->once()) 118 | ->method('getOutput') 119 | ->willReturn('Fail'); 120 | 121 | $process->expects($this->once()) 122 | ->method('getCommandLine') 123 | ->willReturn('"protoc command"'); 124 | 125 | $application = new Application(); 126 | 127 | $application->add($mock); 128 | 129 | $command = $application->find('protobuf:generate'); 130 | $commandTester = new CommandTester($command); 131 | 132 | $commandTester->execute([ 133 | 'protos' => ['./file.proto'] 134 | ]); 135 | 136 | $this->assertContains('protoc exited with an error (255) when executed', $commandTester->getDisplay()); 137 | } 138 | 139 | public function testCreateProcessBuilder() 140 | { 141 | $command = new GenerateCommand('./bin/protobuf-plugin'); 142 | $builder = $this->invokeMethod($command, 'createProcessBuilder', ['./bin/protobuf-plugin', '2.3.1']); 143 | 144 | $this->assertInstanceOf('Protobuf\Compiler\Protoc\ProcessBuilder', $builder); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Generator/Message/WriteToGenerator.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class WriteToGenerator extends BaseGenerator implements GeneratorVisitor 24 | { 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function visit(Entity $entity, GeneratorInterface $class) 29 | { 30 | $class->addMethodFromGenerator($this->generateWriteToMethod($entity)); 31 | } 32 | 33 | /** 34 | * @param \Protobuf\Compiler\Entity $entity 35 | * 36 | * @return \Zend\Code\Generator\GeneratorInterface 37 | */ 38 | protected function generateWriteToMethod(Entity $entity) 39 | { 40 | $lines = $this->generateBody($entity); 41 | $body = implode(PHP_EOL, $lines); 42 | $method = MethodGenerator::fromArray([ 43 | 'name' => 'writeTo', 44 | 'body' => $body, 45 | 'parameters' => [ 46 | [ 47 | 'name' => 'context', 48 | 'type' => '\Protobuf\WriteContext', 49 | ] 50 | ], 51 | 'docblock' => [ 52 | 'shortDescription' => "{@inheritdoc}" 53 | ] 54 | ]); 55 | 56 | return $method; 57 | } 58 | 59 | /** 60 | * @param \Protobuf\Compiler\Entity $entity 61 | * 62 | * @return string[] 63 | */ 64 | protected function generateBody(Entity $entity) 65 | { 66 | $descriptor = $entity->getDescriptor(); 67 | $fields = $descriptor->getFieldList() ?: []; 68 | 69 | $body[] = '$stream = $context->getStream();'; 70 | $body[] = '$writer = $context->getWriter();'; 71 | $body[] = '$sizeContext = $context->getComputeSizeContext();'; 72 | $body[] = null; 73 | 74 | foreach ($fields as $field) { 75 | $lines = $this->generateRequiredFieldException($entity, $field); 76 | $body = array_merge($body, $lines); 77 | } 78 | 79 | foreach ($fields as $field) { 80 | $lines = $this->generateFieldCondition($entity, $field); 81 | $body = array_merge($body, $lines, [null]); 82 | } 83 | 84 | $extensionsField = $this->getUniqueFieldName($descriptor, 'extensions'); 85 | $extensionsVar = '$this->' . $extensionsField; 86 | 87 | $body[] = 'if (' . $extensionsVar . ' !== null) {'; 88 | $body[] = ' ' . $extensionsVar . '->writeTo($context);'; 89 | $body[] = '}'; 90 | $body[] = null; 91 | $body[] = 'return $stream;'; 92 | 93 | return $body; 94 | } 95 | 96 | /** 97 | * @param \Protobuf\Compiler\Entity $entity 98 | * @param \google\protobuf\FieldDescriptorProto $field 99 | * 100 | * @return string[] 101 | */ 102 | protected function generateRequiredFieldException(Entity $entity, FieldDescriptorProto $field) 103 | { 104 | $name = $field->getName(); 105 | $label = $field->getLabel(); 106 | $tag = $field->getNumber(); 107 | $isRequired = $label === Label::LABEL_REQUIRED(); 108 | 109 | if ( ! $isRequired) { 110 | return []; 111 | } 112 | 113 | $class = $entity->getNamespacedName(); 114 | $format = 'Field "%s#%s" (tag %s) is required but has no value.'; 115 | $message = var_export(sprintf($format, $class, $name, $tag), true); 116 | 117 | $body[] = 'if ($this->' . $name . ' === null) {'; 118 | $body[] = ' throw new \UnexpectedValueException(' . $message . ');'; 119 | $body[] = '}'; 120 | $body[] = null; 121 | 122 | return $body; 123 | } 124 | 125 | /** 126 | * @param \Protobuf\Compiler\Entity $entity 127 | * @param \google\protobuf\FieldDescriptorProto $field 128 | * 129 | * @return string[] 130 | */ 131 | protected function generateFieldCondition(Entity $entity, FieldDescriptorProto $field) 132 | { 133 | $body = []; 134 | $fieldName = $field->getName(); 135 | $format = 'if ($this->%s !== null) {'; 136 | $sttm = $this->generateFieldWriteStatement($entity, $field); 137 | $lines = $this->addIndentation($sttm, 1); 138 | 139 | $body[] = sprintf($format, $fieldName); 140 | $body = array_merge($body, $lines); 141 | $body[] = '}'; 142 | 143 | return $body; 144 | } 145 | 146 | /** 147 | * @param \Protobuf\Compiler\Entity $entity 148 | * @param google\protobuf\FieldDescriptorProto $field 149 | * 150 | * @return string[] 151 | */ 152 | protected function generateFieldWriteStatement(Entity $entity, FieldDescriptorProto $field) 153 | { 154 | $generator = new WriteFieldStatementGenerator($this->context); 155 | $statement = $generator->generateFieldWriteStatement($entity, $field); 156 | 157 | return $statement; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /tests/Descriptor/SerializedSizeTest.php: -------------------------------------------------------------------------------- 1 | markTestIncompleteIfProtoClassNotFound(); 24 | 25 | parent::setUp(); 26 | } 27 | 28 | public function testSimpleSerializedSize() 29 | { 30 | $simple = new Simple(); 31 | $calculator = $this->getMock(SizeCalculator::CLASS, [], [], '', false); 32 | $context = $this->getMock(ComputeSizeContext::CLASS, [], [], '', false); 33 | 34 | $simple->setBool(true); 35 | $simple->setBytes("bar"); 36 | $simple->setString("foo"); 37 | $simple->setFloat(12345.123); 38 | $simple->setUint32(123456789); 39 | $simple->setInt32(-123456789); 40 | $simple->setFixed32(123456789); 41 | $simple->setSint32(-123456789); 42 | $simple->setSfixed32(-123456789); 43 | $simple->setDouble(123456789.12345); 44 | $simple->setInt64(-123456789123456789); 45 | $simple->setUint64(123456789123456789); 46 | $simple->setFixed64(123456789123456789); 47 | $simple->setSint64(-123456789123456789); 48 | $simple->setSfixed64(-123456789123456789); 49 | 50 | $context->expects($this->once()) 51 | ->method('getSizeCalculator') 52 | ->willReturn($calculator); 53 | 54 | $calculator->expects($this->exactly(4)) 55 | ->method('computeVarintSize') 56 | ->will($this->returnValueMap([ 57 | [-123456789123456789, 10], 58 | [123456789123456789, 9], 59 | [-123456789, 10], 60 | [123456789, 4], 61 | ])); 62 | 63 | $calculator->expects($this->once()) 64 | ->method('computeStringSize') 65 | ->with('foo') 66 | ->willReturn(3); 67 | 68 | $calculator->expects($this->once()) 69 | ->method('computeByteStreamSize') 70 | ->with('bar') 71 | ->willReturn(3); 72 | 73 | $calculator->expects($this->once()) 74 | ->method('computeZigzag32Size') 75 | ->with(-123456789) 76 | ->willReturn(4); 77 | 78 | $calculator->expects($this->once()) 79 | ->method('computeZigzag64Size') 80 | ->with(-123456789123456789) 81 | ->willReturn(9); 82 | 83 | $simple->serializedSize($context); 84 | } 85 | 86 | public function testRepeatedStringSerializedSize() 87 | { 88 | $repeated = new Repeated(); 89 | $calculator = $this->getMock(SizeCalculator::CLASS, [], [], '', false); 90 | $context = $this->getMock(ComputeSizeContext::CLASS, [], [], '', false); 91 | 92 | $repeated->addString('one'); 93 | $repeated->addString('two'); 94 | $repeated->addString('three'); 95 | 96 | $context->expects($this->once()) 97 | ->method('getSizeCalculator') 98 | ->willReturn($calculator); 99 | 100 | $calculator->expects($this->exactly(3)) 101 | ->method('computeStringSize') 102 | ->will($this->returnValueMap([ 103 | ['one', 4], 104 | ['two', 4], 105 | ['three', 6] 106 | ])); 107 | 108 | $repeated->serializedSize($context); 109 | } 110 | 111 | public function testRepeatedInt32SerializedSize() 112 | { 113 | $repeated = new Repeated(); 114 | $calculator = $this->getMock(SizeCalculator::CLASS, [], [], '', false); 115 | $context = $this->getMock(ComputeSizeContext::CLASS, [], [], '', false); 116 | 117 | $repeated->addInt(1); 118 | $repeated->addInt(2); 119 | $repeated->addInt(2); 120 | 121 | $context->expects($this->once()) 122 | ->method('getSizeCalculator') 123 | ->willReturn($calculator); 124 | 125 | $calculator->expects($this->exactly(3)) 126 | ->method('computeVarintSize') 127 | ->will($this->returnValueMap([ 128 | [1, 1], 129 | [2, 1], 130 | [3, 1] 131 | ])); 132 | 133 | $repeated->serializedSize($context); 134 | } 135 | 136 | public function testAddressBookWithExtensionsSerializedSize() 137 | { 138 | $message = new AddressBook(); 139 | $person = $this->getMock(Person::CLASS); 140 | $calculator = $this->getMock(SizeCalculator::CLASS, [], [], '', false); 141 | $extensions = $this->getMock(ExtensionFieldMap::CLASS, [], [], '', false); 142 | $context = $this->getMock(ComputeSizeContext::CLASS, [], [], '', false); 143 | $personSize = 2; 144 | $extSize = 4; 145 | 146 | $message->addPerson($person); 147 | $this->setPropertyValue($message, 'extensions', $extensions); 148 | 149 | $context->expects($this->once()) 150 | ->method('getSizeCalculator') 151 | ->willReturn($calculator); 152 | 153 | $calculator->expects($this->once()) 154 | ->method('computeVarintSize') 155 | ->with($this->equalTo($personSize)) 156 | ->willReturn(1); 157 | 158 | $extensions->expects($this->once()) 159 | ->method('serializedSize') 160 | ->willReturn($extSize) 161 | ->with($context); 162 | 163 | $person->expects($this->once()) 164 | ->method('serializedSize') 165 | ->willReturn($personSize) 166 | ->with($context); 167 | 168 | $message->serializedSize($context); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Generator/Message/DescriptorGenerator.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class DescriptorGenerator extends BaseGenerator implements GeneratorVisitor 21 | { 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function visit(Entity $entity, GeneratorInterface $class) 26 | { 27 | $class->addMethodFromGenerator($this->generateMethod($entity)); 28 | } 29 | 30 | 31 | /** 32 | * @param \Protobuf\Compiler\Entity $entity 33 | * 34 | * @return string[] 35 | */ 36 | protected function generateMethod(Entity $entity) 37 | { 38 | $lines = $this->generateBody($entity); 39 | $body = implode(PHP_EOL, $lines); 40 | $method = MethodGenerator::fromArray([ 41 | 'name' => 'descriptor', 42 | 'body' => $body, 43 | 'static' => true, 44 | 'docblock' => [ 45 | 'shortDescription' => "{@inheritdoc}" 46 | ] 47 | ]); 48 | 49 | return $method; 50 | } 51 | 52 | /** 53 | * @param \Protobuf\Compiler\Entity $entity 54 | * 55 | * @return string[] 56 | */ 57 | protected function generateBody(Entity $entity) 58 | { 59 | $lines = []; 60 | $fields = []; 61 | $extensions = []; 62 | $descriptor = $entity->getDescriptor(); 63 | 64 | if ($descriptor->hasOptions()) { 65 | $lines = $this->createOptionsBody($entity); 66 | $lines[] = null; 67 | } 68 | 69 | foreach (($descriptor->getFieldList() ?: []) as $field) { 70 | $values = $this->generateFieldBody($field); 71 | $fields = array_merge($fields, $this->addIndentation($values, 2)); 72 | } 73 | 74 | foreach (($descriptor->getExtensionList() ?: []) as $field) { 75 | $values = $this->generateFieldBody($field); 76 | $extensions = array_merge($extensions, $this->addIndentation($values, 2)); 77 | } 78 | 79 | $lines[] = "return \google\protobuf\DescriptorProto::fromArray(["; 80 | $lines[] = " 'name' => " . var_export($descriptor->getName(), true) . ","; 81 | 82 | if ( ! empty($fields)) { 83 | $lines[] = " 'field' => ["; 84 | $lines = array_merge($lines, $fields); 85 | $lines[] = " ],"; 86 | } 87 | 88 | if ( ! empty($extensions)) { 89 | $lines[] = " 'extension' => ["; 90 | $lines = array_merge($lines, $extensions); 91 | $lines[] = " ],"; 92 | } 93 | 94 | if ($descriptor->hasOptions()) { 95 | $lines[] = " 'options' => \$options"; 96 | } 97 | 98 | $lines[] = "]);"; 99 | 100 | return $lines; 101 | } 102 | 103 | /** 104 | * @param \google\protobuf\FieldDescriptorProto $field 105 | * 106 | * @return string[] 107 | */ 108 | protected function generateFieldBody(FieldDescriptorProto $field) 109 | { 110 | $lines = []; 111 | $name = $field->getName(); 112 | $number = $field->getNumber(); 113 | $typeName = $field->getTypeName(); 114 | $extendee = $field->getExtendee(); 115 | $type = $field->getType()->name(); 116 | $label = $field->getLabel()->name(); 117 | $default = $this->getDefaultFieldValue($field); 118 | $values = [ 119 | 'number' => var_export($number, true), 120 | 'name' => var_export($name, true), 121 | 'type' => '\google\protobuf\FieldDescriptorProto\Type::' . $type . '()', 122 | 'label' => '\google\protobuf\FieldDescriptorProto\Label::' . $label . '()', 123 | ]; 124 | 125 | if ($extendee) { 126 | $values['extendee'] = var_export($extendee, true); 127 | } 128 | 129 | if ($typeName) { 130 | $values['type_name'] = var_export($typeName, true); 131 | } 132 | 133 | if ($field->hasDefaultValue()) { 134 | $values['default_value'] = $default; 135 | } 136 | 137 | $lines[] = '\google\protobuf\FieldDescriptorProto::fromArray(['; 138 | $lines = array_merge($lines, $this->generateArrayLines($values)); 139 | $lines[] = ']),'; 140 | 141 | return $lines; 142 | } 143 | 144 | /** 145 | * @param \Protobuf\Compiler\Entity $entity 146 | * 147 | * @return string[] 148 | */ 149 | protected function createOptionsBody(Entity $entity) 150 | { 151 | $descriptor = $entity->getDescriptor(); 152 | $values = []; 153 | $lines = []; 154 | 155 | $options = $descriptor->getOptions(); 156 | $extensions = $options->extensions(); 157 | 158 | $lines[] = '$options = \google\protobuf\MessageOptions::fromArray(['; 159 | $lines = array_merge($lines, $this->generateArrayLines($values)); 160 | $lines[] = ']);'; 161 | $lines[] = null; 162 | 163 | for ($extensions->rewind(); $extensions->valid(); $extensions->next()) { 164 | $extension = $extensions->current(); 165 | $info = $extensions->getInfo(); 166 | 167 | $method = '\\' . $extension->getMethod(); 168 | $value = var_export($info, true); 169 | $name = $extension->getName(); 170 | 171 | if (is_object($info)) { 172 | $value = '\\' . $value; 173 | } 174 | 175 | $lines[] = '$options->extensions()->add(' . $method . '(), ' . $value . ');'; 176 | } 177 | 178 | return $lines; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Generator/Message/ReadFromGenerator.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class ReadFromGenerator extends BaseGenerator implements GeneratorVisitor 26 | { 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function visit(Entity $entity, GeneratorInterface $class) 31 | { 32 | $class->addMethodFromGenerator($this->generateMethod($entity)); 33 | } 34 | 35 | /** 36 | * @param \Protobuf\Compiler\Entity $entity 37 | * 38 | * @return \Zend\Code\Generator\GeneratorInterface 39 | */ 40 | protected function generateMethod(Entity $entity) 41 | { 42 | $lines = $this->generateBody($entity); 43 | $body = implode(PHP_EOL, $lines); 44 | $method = MethodGenerator::fromArray([ 45 | 'name' => 'readFrom', 46 | 'body' => $body, 47 | 'parameters' => [ 48 | [ 49 | 'name' => 'context', 50 | 'type' => '\Protobuf\ReadContext', 51 | ] 52 | ], 53 | 'docblock' => [ 54 | 'shortDescription' => "{@inheritdoc}" 55 | ] 56 | ]); 57 | 58 | return $method; 59 | } 60 | 61 | /** 62 | * @param \Protobuf\Compiler\Entity $entity 63 | * 64 | * @return string[] 65 | */ 66 | public function generateBody(Entity $entity) 67 | { 68 | $innerLoop = $this->addIndentation($this->generateInnerLoop($entity), 1); 69 | 70 | $body[] = '$reader = $context->getReader();'; 71 | $body[] = '$length = $context->getLength();'; 72 | $body[] = '$stream = $context->getStream();'; 73 | $body[] = null; 74 | $body[] = '$limit = ($length !== null)'; 75 | $body[] = ' ? ($stream->tell() + $length)'; 76 | $body[] = ' : null;'; 77 | $body[] = null; 78 | $body[] = 'while ($limit === null || $stream->tell() < $limit) {'; 79 | 80 | $body = array_merge($body, $innerLoop); 81 | 82 | $body[] = null; 83 | $body[] = '}'; 84 | 85 | return $body; 86 | } 87 | 88 | /** 89 | * @param \Protobuf\Compiler\Entity $entity 90 | * 91 | * @return string[] 92 | */ 93 | protected function generateInnerLoop(Entity $entity) 94 | { 95 | $descriptor = $entity->getDescriptor(); 96 | $fields = $descriptor->getFieldList() ?: []; 97 | 98 | $body[] = null; 99 | $body[] = 'if ($stream->eof()) {'; 100 | $body[] = ' break;'; 101 | $body[] = '}'; 102 | $body[] = null; 103 | $body[] = '$key = $reader->readVarint($stream);'; 104 | $body[] = '$wire = \Protobuf\WireFormat::getTagWireType($key);'; 105 | $body[] = '$tag = \Protobuf\WireFormat::getTagFieldNumber($key);'; 106 | $body[] = null; 107 | $body[] = 'if ($stream->eof()) {'; 108 | $body[] = ' break;'; 109 | $body[] = '}'; 110 | $body[] = null; 111 | 112 | foreach ($fields as $field) { 113 | $lines = $this->generateFieldCondition($entity, $field); 114 | $body = array_merge($body, $lines); 115 | } 116 | 117 | $unknowFieldName = $this->getUniqueFieldName($descriptor, 'unknownFieldSet'); 118 | $extensionsFieldName = $this->getUniqueFieldName($descriptor, 'extensions'); 119 | 120 | $body[] = '$extensions = $context->getExtensionRegistry();'; 121 | $body[] = '$extension = $extensions ? $extensions->findByNumber(__CLASS__, $tag) : null;'; 122 | $body[] = null; 123 | $body[] = 'if ($extension !== null) {'; 124 | $body[] = ' $this->extensions()->add($extension, $extension->readFrom($context, $wire));'; 125 | $body[] = null; 126 | $body[] = ' continue;'; 127 | $body[] = '}'; 128 | $body[] = null; 129 | $body[] = 'if ($this->' . $unknowFieldName . ' === null) {'; 130 | $body[] = ' $this->' . $unknowFieldName . ' = new \Protobuf\UnknownFieldSet();'; 131 | $body[] = '}'; 132 | $body[] = null; 133 | $body[] = '$data = $reader->readUnknown($stream, $wire);'; 134 | $body[] = '$unknown = new \Protobuf\Unknown($tag, $wire, $data);'; 135 | $body[] = null; 136 | $body[] = '$this->' . $unknowFieldName . '->add($unknown);'; 137 | 138 | return $body; 139 | } 140 | 141 | /** 142 | * @param \Protobuf\Compiler\Entity $entity 143 | * @param google\protobuf\FieldDescriptorProto $field 144 | * 145 | * @return string[] 146 | */ 147 | protected function generateFieldCondition(Entity $entity, FieldDescriptorProto $field) 148 | { 149 | $tag = $field->getNumber(); 150 | $lines = $this->generateFieldReadStatement($entity, $field); 151 | $lines = $this->addIndentation($lines, 1); 152 | 153 | $body[] = 'if ($tag === ' . $tag . ') {'; 154 | $body = array_merge($body, $lines); 155 | $body[] = '}'; 156 | $body[] = null; 157 | 158 | return $body; 159 | } 160 | 161 | /** 162 | * @param \Protobuf\Compiler\Entity $entity 163 | * @param \google\protobuf\FieldDescriptorProto $field 164 | * 165 | * @return string[] 166 | */ 167 | protected function generateFieldReadStatement(Entity $entity, FieldDescriptorProto $field) 168 | { 169 | $generator = new ReadFieldStatementGenerator($this->context); 170 | $statement = $generator->generateFieldReadStatement($entity, $field); 171 | 172 | return $statement; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Compiler.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class Compiler 25 | { 26 | /** 27 | * @var \Psr\Log\LoggerInterface 28 | */ 29 | protected $logger; 30 | 31 | /** 32 | * @var \Protobuf\Configuration 33 | */ 34 | protected $config; 35 | 36 | /** 37 | * @param \Psr\Log\LoggerInterface $logger 38 | * @param \Protobuf\Configuration $config 39 | */ 40 | public function __construct(LoggerInterface $logger, Configuration $config = null) 41 | { 42 | $this->logger = $logger; 43 | $this->config = $config ?: self::defaultConfig(); 44 | } 45 | 46 | /** 47 | * @param \Protobuf\Stream $stream 48 | * 49 | * @return \Protobuf\Stream 50 | */ 51 | public function compile(Stream $stream) 52 | { 53 | // Parse the request 54 | $request = CodeGeneratorRequest::fromStream($stream, $this->config); 55 | $response = new CodeGeneratorResponse(); 56 | $context = $this->createContext($request); 57 | $entities = $context->getEntities(); 58 | $options = $context->getOptions(); 59 | $generator = new Generator($context); 60 | 61 | // whether or not it will renegerate classes with new extensions 62 | $regenerate = false; 63 | $hasExtension = $context->hasProtobufExtension(); 64 | 65 | // Run each entity 66 | foreach ($entities as $key => $entity) { 67 | $generateImported = $options->getGenerateImported(); 68 | $isFileToGenerate = $entity->isFileToGenerate(); 69 | 70 | // Only compile those given to generate, not the imported ones 71 | if ( ! $generateImported && ! $isFileToGenerate) { 72 | $this->logger->debug(sprintf('Skipping generation of imported class "%s"', $entity->getClass())); 73 | 74 | continue; 75 | } 76 | 77 | $this->logger->info(sprintf('Generating class "%s"', $entity->getClass())); 78 | 79 | $generator->visit($entity); 80 | 81 | $file = new File(); 82 | $path = $entity->getPath(); 83 | $content = $entity->getContent(); 84 | 85 | $file->setName($path); 86 | $file->setContent($content); 87 | 88 | $response->addFile($file); 89 | 90 | if ($hasExtension && $this->loadEntityClass($entity)) { 91 | $regenerate = true; 92 | } 93 | } 94 | 95 | if ($regenerate) { 96 | 97 | $this->logger->info('Regenerating classes with new extensions'); 98 | 99 | $stream->seek(0); 100 | 101 | // Renegerate classes with new extensions 102 | return $this->compile($stream); 103 | } 104 | 105 | $this->logger->info('Generation completed.'); 106 | 107 | // Finally serialize the response object 108 | return $response->toStream($this->config); 109 | } 110 | 111 | /** 112 | * @param \Protobuf\Compiler\Entity $entity 113 | * 114 | * @return bool 115 | */ 116 | protected function loadEntityClass(Entity $entity) 117 | { 118 | $type = $entity->getType(); 119 | $content = $entity->getContent(); 120 | $class = $entity->getNamespacedName(); 121 | 122 | if (class_exists($class) || interface_exists($class)) { 123 | return false; 124 | } 125 | 126 | $this->logger->info(sprintf('Loading class "%s"', $class)); 127 | 128 | $tempname = tempnam(sys_get_temp_dir(), 'proto') . '.php'; 129 | 130 | file_put_contents($tempname, $content); 131 | 132 | include($tempname); 133 | @unlink($tempname); 134 | 135 | if ($type === Entity::TYPE_EXTENSION) { 136 | 137 | $this->logger->info(sprintf('Registering extension "%s"', $class)); 138 | 139 | $config = $this->config; 140 | $registry = $config->getExtensionRegistry(); 141 | 142 | $class::registerAllExtensions($registry); 143 | 144 | return true; 145 | } 146 | 147 | return false; 148 | } 149 | 150 | /** 151 | * @param \google\protobuf\compiler\CodeGeneratorRequest $request 152 | * 153 | * @return \Protobuf\Compiler\Context 154 | */ 155 | public function createContext(CodeGeneratorRequest $request) 156 | { 157 | $options = $this->createOptions($request); 158 | $entities = $this->createEntities($request); 159 | $context = new Context($entities, $options, $this->config); 160 | 161 | return $context; 162 | } 163 | 164 | /** 165 | * @param \google\protobuf\compiler\CodeGeneratorRequest $request 166 | * 167 | * @return array 168 | */ 169 | protected function createEntities(CodeGeneratorRequest $request) 170 | { 171 | $builder = new EntityBuilder($request); 172 | $entities = $builder->buildEntities(); 173 | 174 | return $entities; 175 | } 176 | 177 | /** 178 | * @param \google\protobuf\compiler\CodeGeneratorRequest $request 179 | * 180 | * @return \Protobuf\Compiler\Options 181 | */ 182 | protected function createOptions(CodeGeneratorRequest $request) 183 | { 184 | $parameter = $request->getParameter(); 185 | $options = []; 186 | 187 | parse_str($parameter, $options); 188 | 189 | return Options::fromArray($options); 190 | } 191 | 192 | /** 193 | * @return \Protobuf\Configuration 194 | */ 195 | public static function defaultConfig() 196 | { 197 | $config = Configuration::getInstance(); 198 | $registry = $config->getExtensionRegistry(); 199 | 200 | Extension::registerAllExtensions($registry); 201 | 202 | return $config; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/Entity.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class Entity 15 | { 16 | const TYPE_EXTENSION = 'extension'; 17 | const TYPE_MESSAGE = 'message'; 18 | const TYPE_SERVICE = 'service'; 19 | const TYPE_ENUM = 'enum'; 20 | 21 | /** 22 | * @var string 23 | */ 24 | protected $type; 25 | 26 | /** 27 | * @var string 28 | */ 29 | protected $path; 30 | 31 | /** 32 | * @var string 33 | */ 34 | protected $name; 35 | 36 | /** 37 | * @var string 38 | */ 39 | protected $parent; 40 | 41 | /** 42 | * @var string 43 | */ 44 | protected $content; 45 | 46 | /** 47 | * @var \Protobuf\Message 48 | */ 49 | protected $descriptor; 50 | 51 | /** 52 | * @var \google\protobuf\FileDescriptorProto 53 | */ 54 | protected $fileDescriptor; 55 | 56 | /** 57 | * @var bool 58 | */ 59 | protected $fileToGenerate = false; 60 | 61 | /** 62 | * @var bool 63 | */ 64 | protected $protobufExtension = false; 65 | 66 | /** 67 | * @param string $type 68 | * @param string $name 69 | * @param \Protobuf\Message $descriptor 70 | * @param \google\protobuf\FileDescriptorProto $fileDescriptor 71 | * @param string $parent 72 | */ 73 | public function __construct($type, $name, Message $descriptor, FileDescriptorProto $fileDescriptor, $parent = null) 74 | { 75 | $this->type = $type; 76 | $this->name = $name; 77 | $this->parent = $parent; 78 | $this->descriptor = $descriptor; 79 | $this->fileDescriptor = $fileDescriptor; 80 | } 81 | 82 | /** 83 | * @return string 84 | */ 85 | public function getPath() 86 | { 87 | return $this->path; 88 | } 89 | 90 | /** 91 | * @param string $path 92 | */ 93 | public function setPath($path) 94 | { 95 | $this->path = $path; 96 | } 97 | 98 | /** 99 | * @return string 100 | */ 101 | public function getClass() 102 | { 103 | $name = $this->getName(); 104 | $package = $this->getPackage(); 105 | 106 | return$this->fullyQualifiedName($package, $name); 107 | } 108 | 109 | /** 110 | * @return string 111 | */ 112 | public function getName() 113 | { 114 | return $this->name; 115 | } 116 | 117 | /** 118 | * @return string 119 | */ 120 | public function getPackage() 121 | { 122 | $parent = $this->parent; 123 | $package = $this->fileDescriptor->getPackage(); 124 | 125 | return $this->fullyQualifiedName($package, $parent); 126 | } 127 | 128 | /** 129 | * @return string 130 | */ 131 | public function getNamespace() 132 | { 133 | $package = $this->getPackage(); 134 | $extension = Extension::package(); 135 | $extensions = $this->getFileOptionsExtensions(); 136 | 137 | if ($extensions !== null && $extensions->offsetExists($extension)) { 138 | $package = $this->fullyQualifiedName($extensions->get($extension), $this->parent); 139 | } 140 | 141 | if ($package === null) { 142 | return $package; 143 | } 144 | 145 | return str_replace('.', '\\', $package); 146 | } 147 | 148 | /** 149 | * @return string 150 | */ 151 | public function getNamespacedName() 152 | { 153 | $namespace = $this->getNamespace(); 154 | $name = $this->getName(); 155 | 156 | if ($namespace === null) { 157 | return '\\' . $name; 158 | } 159 | 160 | return '\\' . $namespace . '\\' . $name; 161 | } 162 | 163 | /** 164 | * @return string 165 | */ 166 | public function getType() 167 | { 168 | return $this->type; 169 | } 170 | 171 | /** 172 | * @return string 173 | */ 174 | public function getContent() 175 | { 176 | return $this->content; 177 | } 178 | 179 | /** 180 | * @param string $content 181 | */ 182 | public function setContent($content) 183 | { 184 | $this->content = $content; 185 | } 186 | 187 | /** 188 | * @return bool 189 | */ 190 | public function isFileToGenerate() 191 | { 192 | return $this->fileToGenerate; 193 | } 194 | 195 | /** 196 | * @param bool $flag 197 | */ 198 | public function setFileToGenerate($flag) 199 | { 200 | $this->fileToGenerate = $flag; 201 | } 202 | 203 | /** 204 | * @return bool 205 | */ 206 | public function isProtobufExtension() 207 | { 208 | return $this->protobufExtension; 209 | } 210 | 211 | /** 212 | * @param bool $flag 213 | */ 214 | public function setProtobufExtension($flag) 215 | { 216 | $this->protobufExtension = $flag; 217 | } 218 | 219 | /** 220 | * @return \Protobuf\Message 221 | */ 222 | public function getDescriptor() 223 | { 224 | return $this->descriptor; 225 | } 226 | 227 | /** 228 | * @return \google\protobuf\FileDescriptorProto 229 | */ 230 | public function getFileDescriptor() 231 | { 232 | return $this->fileDescriptor; 233 | } 234 | 235 | /** 236 | * @return \Protobuf\Extension\ExtensionFieldMap 237 | */ 238 | protected function getFileOptionsExtensions() 239 | { 240 | if ( ! $this->fileDescriptor->hasOptions()) { 241 | return null; 242 | } 243 | 244 | return $this->fileDescriptor->getOptions()->extensions(); 245 | } 246 | 247 | /** 248 | * @return string 249 | */ 250 | protected function fullyQualifiedName() 251 | { 252 | $args = func_get_args(); 253 | $parts = array_filter($args); 254 | $name = implode('.', $parts); 255 | 256 | if (empty($parts)) { 257 | return null; 258 | } 259 | 260 | return $name; 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /tests/EntityTest.php: -------------------------------------------------------------------------------- 1 | assertNull($this->invokeMethod($entity, 'fullyQualifiedName', [null])); 24 | $this->assertEquals('package.SimpleMessage', $this->invokeMethod($entity, 'fullyQualifiedName', ['package', null, 'SimpleMessage'])); 25 | $this->assertEquals('package.Parent.SimpleMessage', $this->invokeMethod($entity, 'fullyQualifiedName', ['package', 'Parent', 'SimpleMessage'])); 26 | } 27 | 28 | public function testGetPackage() 29 | { 30 | $name = 'SimpleMessage'; 31 | $type = Entity::TYPE_MESSAGE; 32 | $descriptor = new DescriptorProto(); 33 | $fileDescriptor1 = new FileDescriptorProto(); 34 | $fileDescriptor2 = new FileDescriptorProto(); 35 | $fileDescriptor3 = new FileDescriptorProto(); 36 | 37 | $fileDescriptor2->setPackage('package'); 38 | $fileDescriptor3->setPackage('package'); 39 | 40 | $entity1 = new Entity($type, $name, $descriptor, $fileDescriptor1); 41 | $entity2 = new Entity($type, $name, $descriptor, $fileDescriptor2); 42 | $entity3 = new Entity($type, $name, $descriptor, $fileDescriptor3, 'Parent'); 43 | 44 | $this->assertNull($entity1->getPackage()); 45 | $this->assertEquals('package', $entity2->getPackage()); 46 | $this->assertEquals('package.Parent', $entity3->getPackage()); 47 | } 48 | 49 | public function testGetClass() 50 | { 51 | $name = 'SimpleMessage'; 52 | $type = Entity::TYPE_MESSAGE; 53 | $descriptor = new DescriptorProto(); 54 | $fileDescriptor1 = new FileDescriptorProto(); 55 | $fileDescriptor2 = new FileDescriptorProto(); 56 | $fileDescriptor3 = new FileDescriptorProto(); 57 | 58 | $fileDescriptor2->setPackage('package'); 59 | $fileDescriptor3->setPackage('package'); 60 | 61 | $entity1 = new Entity($type, $name, $descriptor, $fileDescriptor1); 62 | $entity2 = new Entity($type, $name, $descriptor, $fileDescriptor2); 63 | $entity3 = new Entity($type, $name, $descriptor, $fileDescriptor3, 'Parent'); 64 | 65 | $this->assertEquals('SimpleMessage', $entity1->getClass()); 66 | $this->assertEquals('package.SimpleMessage', $entity2->getClass()); 67 | $this->assertEquals('package.Parent.SimpleMessage', $entity3->getClass()); 68 | } 69 | 70 | public function testGetNamespace() 71 | { 72 | $name = 'SimpleMessage'; 73 | $type = Entity::TYPE_MESSAGE; 74 | $descriptor = new DescriptorProto(); 75 | $fileDescriptor1 = new FileDescriptorProto(); 76 | $fileDescriptor2 = new FileDescriptorProto(); 77 | $fileDescriptor3 = new FileDescriptorProto(); 78 | 79 | $fileDescriptor2->setPackage('package'); 80 | $fileDescriptor3->setPackage('package'); 81 | 82 | $entity1 = new Entity($type, $name, $descriptor, $fileDescriptor1); 83 | $entity2 = new Entity($type, $name, $descriptor, $fileDescriptor2); 84 | $entity3 = new Entity($type, $name, $descriptor, $fileDescriptor3, 'Parent'); 85 | 86 | $this->assertNull($entity1->getNamespace()); 87 | $this->assertEquals('package', $entity2->getNamespace()); 88 | $this->assertEquals('package\\Parent', $entity3->getNamespace()); 89 | } 90 | 91 | public function testGetNamespaceUsingPhpOptions() 92 | { 93 | $name = 'SimpleMessage'; 94 | $type = Entity::TYPE_MESSAGE; 95 | $descriptor = new DescriptorProto(); 96 | $fileDescriptor1 = new FileDescriptorProto(); 97 | $fileDescriptor2 = new FileDescriptorProto(); 98 | $fileDescriptor3 = new FileDescriptorProto(); 99 | 100 | $fileOptions2 = new FileOptions(); 101 | $fileOptions3 = new FileOptions(); 102 | 103 | $fileOptions2->extensions()->put(Extension::package(), 'Package'); 104 | $fileOptions3->extensions()->put(Extension::package(), 'Package'); 105 | 106 | $fileDescriptor1->setPackage('package'); 107 | $fileDescriptor2->setPackage('package'); 108 | $fileDescriptor3->setPackage('package'); 109 | $fileDescriptor2->setOptions($fileOptions2); 110 | $fileDescriptor3->setOptions($fileOptions3); 111 | 112 | $entity1 = new Entity($type, $name, $descriptor, $fileDescriptor1); 113 | $entity2 = new Entity($type, $name, $descriptor, $fileDescriptor2); 114 | $entity3 = new Entity($type, $name, $descriptor, $fileDescriptor3, 'Parent'); 115 | 116 | $this->assertEquals('package', $entity1->getNamespace()); 117 | $this->assertEquals('Package', $entity2->getNamespace()); 118 | $this->assertEquals('Package\\Parent', $entity3->getNamespace()); 119 | } 120 | 121 | public function testGetNamespacedName() 122 | { 123 | $name = 'SimpleMessage'; 124 | $type = Entity::TYPE_MESSAGE; 125 | $descriptor = new DescriptorProto(); 126 | $fileDescriptor1 = new FileDescriptorProto(); 127 | $fileDescriptor2 = new FileDescriptorProto(); 128 | $fileDescriptor3 = new FileDescriptorProto(); 129 | 130 | $fileDescriptor2->setPackage('package'); 131 | $fileDescriptor3->setPackage('package'); 132 | 133 | $entity1 = new Entity($type, $name, $descriptor, $fileDescriptor1); 134 | $entity2 = new Entity($type, $name, $descriptor, $fileDescriptor2); 135 | $entity3 = new Entity($type, $name, $descriptor, $fileDescriptor3, 'Parent'); 136 | 137 | $this->assertEquals('\\SimpleMessage', $entity1->getNamespacedName()); 138 | $this->assertEquals('\\package\\SimpleMessage', $entity2->getNamespacedName()); 139 | $this->assertEquals('\\package\\Parent\\SimpleMessage', $entity3->getNamespacedName()); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Generator/Message/WriteFieldStatementGenerator.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class WriteFieldStatementGenerator extends BaseGenerator 20 | { 21 | /** 22 | * @var string 23 | */ 24 | protected $targetVar; 25 | 26 | /** 27 | * @param string $variable 28 | */ 29 | public function setTargetVar($variable) 30 | { 31 | $this->targetVar = $variable; 32 | } 33 | 34 | /** 35 | * @param \Protobuf\Compiler\Entity $entity 36 | * @param \google\protobuf\FieldDescriptorProto $field 37 | * 38 | * @return string[] 39 | */ 40 | public function generateFieldWriteStatement(Entity $entity, FieldDescriptorProto $field) 41 | { 42 | $body = []; 43 | $name = $field->getName(); 44 | $type = $field->getType(); 45 | $rule = $field->getLabel(); 46 | $tag = $field->getNumber(); 47 | $options = $field->getOptions(); 48 | $variable = $this->targetVar ?: '$this->' . $name; 49 | $isPack = $options ? $options->getPacked() : false; 50 | 51 | $wire = $isPack ? WireFormat::WIRE_LENGTH : WireFormat::getWireType($type->value(), null); 52 | $key = WireFormat::getFieldKey($tag, $wire); 53 | 54 | if ($rule === Label::LABEL_REPEATED() && $isPack) { 55 | $itemValSttm = ($type === Type::TYPE_ENUM()) 56 | ? '$val->value()' 57 | : '$val'; 58 | 59 | $body[] = '$innerSize = 0;'; 60 | $body[] = '$calculator = $sizeContext->getSizeCalculator();'; 61 | $body[] = null; 62 | $body[] = 'foreach (' . $variable . ' as $val) {'; 63 | $body[] = ' $innerSize += ' . $this->generateValueSizeStatement($type->value(), $itemValSttm) . ';'; 64 | $body[] = '}'; 65 | $body[] = null; 66 | $body[] = '$writer->writeVarint($stream, ' . $key . ');'; 67 | $body[] = '$writer->writeVarint($stream, $innerSize);'; 68 | $body[] = null; 69 | $body[] = 'foreach (' . $variable . ' as $val) {'; 70 | $body[] = ' ' . $this->generateWriteScalarStatement($type->value(), $itemValSttm) . ';'; 71 | $body[] = '}'; 72 | 73 | return $body; 74 | } 75 | 76 | if ($type === Type::TYPE_MESSAGE() && $rule === Label::LABEL_REPEATED()) { 77 | $body[] = 'foreach (' . $variable . ' as $val) {'; 78 | $body[] = ' $writer->writeVarint($stream, ' . $key . ');'; 79 | $body[] = ' $writer->writeVarint($stream, $val->serializedSize($sizeContext));'; 80 | $body[] = ' $val->writeTo($context);'; 81 | $body[] = '}'; 82 | 83 | return $body; 84 | } 85 | 86 | if ($type === Type::TYPE_ENUM() && $rule === LABEL::LABEL_REPEATED()) { 87 | $body[] = 'foreach (' . $variable . ' as $val) {'; 88 | $body[] = ' $writer->writeVarint($stream, ' . $key . ');'; 89 | $body[] = ' ' . $this->generateWriteScalarStatement($type->value(), '$val->value()') . ';'; 90 | $body[] = '}'; 91 | 92 | return $body; 93 | } 94 | 95 | if ($rule === Label::LABEL_REPEATED()) { 96 | $body[] = 'foreach (' . $variable . ' as $val) {'; 97 | $body[] = ' $writer->writeVarint($stream, ' . $key . ');'; 98 | $body[] = ' ' . $this->generateWriteScalarStatement($type->value(), '$val') . ';'; 99 | $body[] = '}'; 100 | 101 | return $body; 102 | } 103 | 104 | if ($type === Type::TYPE_ENUM()) { 105 | $body[] = sprintf('$writer->writeVarint($stream, %s);', $key); 106 | $body[] = $this->generateWriteScalarStatement($type->value(), $variable . '->value()') . ';'; 107 | 108 | return $body; 109 | } 110 | 111 | if ($type !== Type::TYPE_MESSAGE()) { 112 | $body[] = sprintf('$writer->writeVarint($stream, %s);', $key); 113 | $body[] = $this->generateWriteScalarStatement($type->value(), $variable) . ';'; 114 | 115 | return $body; 116 | } 117 | 118 | $body[] = '$writer->writeVarint($stream, ' . $key . ');'; 119 | $body[] = '$writer->writeVarint($stream, ' . $variable . '->serializedSize($sizeContext));'; 120 | $body[] = $variable . '->writeTo($context);'; 121 | 122 | return $body; 123 | } 124 | 125 | /** 126 | * write a scalar value. 127 | * 128 | * @param integer $type 129 | * @param string $value 130 | * @param string $stream 131 | * 132 | * @return array 133 | */ 134 | public function generateWriteScalarStatement($type, $value, $stream = '$stream') 135 | { 136 | $mapping = [ 137 | Type::TYPE_INT32_VALUE => '$writer->writeVarint(%s, %s)', 138 | Type::TYPE_INT64_VALUE => '$writer->writeVarint(%s, %s)', 139 | Type::TYPE_UINT64_VALUE => '$writer->writeVarint(%s, %s)', 140 | Type::TYPE_UINT32_VALUE => '$writer->writeVarint(%s, %s)', 141 | Type::TYPE_DOUBLE_VALUE => '$writer->writeDouble(%s, %s)', 142 | Type::TYPE_FIXED64_VALUE => '$writer->writeFixed64(%s, %s)', 143 | Type::TYPE_SFIXED64_VALUE => '$writer->writeSFixed64(%s, %s)', 144 | Type::TYPE_FLOAT_VALUE => '$writer->writeFloat(%s, %s)', 145 | Type::TYPE_FIXED32_VALUE => '$writer->writeFixed32(%s, %s)', 146 | Type::TYPE_SFIXED32_VALUE => '$writer->writeSFixed32(%s, %s)', 147 | Type::TYPE_ENUM_VALUE => '$writer->writeVarint(%s, %s)', 148 | Type::TYPE_SINT32_VALUE => '$writer->writeZigzag32(%s, %s)', 149 | Type::TYPE_SINT64_VALUE => '$writer->writeZigzag64(%s, %s)', 150 | Type::TYPE_BOOL_VALUE => '$writer->writeBool(%s, %s)', 151 | Type::TYPE_STRING_VALUE => '$writer->writeString(%s, %s)', 152 | Type::TYPE_BYTES_VALUE => '$writer->writeByteStream(%s, %s)', 153 | ]; 154 | 155 | if (isset($mapping[$type])) { 156 | return sprintf($mapping[$type], $stream, $value); 157 | } 158 | 159 | throw new \InvalidArgumentException('Unknown field type : ' . $type); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Protoc/ProcessBuilder.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ProcessBuilder 18 | { 19 | /** 20 | * @var string 21 | */ 22 | protected $plugin; 23 | 24 | /** 25 | * @var string 26 | */ 27 | protected $protoc; 28 | 29 | /** 30 | * @var string 31 | */ 32 | protected $version; 33 | 34 | /** 35 | * @var bool 36 | */ 37 | protected $includeDescriptors = false; 38 | 39 | /** 40 | * @var array 41 | */ 42 | protected $descriptorPaths = []; 43 | 44 | /** 45 | * @param string $plugin 46 | * @param string $protoc 47 | * @param string $version 48 | */ 49 | public function __construct($plugin, $protoc, $version = '2.3.0') 50 | { 51 | $this->plugin = $plugin; 52 | $this->protoc = $protoc; 53 | $this->version = $version; 54 | } 55 | 56 | /** 57 | * @param bool $flag 58 | */ 59 | public function setIncludeDescriptors($flag) 60 | { 61 | $this->includeDescriptors = $flag; 62 | } 63 | 64 | /** 65 | * @param array $paths 66 | */ 67 | public function setDescriptorPaths($paths) 68 | { 69 | $this->descriptorPaths = $paths; 70 | } 71 | 72 | /** 73 | * Assert min protoc version. 74 | * 75 | * @throws \UnexpectedValueException Indicates a problem with protoc. 76 | */ 77 | public function assertVersion() 78 | { 79 | $process = $this->createProtocVersionProcess(); 80 | 81 | // Check if protoc is available 82 | $process->mustRun(); 83 | 84 | $return = $process->getExitCode(); 85 | $result = trim($process->getOutput()); 86 | 87 | if (0 !== $return && 1 !== $return) { 88 | throw new UnexpectedValueException("Unable to find the protoc command. Please make sure it's installed and available in the path."); 89 | } 90 | 91 | if ( ! preg_match('/[0-9\.]+/', $result, $match)) { 92 | throw new UnexpectedValueException("Unable to get protoc command version. Please make sure it's installed and available in the path."); 93 | } 94 | 95 | if (version_compare($match[0], $this->version) < 0) { 96 | throw new UnexpectedValueException("The protoc command in your system is too old. Minimum version required is '{$this->version}' but found '$result'."); 97 | } 98 | } 99 | 100 | /** 101 | * Create a process 102 | * 103 | * @param string|SplFileInfo $outPath 104 | * @param string[]|SplFileInfo[] $protosFiles 105 | * @param string[]|SplFileInfo[] $includeDirs 106 | * @param string[]|SplFileInfo[] $parameters 107 | * 108 | * @return \Symfony\Component\Process\Process 109 | * 110 | * @throws \InvalidArgumentException 111 | */ 112 | public function createProcess($outPath, array $protosFiles, array $includeDirs, array $parameters) 113 | { 114 | if (empty($protosFiles)) { 115 | throw new InvalidArgumentException('Proto file list cannot be empty.'); 116 | } 117 | 118 | $builder = new SymfonyProcessBuilder(); 119 | $outDir = $this->getRealPath($outPath); 120 | $include = $this->getRealPaths($includeDirs); 121 | $protos = $this->getRealPaths($protosFiles, true); 122 | 123 | $builder->setPrefix($this->protoc); 124 | 125 | $builder->add(sprintf('--plugin=protoc-gen-php=%s', $this->plugin)); 126 | 127 | foreach ($include as $i) { 128 | $builder->add(sprintf('--proto_path=%s', $i)); 129 | } 130 | 131 | if ($this->includeDescriptors) { 132 | $builder->add(sprintf('--proto_path=%s', $this->findDescriptorsPath())); 133 | } 134 | 135 | // Protoc will pass custom arguments to the plugin if they are given 136 | // before a colon character. ie: --php_out="foo=bar:/path/to/plugin" 137 | $out = ( ! empty($parameters)) 138 | ? http_build_query($parameters, '', '&') . ':' . $outDir 139 | : $outDir; 140 | 141 | $builder->add(sprintf('--php_out=%s', $out)); 142 | 143 | // Add the chosen proto files to generate 144 | foreach ($protos as $proto) { 145 | $builder->add($proto); 146 | } 147 | 148 | return $builder->getProcess(); 149 | } 150 | 151 | /** 152 | * @param array $files 153 | * @param bool $isFile 154 | * 155 | * @return array 156 | * 157 | * @throws \InvalidArgumentException 158 | */ 159 | protected function getRealPaths(array $files, $isFile = false) 160 | { 161 | $paths = []; 162 | 163 | foreach ($files as $file) { 164 | $paths[] = $this->getRealPath($file, $isFile); 165 | } 166 | 167 | return $paths; 168 | } 169 | 170 | /** 171 | * @param string|SplFileInfo $file 172 | * @param bool $isFile 173 | * 174 | * @return array 175 | * 176 | * @throws \InvalidArgumentException 177 | */ 178 | protected function getRealPath($file, $isFile = false) 179 | { 180 | if ( ! $file instanceof SplFileInfo) { 181 | $file = new SplFileInfo($file); 182 | } 183 | 184 | $realpath = $file->getRealPath(); 185 | 186 | if (false === $realpath) { 187 | throw new InvalidArgumentException(sprintf( 188 | 'The %s "%s" does not exist.', 189 | ($isFile ? 'file' : 'directory'), 190 | $file->getPathname() 191 | )); 192 | } 193 | 194 | return $realpath; 195 | } 196 | 197 | /** 198 | * @return string 199 | * 200 | * @throws \RuntimeException 201 | */ 202 | protected function findDescriptorsPath() 203 | { 204 | foreach ($this->descriptorPaths as $path) { 205 | if ( ! is_dir($path)) { 206 | continue; 207 | } 208 | 209 | return realpath($path) ?: $path; 210 | } 211 | 212 | throw new RuntimeException('Unable to find "protobuf-php/google-protobuf-proto".'); 213 | } 214 | 215 | /** 216 | * @return \Symfony\Component\Process\Process 217 | */ 218 | public function createProtocVersionProcess() 219 | { 220 | $protoc = $this->protoc; 221 | $process = new Process("$protoc --version"); 222 | 223 | return $process; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/Generator/Message/FromArrayGenerator.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class FromArrayGenerator extends BaseGenerator implements GeneratorVisitor 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function visit(Entity $entity, GeneratorInterface $class) 27 | { 28 | $class->addMethodFromGenerator($this->generateMethod($entity)); 29 | } 30 | 31 | /** 32 | * @param \Protobuf\Compiler\Entity $entity 33 | * 34 | * @return string 35 | */ 36 | protected function generateMethod(Entity $entity) 37 | { 38 | $lines = $this->generateBody($entity); 39 | $body = implode(PHP_EOL, $lines); 40 | $method = MethodGenerator::fromArray([ 41 | 'name' => 'fromArray', 42 | 'body' => $body, 43 | 'static' => true, 44 | 'parameters' => [ 45 | [ 46 | 'name' => 'values', 47 | 'type' => 'array', 48 | ] 49 | ], 50 | 'docblock' => [ 51 | 'shortDescription' => "{@inheritdoc}" 52 | ] 53 | ]); 54 | 55 | return $method; 56 | } 57 | 58 | /** 59 | * @param \Protobuf\Compiler\Entity $entity 60 | * 61 | * @return string[] 62 | */ 63 | public function generateBody(Entity $entity) 64 | { 65 | $lines = []; 66 | $addLine = false; 67 | $descriptor = $entity->getDescriptor(); 68 | $fields = $descriptor->getFieldList() ?: []; 69 | $defaults = $this->generateDefaultValues($entity); 70 | 71 | foreach ($fields as $field) { 72 | if ($field->getLabel() !== Label::LABEL_REQUIRED()) { 73 | continue; 74 | } 75 | 76 | if ($field->hasDefaultValue()) { 77 | continue; 78 | } 79 | 80 | $item = $this->generateRequiredFieldException($entity, $field); 81 | $lines = array_merge($lines, $item, [null]); 82 | } 83 | 84 | $lines[] = '$message = new self();'; 85 | $lines[] = '$values = array_merge(['; 86 | $lines = array_merge($lines, $this->addIndentation($defaults, 1)); 87 | $lines[] = '], $values);'; 88 | $lines[] = null; 89 | 90 | foreach ($fields as $field) { 91 | if ($field->getLabel() === Label::LABEL_REPEATED()) { 92 | continue; 93 | } 94 | 95 | $item = $this->generateSetValue($entity, $field); 96 | $lines = array_merge($lines, $item); 97 | 98 | $addLine = true; 99 | } 100 | 101 | if ($addLine) { 102 | $lines[] = null; 103 | } 104 | 105 | foreach ($fields as $field) { 106 | if ($field->getLabel() !== Label::LABEL_REPEATED()) { 107 | continue; 108 | } 109 | 110 | $item = $this->generateAddValue($entity, $field); 111 | $lines = array_merge($lines, $item, [null]); 112 | } 113 | 114 | $lines[] = 'return $message;'; 115 | 116 | return $lines; 117 | } 118 | 119 | /** 120 | * @param \Protobuf\Compiler\Entity $entity 121 | * @param \google\protobuf\FieldDescriptorProto $field 122 | * 123 | * @return string[] 124 | */ 125 | protected function generateRequiredFieldException(Entity $entity, FieldDescriptorProto $field) 126 | { 127 | $name = $field->getName(); 128 | $tag = $field->getNumber(); 129 | 130 | $class = $entity->getNamespacedName(); 131 | $format = 'Field "%s" (tag %s) is required but has no value.'; 132 | $message = var_export(sprintf($format, $name, $tag), true); 133 | 134 | $body[] = 'if ( ! isset($values[\'' . $name . '\'])) {'; 135 | $body[] = ' throw new \InvalidArgumentException(' . $message . ');'; 136 | $body[] = '}'; 137 | 138 | return $body; 139 | } 140 | 141 | /** 142 | * @param \Protobuf\Compiler\Entity $entity 143 | * @param \google\protobuf\FieldDescriptorProto $field 144 | * 145 | * @return array 146 | */ 147 | protected function generateSetValue(Entity $entity, FieldDescriptorProto $field) 148 | { 149 | $lines = []; 150 | $fieldName = $field->getName(); 151 | $methodName = $this->getAccessorName('set', $field); 152 | 153 | $lines[] = '$message->' . $methodName . '($values[\'' . $fieldName . '\']);'; 154 | 155 | return $lines; 156 | } 157 | 158 | /** 159 | * @param \Protobuf\Compiler\Entity $entity 160 | * @param \google\protobuf\FieldDescriptorProto $field 161 | * 162 | * @return array 163 | */ 164 | protected function generateAddValue(Entity $entity, FieldDescriptorProto $field) 165 | { 166 | $lines = []; 167 | $fieldName = $field->getName(); 168 | $methodName = 'add' . $this->getClassifiedName($field); 169 | 170 | $lines[] = 'foreach ($values[\'' . $fieldName . '\'] as $item) {'; 171 | $lines[] = ' $message->' . $methodName . '($item);'; 172 | $lines[] = '}'; 173 | 174 | return $lines; 175 | } 176 | 177 | /** 178 | * @param \Protobuf\Compiler\Entity $entity 179 | * 180 | * @return array 181 | */ 182 | protected function generateDefaultValues(Entity $entity) 183 | { 184 | $descriptor = $entity->getDescriptor(); 185 | $fields = $descriptor->getFieldList() ?: []; 186 | $size = count($fields); 187 | $lines = []; 188 | 189 | foreach ($fields as $i => $field) { 190 | $name = $field->getName(); 191 | $comma = ($i +1 < $size) ? ',' : ''; 192 | $value = $this->getDefaultFieldValue($field); 193 | 194 | // required field throw InvalidArgumentException 195 | if ($field->getLabel() === Label::LABEL_REQUIRED()) { 196 | continue; 197 | } 198 | 199 | if ($field->getLabel() === Label::LABEL_REPEATED()) { 200 | $value = '[]'; 201 | } 202 | 203 | $lines[] = "'$name' => $value" . $comma; 204 | } 205 | 206 | return $lines; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Generator/EnumGenerator.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class EnumGenerator extends BaseGenerator implements EntityVisitor 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function visit(Entity $entity) 25 | { 26 | $name = $entity->getName(); 27 | $namespace = $entity->getNamespace(); 28 | $shortDescription = 'Protobuf enum : ' . $entity->getClass(); 29 | $class = ClassGenerator::fromArray([ 30 | 'name' => $name, 31 | 'namespacename' => $namespace, 32 | 'extendedclass' => '\Protobuf\Enum', 33 | 'methods' => $this->generateMethods($entity), 34 | 'properties' => $this->generateProperties($entity), 35 | 'docblock' => [ 36 | 'shortDescription' => $shortDescription, 37 | ] 38 | ]); 39 | 40 | $entity->setContent($this->generateFileContent($class, $entity)); 41 | } 42 | 43 | /** 44 | * @param \Protobuf\Compiler\Entity $entity 45 | * 46 | * @return string[] 47 | */ 48 | protected function generateProperties(Entity $entity) 49 | { 50 | $descriptor = $entity->getDescriptor(); 51 | $class = $entity->getNamespacedName(); 52 | $properties = $this->generateConstants($entity); 53 | $values = $descriptor->getValueList() ?: []; 54 | 55 | foreach ($values as $value) { 56 | $properties[] = PropertyGenerator::fromArray([ 57 | 'static' => true, 58 | 'name' => $value->getName(), 59 | 'visibility' => PropertyGenerator::VISIBILITY_PROTECTED, 60 | 'docblock' => [ 61 | 'tags' => [ 62 | [ 63 | 'name' => 'var', 64 | 'description' => $class, 65 | ] 66 | ] 67 | ] 68 | ]); 69 | } 70 | 71 | foreach ($properties as $value) { 72 | $value->getDocblock()->setWordWrap(false); 73 | } 74 | 75 | return $properties; 76 | } 77 | 78 | /** 79 | * @param \Protobuf\Compiler\Entity $entity 80 | * 81 | * @return string[] 82 | */ 83 | public function generateConstants(Entity $entity) 84 | { 85 | $constants = []; 86 | $descriptor = $entity->getDescriptor(); 87 | $values = $descriptor->getValueList() ?: []; 88 | 89 | foreach ($values as $value) { 90 | $name = $value->getName(); 91 | $number = $value->getNumber(); 92 | $constant = PropertyGenerator::fromArray([ 93 | 'const' => true, 94 | 'defaultvalue' => $number, 95 | 'name' => $name . '_VALUE', 96 | 'docblock' => [ 97 | 'shortDescription' => "$name = $number", 98 | ] 99 | ]); 100 | 101 | $constants[] = $constant; 102 | } 103 | 104 | return $constants; 105 | } 106 | 107 | /** 108 | * @param \Protobuf\Compiler\Entity $entity 109 | * 110 | * @return string[] 111 | */ 112 | public function generateMethods(Entity $entity) 113 | { 114 | $methods = []; 115 | $descriptor = $entity->getDescriptor(); 116 | $values = $descriptor->getValueList() ?: []; 117 | 118 | foreach ($values as $value) { 119 | $methods[] = $this->generateMethod($entity, $value); 120 | } 121 | 122 | $methods[] = $this->generateValueOfMethod($entity); 123 | 124 | return $methods; 125 | } 126 | 127 | /** 128 | * @param \Protobuf\Compiler\Entity $entity 129 | * @param EnumValueDescriptorProto $value 130 | * 131 | * @return string 132 | */ 133 | public function generateMethod(Entity $entity, EnumValueDescriptorProto $value) 134 | { 135 | $body = []; 136 | $name = $value->getName(); 137 | $number = $value->getNumber(); 138 | $class = $entity->getNamespacedName(); 139 | $args = var_export($name, true) . ', self::' . $name . '_VALUE'; 140 | 141 | $body[] = 'if (self::$' . $name . ' !== null) {'; 142 | $body[] = ' return self::$' . $name . ';'; 143 | $body[] = '}'; 144 | $body[] = null; 145 | $body[] = 'return self::$' . $name . ' = new self(' . $args . ');'; 146 | 147 | $method = MethodGenerator::fromArray([ 148 | 'static' => true, 149 | 'name' => $name, 150 | 'body' => implode(PHP_EOL, $body), 151 | 'docblock' => [ 152 | 'tags' => [ 153 | [ 154 | 'name' => 'return', 155 | 'description' => $class, 156 | ] 157 | ] 158 | ] 159 | ]); 160 | 161 | $method->getDocblock()->setWordWrap(false); 162 | 163 | return $method; 164 | } 165 | 166 | /** 167 | * @param \Protobuf\Compiler\Entity $entity 168 | * 169 | * @return string 170 | */ 171 | public function generateValueOfMethod(Entity $entity) 172 | { 173 | $body = []; 174 | $descriptor = $entity->getDescriptor(); 175 | $class = $entity->getNamespacedName(); 176 | $values = $descriptor->getValueList() ?: []; 177 | 178 | $body[] = 'switch ($value) {'; 179 | 180 | foreach ($values as $value) { 181 | $name = $value->getName(); 182 | $number = $value->getNumber(); 183 | 184 | $body[] = ' case ' . $number . ': return self::' . $name . '();'; 185 | } 186 | 187 | $body[] = ' default: return null;'; 188 | $body[] = '}'; 189 | 190 | $method = MethodGenerator::fromArray([ 191 | 'static' => true, 192 | 'name' => 'valueOf', 193 | 'body' => implode(PHP_EOL, $body), 194 | 'parameters' => [ 195 | [ 196 | 'name' => 'value', 197 | 'type' => 'int', 198 | ] 199 | ], 200 | 'docblock' => [ 201 | 'tags' => [ 202 | [ 203 | 'name' => 'param', 204 | 'description' => 'int $value', 205 | ], 206 | [ 207 | 'name' => 'return', 208 | 'description' => $class, 209 | ] 210 | ] 211 | ] 212 | ]); 213 | 214 | $method->getDocblock()->setWordWrap(false); 215 | 216 | return $method; 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /tests/Generator/Message/SerializedSizeFieldStatementGeneratorTest.php: -------------------------------------------------------------------------------- 1 | createContext([ 17 | [ 18 | 'name' => 'simple.proto', 19 | 'package' => 'ProtobufCompilerTest.Protos', 20 | 'values' => [ 21 | 'messages' => [ 22 | [ 23 | 'name' => 'Simple', 24 | 'fields' => $fields 25 | ] 26 | ] 27 | ] 28 | ] 29 | ]); 30 | } 31 | 32 | public function descriptorProvider() 33 | { 34 | return [ 35 | 36 | // required int32 37 | [ 38 | [ 39 | 1 => ['count', Field::TYPE_INT32, Field::LABEL_REQUIRED] 40 | ], 41 | <<<'CODE' 42 | $size += 1; 43 | $size += $calculator->computeVarintSize($this->count); 44 | CODE 45 | ], 46 | 47 | // repeated string 48 | [ 49 | [ 50 | 1 => ['lines', Field::TYPE_STRING, Field::LABEL_REPEATED] 51 | ], 52 | <<<'CODE' 53 | foreach ($this->lines as $val) { 54 | $size += 1; 55 | $size += $calculator->computeStringSize($val); 56 | } 57 | CODE 58 | ], 59 | 60 | // required int32 packed 61 | [ 62 | [ 63 | 1 => ['tags', Field::TYPE_INT32, Field::LABEL_REPEATED, null, [ 'options' => ['packed' => true] ]] 64 | ], 65 | <<<'CODE' 66 | $innerSize = 0; 67 | 68 | foreach ($this->tags as $val) { 69 | $innerSize += $calculator->computeVarintSize($val); 70 | } 71 | 72 | $size += 1; 73 | $size += $innerSize; 74 | $size += $calculator->computeVarintSize($innerSize); 75 | CODE 76 | ], 77 | 78 | // required message 79 | [ 80 | [ 81 | 1 => ['phone', Field::TYPE_MESSAGE, Field::LABEL_REQUIRED, 'ProtobufCompiler.Proto.PhoneNumber'] 82 | ], 83 | <<<'CODE' 84 | $innerSize = $this->phone->serializedSize($context); 85 | 86 | $size += 1; 87 | $size += $innerSize; 88 | $size += $calculator->computeVarintSize($innerSize); 89 | CODE 90 | ], 91 | 92 | // repeated message 93 | [ 94 | [ 95 | 1 => ['files', Field::TYPE_MESSAGE, Field::LABEL_REPEATED, 'ProtobufCompiler.Proto.File'] 96 | ], 97 | <<<'CODE' 98 | foreach ($this->files as $val) { 99 | $innerSize = $val->serializedSize($context); 100 | 101 | $size += 1; 102 | $size += $innerSize; 103 | $size += $calculator->computeVarintSize($innerSize); 104 | } 105 | CODE 106 | ], 107 | ]; 108 | } 109 | 110 | /** 111 | * @dataProvider descriptorProvider 112 | */ 113 | public function testGenerateFieldSizeStatement($fields, $expected, $fieldIndex = 0) 114 | { 115 | $context = $this->createMessagesContext($fields); 116 | $entity = $context->getEntity('ProtobufCompilerTest.Protos.Simple'); 117 | $generator = new SerializedSizeFieldStatementGenerator($context); 118 | $descritor = $entity->getDescriptor(); 119 | $field = $descritor->getFieldList()[$fieldIndex]; 120 | $actual = $generator->generateFieldSizeStatement($entity, $field); 121 | 122 | $this->assertEquals($expected, implode(PHP_EOL, $actual)); 123 | } 124 | 125 | public function testGenerateEnumRepeatedFieldSizeStatement() 126 | { 127 | $context = $this->createContext([ 128 | [ 129 | 'name' => 'simple.proto', 130 | 'package' => 'ProtobufCompilerTest.Protos', 131 | 'values' => [ 132 | 'messages' => [ 133 | [ 134 | 'name' => 'Simple', 135 | 'fields' => [ 136 | 1 => ['status', Field::TYPE_ENUM, Field::LABEL_REPEATED, 'ProtobufCompilerTest.Protos.Type'] 137 | ] 138 | ], 139 | [ 140 | 'name' => 'Type', 141 | 'fields' => [] 142 | ] 143 | ] 144 | ] 145 | ] 146 | ]); 147 | 148 | $entity = $context->getEntity('ProtobufCompilerTest.Protos.Simple'); 149 | $generator = new SerializedSizeFieldStatementGenerator($context); 150 | $descritor = $entity->getDescriptor(); 151 | $field = $descritor->getFieldList()[0]; 152 | 153 | $actual = $generator->generateFieldSizeStatement($entity, $field); 154 | $expected = <<<'CODE' 155 | foreach ($this->status as $val) { 156 | $size += 1; 157 | $size += $calculator->computeVarintSize($val->value()); 158 | } 159 | CODE; 160 | 161 | $this->assertEquals($expected, implode(PHP_EOL, $actual)); 162 | } 163 | 164 | public function testGenerateEnumPackadRepeatedFieldSizeStatement() 165 | { 166 | $options = new FieldOptions(); 167 | $context = $this->createContext([ 168 | [ 169 | 'name' => 'simple.proto', 170 | 'package' => 'ProtobufCompilerTest.Protos', 171 | 'values' => [ 172 | 'messages' => [ 173 | [ 174 | 'name' => 'Simple', 175 | 'fields' => [ 176 | 1 => ['status', Field::TYPE_ENUM, Field::LABEL_REPEATED, 'ProtobufCompilerTest.Protos.Type'] 177 | ] 178 | ], 179 | [ 180 | 'name' => 'Type', 181 | 'fields' => [] 182 | ] 183 | ] 184 | ] 185 | ] 186 | ]); 187 | 188 | $entity = $context->getEntity('ProtobufCompilerTest.Protos.Simple'); 189 | $generator = new SerializedSizeFieldStatementGenerator($context); 190 | $descritor = $entity->getDescriptor(); 191 | $field = $descritor->getFieldList()[0]; 192 | 193 | $options->setPacked(true); 194 | $field->setOptions($options); 195 | 196 | $actual = $generator->generateFieldSizeStatement($entity, $field); 197 | $expected = <<<'CODE' 198 | $innerSize = 0; 199 | 200 | foreach ($this->status as $val) { 201 | $innerSize += $calculator->computeVarintSize($val->value()); 202 | } 203 | 204 | $size += 1; 205 | $size += $innerSize; 206 | $size += $calculator->computeVarintSize($innerSize); 207 | CODE; 208 | 209 | $this->assertEquals($expected, implode(PHP_EOL, $actual)); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /tests/EntityBuilderTest.php: -------------------------------------------------------------------------------- 1 | createFileDescriptorProto('simple.proto', 'Protos', [ 21 | 'messages' => [ 22 | [ 23 | 'name' => 'Simple', 24 | 'fields' => [] 25 | ] 26 | ] 27 | ]); 28 | 29 | $descriptor2 = $this->createFileDescriptorProto('include.proto', 'Protos', [ 30 | 'messages' => [ 31 | [ 32 | 'name' => 'Include', 33 | 'fields' => [] 34 | ] 35 | ] 36 | ]); 37 | 38 | $request = new CodeGeneratorRequest(); 39 | 40 | $request->addProtoFile($descriptor1); 41 | $request->addProtoFile($descriptor2); 42 | $request->addFileToGenerate($descriptor1->getName()); 43 | 44 | $builder = new EntityBuilder($request); 45 | $entities = $builder->buildEntities(); 46 | 47 | $this->assertCount(2, $entities); 48 | $this->assertArrayhasKey('Protos.Simple', $entities); 49 | $this->assertArrayhasKey('Protos.Include', $entities); 50 | 51 | $this->assertInstanceOf(Entity::CLASS, $entities['Protos.Simple']); 52 | $this->assertInstanceOf(Entity::CLASS, $entities['Protos.Include']); 53 | 54 | $this->assertEquals('Protos.Simple', $entities['Protos.Simple']->getClass()); 55 | $this->assertEquals('Protos.Include', $entities['Protos.Include']->getClass()); 56 | 57 | $this->assertEquals(Entity::TYPE_MESSAGE, $entities['Protos.Simple']->getType()); 58 | $this->assertEquals(Entity::TYPE_MESSAGE, $entities['Protos.Include']->getType()); 59 | 60 | $this->assertTrue($entities['Protos.Simple']->isFileToGenerate()); 61 | $this->assertFalse($entities['Protos.Include']->isFileToGenerate()); 62 | } 63 | 64 | public function testBuildFileEntities() 65 | { 66 | $request = new CodeGeneratorRequest(); 67 | $descriptor = $this->createFileDescriptorProto('simple.proto', 'Protos', [ 68 | 'messages' => [ 69 | [ 70 | 'name' => 'ParentMessage', 71 | 'fields' => [], 72 | 'values' => [ 73 | 'messages' => [ 74 | [ 75 | 'name' => 'InnerMessage', 76 | 'fields' => [], 77 | 'values' => [ 78 | 'enums' => [ 79 | [ 80 | 'name' => 'InnerMessageEnum', 81 | 'values' => [] 82 | ] 83 | ] 84 | ] 85 | ] 86 | ] 87 | ] 88 | ] 89 | ], 90 | 'extensions' => [ 91 | 200 => ['extension', Field::TYPE_STRING, Field::LABEL_OPTIONAL, '.google.protobuf.MessageOptions'] 92 | ], 93 | 'enums' => [ 94 | [ 95 | 'name' => 'EnumType', 96 | 'values' => [] 97 | ] 98 | ], 99 | 'services' => [ 100 | [ 101 | 'name' => 'SearchService', 102 | 'values' => [] 103 | ] 104 | ] 105 | ]); 106 | 107 | $builder = new EntityBuilder($request); 108 | $entities = $this->invokeMethod($builder, 'buildFileEntities', [$descriptor]); 109 | 110 | $this->assertCount(6, $entities); 111 | $this->assertInstanceOf(Entity::CLASS, $entities[0]); 112 | $this->assertInstanceOf(Entity::CLASS, $entities[1]); 113 | $this->assertInstanceOf(Entity::CLASS, $entities[2]); 114 | $this->assertInstanceOf(Entity::CLASS, $entities[3]); 115 | $this->assertInstanceOf(Entity::CLASS, $entities[4]); 116 | $this->assertInstanceOf(Entity::CLASS, $entities[5]); 117 | 118 | $this->assertEquals('Protos.ParentMessage', $entities[0]->getClass()); 119 | $this->assertEquals('Protos.ParentMessage.InnerMessage', $entities[1]->getClass()); 120 | $this->assertEquals('Protos.ParentMessage.InnerMessage.InnerMessageEnum', $entities[2]->getClass()); 121 | $this->assertEquals('Protos.SearchService', $entities[3]->getClass()); 122 | $this->assertEquals('Protos.EnumType', $entities[4]->getClass()); 123 | $this->assertEquals('Protos.Extension', $entities[5]->getClass()); 124 | $this->assertTrue($entities[5]->isProtobufExtension()); 125 | 126 | $this->assertEquals(Entity::TYPE_MESSAGE, $entities[0]->getType()); 127 | $this->assertEquals(Entity::TYPE_MESSAGE, $entities[1]->getType()); 128 | $this->assertEquals(Entity::TYPE_ENUM, $entities[2]->getType()); 129 | $this->assertEquals(Entity::TYPE_SERVICE, $entities[3]->getType()); 130 | $this->assertEquals(Entity::TYPE_ENUM, $entities[4]->getType()); 131 | $this->assertEquals(Entity::TYPE_EXTENSION, $entities[5]->getType()); 132 | } 133 | 134 | public function testHasExtension() 135 | { 136 | $descriptor1 = $this->createFileDescriptorProto('simple.proto', 'Protos', [ 137 | 'extensions' => [ 138 | 200 => ['extension', Field::TYPE_STRING, Field::LABEL_OPTIONAL, '.Protos.Extendee'] 139 | ] 140 | ]); 141 | 142 | $descriptor2 = $this->createFileDescriptorProto('simple.proto', 'Protos', [ 143 | 'messages' => [ 144 | [ 145 | 'name' => 'Dog', 146 | 'fields' => [], 147 | 'values' => [ 148 | 'extensions' => [ 149 | 101 => ['animal', Field::TYPE_MESSAGE, Field::LABEL_OPTIONAL, '.ProtobufCompilerTest.Protos.Extension.Animal', '.ProtobufCompilerTest.Protos.Extension.Dog'] 150 | ] 151 | ] 152 | ] 153 | ] 154 | ]); 155 | 156 | $descriptor3 = $this->createFileDescriptorProto('simple.proto', 'Protos', []); 157 | $descriptor4 = $this->createFileDescriptorProto('simple.proto', 'Protos', [ 158 | 'messages' => [ 159 | [ 160 | 'name' => 'Simple', 161 | 'fields' => [] 162 | ] 163 | ] 164 | ]); 165 | 166 | $request = new CodeGeneratorRequest(); 167 | $builder = new EntityBuilder($request); 168 | 169 | $this->assertTrue($this->invokeMethod($builder, 'hasExtension', [$descriptor1])); 170 | $this->assertTrue($this->invokeMethod($builder, 'hasExtension', [$descriptor2])); 171 | $this->assertFalse($this->invokeMethod($builder, 'hasExtension', [$descriptor3])); 172 | $this->assertFalse($this->invokeMethod($builder, 'hasExtension', [$descriptor4])); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Generator/ExtensionGenerator.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class ExtensionGenerator extends BaseGenerator implements EntityVisitor 26 | { 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function visit(Entity $entity) 31 | { 32 | $name = $entity->getName(); 33 | $namespace = $entity->getNamespace(); 34 | $shortDescription = 'Protobuf extension : ' . $entity->getClass(); 35 | $class = ClassGenerator::fromArray([ 36 | 'name' => $name, 37 | 'namespacename' => $namespace, 38 | 'implementedinterfaces' => ['\Protobuf\Extension'], 39 | 'properties' => $this->generateFields($entity), 40 | 'methods' => $this->generateMethods($entity), 41 | 'docblock' => [ 42 | 'shortDescription' => $shortDescription 43 | ] 44 | ]); 45 | 46 | $entity->setContent($this->generateFileContent($class, $entity)); 47 | } 48 | 49 | /** 50 | * @param \Protobuf\Compiler\Entity $entity 51 | * 52 | * @return string[] 53 | */ 54 | protected function generateFields(Entity $entity) 55 | { 56 | $fields = []; 57 | $descriptor = $entity->getDescriptor(); 58 | $extensions = $descriptor->getExtensionList() ?: []; 59 | 60 | foreach ($extensions as $field) { 61 | $fields[] = $this->generateExtensionField($entity, $field); 62 | } 63 | 64 | return $fields; 65 | } 66 | 67 | /** 68 | * @param \Protobuf\Compiler\Entity $entity 69 | * @param \google\protobuf\FieldDescriptorProto $field 70 | * 71 | * @return string 72 | */ 73 | protected function generateExtensionField(Entity $entity, FieldDescriptorProto $field) 74 | { 75 | $name = $field->getName(); 76 | $number = $field->getNumber(); 77 | $docBlock = $this->getDocBlockType($field); 78 | $type = $this->getFieldTypeName($field); 79 | $label = $this->getFieldLabelName($field); 80 | $property = PropertyGenerator::fromArray([ 81 | 'static' => true, 82 | 'name' => $name, 83 | 'visibility' => PropertyGenerator::VISIBILITY_PROTECTED, 84 | 'docblock' => [ 85 | 'shortDescription' => "Extension field : $name $label $type = $number", 86 | 'tags' => [ 87 | [ 88 | 'name' => 'var', 89 | 'description' => '\Protobuf\Extension', 90 | ] 91 | ] 92 | ] 93 | ]); 94 | 95 | $property->getDocblock()->setWordWrap(false); 96 | 97 | return $property; 98 | } 99 | 100 | /** 101 | * @param \Protobuf\Compiler\Entity $entity 102 | * 103 | * @return string[] 104 | */ 105 | protected function generateMethods(Entity $entity) 106 | { 107 | $descriptor = $entity->getDescriptor(); 108 | $extensions = $descriptor->getExtensionList() ?: []; 109 | $methods = [$this->generateRegisterAllExtensionsMethod($entity)]; 110 | 111 | foreach ($extensions as $field) { 112 | $methods[] = $this->generateExtensionMethod($entity, $field); 113 | } 114 | 115 | return $methods; 116 | } 117 | 118 | /** 119 | * @param \Protobuf\Compiler\Entity $entity 120 | * @param \google\protobuf\FieldDescriptorProto $field 121 | * 122 | * @return string 123 | */ 124 | protected function generateExtensionMethod(Entity $entity, FieldDescriptorProto $field) 125 | { 126 | $fieldName = $field->getName(); 127 | $descriptor = $entity->getDescriptor(); 128 | $methodName = $this->getCamelizedName($field); 129 | $bodyGen = new ExtensionsGenerator($this->context); 130 | $body = implode(PHP_EOL, $bodyGen->generateBody($entity, $field)); 131 | $method = MethodGenerator::fromArray([ 132 | 'static' => true, 133 | 'body' => $body, 134 | 'name' => $methodName, 135 | 'docblock' => [ 136 | 'shortDescription' => "Extension field : $fieldName", 137 | 'tags' => [ 138 | [ 139 | 'name' => 'return', 140 | 'description' => '\Protobuf\Extension', 141 | ] 142 | ] 143 | ] 144 | ]); 145 | 146 | $method->getDocblock()->setWordWrap(false); 147 | 148 | return $method; 149 | } 150 | 151 | /** 152 | * @param \Protobuf\Compiler\Entity $entity 153 | * 154 | * @return string 155 | */ 156 | protected function generateRegisterAllExtensionsMethod(Entity $entity) 157 | { 158 | $lines = []; 159 | $fields = []; 160 | $descriptor = $entity->getDescriptor(); 161 | $extensions = $descriptor->getExtensionList() ?: []; 162 | $messages = $descriptor->getMessageTypeList() ?: []; 163 | 164 | foreach ($messages as $message) { 165 | 166 | if ( ! $message->hasExtensionList()) { 167 | continue; 168 | } 169 | 170 | foreach ($message->getExtensionList() as $extension) { 171 | $fields[] = $extension; 172 | } 173 | } 174 | 175 | foreach ($fields as $field) { 176 | $type = $field->getTypeName(); 177 | $name = $this->getCamelizedName($field); 178 | 179 | $ref = $this->getEntity($type); 180 | $class = $ref->getNamespacedName(); 181 | $lines[] = '$registry->add(' . $class . '::' . $name. '());'; 182 | } 183 | 184 | foreach ($extensions as $field) { 185 | $type = $field->getTypeName(); 186 | $name = $this->getCamelizedName($field); 187 | 188 | $lines[] = '$registry->add(self::' . $name . '());'; 189 | } 190 | 191 | $body = implode(PHP_EOL, $lines); 192 | $method = MethodGenerator::fromArray([ 193 | 'static' => true, 194 | 'body' => $body, 195 | 'name' => 'registerAllExtensions', 196 | 'parameters' => [ 197 | [ 198 | 'name' => 'registry', 199 | 'type' => '\Protobuf\Extension\ExtensionRegistry' 200 | ] 201 | ], 202 | 'docblock' => [ 203 | 'shortDescription' => "Register all extensions", 204 | 'tags' => [ 205 | [ 206 | 'name' => 'param', 207 | 'description' => '\Protobuf\Extension\ExtensionRegistry', 208 | ] 209 | ] 210 | ] 211 | ]); 212 | 213 | return $method; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/Generator/Message/ReadFieldStatementGenerator.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class ReadFieldStatementGenerator extends BaseGenerator 22 | { 23 | const BREAK_MODE_CONTINUE = 1; 24 | const BREAK_MODE_RETURN = 2; 25 | 26 | /** 27 | * @var string 28 | */ 29 | protected $breakMode = self::BREAK_MODE_CONTINUE; 30 | 31 | /** 32 | * @var string 33 | */ 34 | protected $targetVar; 35 | 36 | /** 37 | * @param integer $mode 38 | */ 39 | public function setBreakMode($mode) 40 | { 41 | $this->breakMode = $mode; 42 | } 43 | 44 | /** 45 | * @param string $variable 46 | */ 47 | public function setTargetVar($variable) 48 | { 49 | $this->targetVar = $variable; 50 | } 51 | 52 | /** 53 | * @param \Protobuf\Compiler\Entity $entity 54 | * @param \google\protobuf\FieldDescriptorProto $field 55 | * 56 | * @return string[] 57 | */ 58 | public function generateFieldReadStatement(Entity $entity, FieldDescriptorProto $field) 59 | { 60 | $body = []; 61 | $reference = null; 62 | $type = $field->getType(); 63 | $name = $field->getName(); 64 | $rule = $field->getLabel(); 65 | $tag = $field->getNumber(); 66 | $options = $field->getOptions(); 67 | $isPack = $options ? $options->getPacked() : false; 68 | $variable = $this->targetVar ?: '$this->' . $name; 69 | $breakSttm = $this->getBreakStatement($variable); 70 | 71 | if ($field->hasTypeName()) { 72 | $typeName = $field->getTypeName(); 73 | $typeEntity = $this->getEntity($typeName); 74 | $reference = $typeEntity->getNamespacedName(); 75 | } 76 | 77 | if ( ! $isPack) { 78 | $body[] = sprintf('\Protobuf\WireFormat::assertWireType($wire, %s);', $type->value()); 79 | $body[] = null; 80 | } 81 | 82 | if ($rule === Label::LABEL_REPEATED() && $isPack) { 83 | $readSttm = ($type === Type::TYPE_ENUM()) 84 | ? $reference . '::valueOf(' . $this->generateReadScalarStatement($type->value()) .')' 85 | : $this->generateReadScalarStatement($type->value()); 86 | 87 | $body[] = '$innerSize = $reader->readVarint($stream);'; 88 | $body[] = '$innerLimit = $stream->tell() + $innerSize;'; 89 | $body[] = null; 90 | $body[] = 'if (' . $variable . ' === null) {'; 91 | $body[] = ' ' . $variable . ' = new ' . $this->getCollectionClassName($field) . '();'; 92 | $body[] = '}'; 93 | $body[] = null; 94 | $body[] = 'while ($stream->tell() < $innerLimit) {'; 95 | $body[] = ' ' . $variable . '->add(' . $readSttm . ');'; 96 | $body[] = '}'; 97 | $body[] = null; 98 | $body[] = $breakSttm; 99 | 100 | return $body; 101 | } 102 | 103 | if ($type === Type::TYPE_MESSAGE() && $rule === Label::LABEL_REPEATED()) { 104 | $body[] = '$innerSize = $reader->readVarint($stream);'; 105 | $body[] = '$innerMessage = new ' . $reference . '();'; 106 | $body[] = null; 107 | $body[] = 'if (' . $variable . ' === null) {'; 108 | $body[] = ' ' . $variable . ' = new \Protobuf\MessageCollection();'; 109 | $body[] = '}'; 110 | $body[] = null; 111 | $body[] = $variable . '->add($innerMessage);'; 112 | $body[] = null; 113 | $body[] = '$context->setLength($innerSize);'; 114 | $body[] = '$innerMessage->readFrom($context);'; 115 | $body[] = '$context->setLength($length);'; 116 | $body[] = null; 117 | $body[] = $breakSttm; 118 | 119 | return $body; 120 | } 121 | 122 | if ($type === Type::TYPE_ENUM() && $rule === LABEL::LABEL_REPEATED()) { 123 | $body[] = 'if (' . $variable . ' === null) {'; 124 | $body[] = ' ' . $variable . ' = new ' . $this->getCollectionClassName($field) . '();'; 125 | $body[] = '}'; 126 | $body[] = null; 127 | $body[] = $variable . '->add(' . $reference . '::valueOf(' . $this->generateReadScalarStatement($type->value()) . '));'; 128 | $body[] = null; 129 | $body[] = $breakSttm; 130 | 131 | return $body; 132 | } 133 | 134 | if ($type === Type::TYPE_MESSAGE()) { 135 | $body[] = '$innerSize = $reader->readVarint($stream);'; 136 | $body[] = '$innerMessage = new ' . $reference . '();'; 137 | $body[] = null; 138 | $body[] = $variable . ' = $innerMessage;'; 139 | $body[] = null; 140 | $body[] = '$context->setLength($innerSize);'; 141 | $body[] = '$innerMessage->readFrom($context);'; 142 | $body[] = '$context->setLength($length);'; 143 | $body[] = null; 144 | $body[] = $breakSttm; 145 | 146 | return $body; 147 | } 148 | 149 | if ($type === Type::TYPE_ENUM()) { 150 | $body[] = $variable . ' = ' . $reference . '::valueOf(' . $this->generateReadScalarStatement($type->value()) . ');'; 151 | $body[] = null; 152 | $body[] = $breakSttm; 153 | 154 | return $body; 155 | } 156 | 157 | if ($rule !== LABEL::LABEL_REPEATED()) { 158 | $body[] = $variable . ' = ' . $this->generateReadScalarStatement($type->value()) . ';'; 159 | $body[] = null; 160 | $body[] = $breakSttm; 161 | 162 | return $body; 163 | } 164 | 165 | $body[] = 'if (' . $variable . ' === null) {'; 166 | $body[] = ' ' . $variable . ' = new ' . $this->getCollectionClassName($field) . '();'; 167 | $body[] = '}'; 168 | $body[] = null; 169 | $body[] = $variable . '->add(' . $this->generateReadScalarStatement($type->value()) . ');'; 170 | $body[] = null; 171 | $body[] = $breakSttm; 172 | 173 | return $body; 174 | } 175 | 176 | /** 177 | * read a scalar value. 178 | * 179 | * @param integer $type 180 | * 181 | * @return string 182 | */ 183 | protected function generateReadScalarStatement($type) 184 | { 185 | $mapping = [ 186 | Type::TYPE_INT32_VALUE => '$reader->readVarint($stream)', 187 | Type::TYPE_INT64_VALUE => '$reader->readVarint($stream)', 188 | Type::TYPE_UINT64_VALUE => '$reader->readVarint($stream)', 189 | Type::TYPE_UINT32_VALUE => '$reader->readVarint($stream)', 190 | Type::TYPE_DOUBLE_VALUE => '$reader->readDouble($stream)', 191 | Type::TYPE_FIXED64_VALUE => '$reader->readFixed64($stream)', 192 | Type::TYPE_SFIXED64_VALUE => '$reader->readSFixed64($stream)', 193 | Type::TYPE_FLOAT_VALUE => '$reader->readFloat($stream)', 194 | Type::TYPE_FIXED32_VALUE => '$reader->readFixed32($stream)', 195 | Type::TYPE_SFIXED32_VALUE => '$reader->readSFixed32($stream)', 196 | Type::TYPE_ENUM_VALUE => '$reader->readVarint($stream)', 197 | Type::TYPE_SINT32_VALUE => '$reader->readZigzag($stream)', 198 | Type::TYPE_SINT64_VALUE => '$reader->readZigzag($stream)', 199 | Type::TYPE_BOOL_VALUE => '$reader->readBool($stream)', 200 | Type::TYPE_STRING_VALUE => '$reader->readString($stream)', 201 | Type::TYPE_BYTES_VALUE => '$reader->readByteStream($stream)', 202 | ]; 203 | 204 | if (isset($mapping[$type])) { 205 | return $mapping[$type]; 206 | } 207 | 208 | throw new InvalidArgumentException('Unknown field type : ' . $type); 209 | } 210 | 211 | /** 212 | * @param string $variable 213 | * 214 | * @return string 215 | */ 216 | protected function getBreakStatement($variable) 217 | { 218 | if ($this->breakMode === self::BREAK_MODE_CONTINUE) { 219 | return 'continue;'; 220 | } 221 | 222 | return 'return ' . $variable . ';'; 223 | } 224 | } 225 | --------------------------------------------------------------------------------