diff --git a/.cirrus.yml b/.cirrus.yml
index 2a3a7c07e..02e196b6a 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -14,6 +14,8 @@ BUILD_TEST_TASK_TEMPLATE: &BUILD_TEST_TASK_TEMPLATE
- composer run lint
static_analysis_script:
- composer run static-code-analysis
+ generate_library_script:
+ - composer gen-lib
test_script:
- composer test
@@ -26,12 +28,15 @@ linux_arm64_task:
- VERSION: 8.0
arm_container:
image: php:$VERSION
+ cpu: 4
+ memory: 12G
pre_req_script:
- - apt update --yes && apt install --yes zip unzip git libffi-dev
+ - apt update --yes && apt install --yes zip unzip git libffi-dev protobuf-compiler
- curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php
- php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer
- docker-php-ext-install sockets
- docker-php-ext-install ffi
+ - MAKEFLAGS=" -j4" pecl install grpc
version_check_script:
- php --version
<< : *BUILD_TEST_TASK_TEMPLATE
@@ -46,7 +51,8 @@ macos_arm64_task:
macos_instance:
image: ghcr.io/cirruslabs/macos-ventura-base:latest
pre_req_script:
- - brew install php@$VERSION composer
+ - brew install php@$VERSION composer protobuf
+ - MAKEFLAGS=" -j4" pecl install grpc
version_check_script:
- php --version
<< : *BUILD_TEST_TASK_TEMPLATE
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a7ebeac3f..599bf3397 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -60,15 +60,33 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
- extensions: sockets, curl, zip, ffi
+ extensions: ${{ matrix.operating-system == 'windows-latest' && matrix.php == '8.2' && 'sockets, curl, zip, ffi' || 'sockets, curl, zip, ffi, grpc' }}
php-version: ${{ matrix.php }}
coverage: none
ini-values: ${{ matrix.operating-system == 'windows-latest' && 'opcache.enable=0 opcache.enable_cli=0' || '' }}
+ - name: Install Protoc
+ uses: arduino/setup-protoc@v2
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Install gRPC Extension (for PHP 8.2 on Windows)
+ run: |
+ cd C:\tools\php
+ Invoke-WebRequest -Uri https://phpdev.toolsforresearch.com/php-8.2.7-nts-Win32-vs16-x64-grpc-protobuf.zip -OutFile php8.2.zip
+ unzip php8.2.zip ext/php_grpc.dll
+ rm php8.2.zip
+ echo "extension=php_grpc.dll" >> php.ini
+ if: ${{ matrix.operating-system == 'windows-latest' && matrix.php == '8.2' }}
+ shell: pwsh
+
- name: Composer install
uses: ramsey/composer-install@v2
with:
dependency-versions: ${{ matrix.dependencies }}
+ - name: Generate Library
+ run: composer gen-lib
+
- name: Composer test
run: composer test
diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
index 43a2591df..3bb3d8503 100644
--- a/.php-cs-fixer.php
+++ b/.php-cs-fixer.php
@@ -5,6 +5,7 @@
->in(__DIR__ . '/tests')
->in(__DIR__ . '/example')
->in(__DIR__ . '/compatibility-suite/tests')
+ ->exclude('library/src')
->name('*.php');
$config = new PhpCsFixer\Config();
diff --git a/composer.json b/composer.json
index 577c5b37f..af8416bd4 100644
--- a/composer.json
+++ b/composer.json
@@ -36,7 +36,8 @@
"guzzlehttp/guzzle": "^7.8",
"behat/behat": "^3.13",
"galbar/jsonpath": "^3.0",
- "ramsey/uuid": "^4.7"
+ "ramsey/uuid": "^4.7",
+ "pact-foundation/example-protobuf-sync-message-provider": "@dev"
},
"autoload": {
"psr-4": {
@@ -72,7 +73,14 @@
"MatchersProvider\\Tests\\": "example/matchers/provider/tests",
"GeneratorsConsumer\\": "example/generators/consumer/src",
"GeneratorsConsumer\\Tests\\": "example/generators/consumer/tests",
- "GeneratorsProvider\\Tests\\": "example/generators/provider/tests"
+ "GeneratorsProvider\\Tests\\": "example/generators/provider/tests",
+ "": [
+ "example/protobuf-sync-message/library/src"
+ ],
+ "ProtobufSyncMessageConsumer\\": "example/protobuf-sync-message/consumer/src",
+ "ProtobufSyncMessageConsumer\\Tests\\": "example/protobuf-sync-message/consumer/tests",
+ "ProtobufSyncMessageProvider\\": "example/protobuf-sync-message/provider/src",
+ "ProtobufSyncMessageProvider\\Tests\\": "example/protobuf-sync-message/provider/tests"
}
},
"scripts": {
@@ -83,6 +91,9 @@
"php -r \"array_map('unlink', glob('./example/*/pacts/*.json'));\"",
"phpunit --debug"
],
+ "gen-lib": [
+ "protoc --php_out=example/protobuf-sync-message/library/src example/protobuf-sync-message/library/proto/area_calculator.proto"
+ ],
"check-compatibility": "behat"
},
"extra": {
@@ -120,5 +131,11 @@
"allow-plugins": {
"tienvx/composer-downloads-plugin": true
}
- }
+ },
+ "repositories": [
+ {
+ "type": "path",
+ "url": "example/protobuf-sync-message/provider"
+ }
+ ]
}
diff --git a/example/protobuf-sync-message/consumer/phpunit.xml b/example/protobuf-sync-message/consumer/phpunit.xml
new file mode 100644
index 000000000..62a9eb007
--- /dev/null
+++ b/example/protobuf-sync-message/consumer/phpunit.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ ./tests
+
+
+
+
+
+
diff --git a/example/protobuf-sync-message/consumer/src/CalculatorClient.php b/example/protobuf-sync-message/consumer/src/CalculatorClient.php
new file mode 100644
index 000000000..4ce7497ac
--- /dev/null
+++ b/example/protobuf-sync-message/consumer/src/CalculatorClient.php
@@ -0,0 +1,23 @@
+_simpleRequest(
+ '/plugins.Calculator/calculate',
+ $request,
+ [AreaResponse::class, 'decode'],
+ $metadata,
+ []
+ )->wait();
+
+ return $response;
+ }
+}
diff --git a/example/protobuf-sync-message/consumer/src/ProtobufClient.php b/example/protobuf-sync-message/consumer/src/ProtobufClient.php
new file mode 100644
index 000000000..44602d6ed
--- /dev/null
+++ b/example/protobuf-sync-message/consumer/src/ProtobufClient.php
@@ -0,0 +1,23 @@
+baseUrl, [
+ 'credentials' => ChannelCredentials::createInsecure(),
+ ]);
+
+ return $client->calculate($shapeMessage);
+ }
+}
diff --git a/example/protobuf-sync-message/consumer/tests/ProtobufClientTest.php b/example/protobuf-sync-message/consumer/tests/ProtobufClientTest.php
new file mode 100644
index 000000000..55cde3ccd
--- /dev/null
+++ b/example/protobuf-sync-message/consumer/tests/ProtobufClientTest.php
@@ -0,0 +1,61 @@
+setConsumer('protobufSyncMessageConsumer');
+ $config->setProvider('protobufSyncMessageProvider');
+ $config->setPactSpecificationVersion('4.0.0');
+ $config->setPactDir(__DIR__.'/../../pacts');
+ if ($logLevel = \getenv('PACT_LOGLEVEL')) {
+ $config->setLogLevel($logLevel);
+ }
+ $config->setHost('127.0.0.1');
+ $builder = new SyncMessageBuilder($config, new ProtobufSyncMessageDriverFactory());
+ $builder
+ ->expectsToReceive('request for calculate shape area')
+ ->withMetadata([])
+ ->withContent(new Text(
+ json_encode([
+ 'pact:proto' => $protoPath,
+ 'pact:content-type' => 'application/grpc',
+ 'pact:proto-service' => 'Calculator/calculate',
+
+ 'request' => [
+ 'rectangle' => [
+ 'length' => 'matching(number, 3)',
+ 'width' => 'matching(number, 4)',
+ ],
+ ],
+ 'response' => [
+ 'value' => 'matching(number, 12)',
+ ]
+ ]),
+ 'application/grpc'
+ ));
+ $builder->registerMessage();
+
+ $service = new ProtobufClient("{$config->getHost()}:{$config->getPort()}");
+ $rectangle = (new Rectangle())->setLength(3)->setWidth(4);
+ $message = (new ShapeMessage())->setRectangle($rectangle);
+ $response = $service->calculate($message);
+
+ $this->assertTrue($builder->verify());
+ $this->assertEquals(3 * 4, $response->getValue());
+ }
+}
diff --git a/example/protobuf-sync-message/library/proto/area_calculator.proto b/example/protobuf-sync-message/library/proto/area_calculator.proto
new file mode 100644
index 000000000..8bce6c994
--- /dev/null
+++ b/example/protobuf-sync-message/library/proto/area_calculator.proto
@@ -0,0 +1,47 @@
+syntax = "proto3";
+
+package plugins;
+
+option php_generic_services = true;
+
+service Calculator {
+ rpc calculate (ShapeMessage) returns (AreaResponse) {}
+}
+
+message ShapeMessage {
+ oneof shape {
+ Square square = 1;
+ Rectangle rectangle = 2;
+ Circle circle = 3;
+ Triangle triangle = 4;
+ Parallelogram parallelogram = 5;
+ }
+}
+
+message Square {
+ float edge_length = 1;
+}
+
+message Rectangle {
+ float length = 1;
+ float width = 2;
+}
+
+message Circle {
+ float radius = 1;
+}
+
+message Triangle {
+ float edge_a = 1;
+ float edge_b = 2;
+ float edge_c = 3;
+}
+
+message Parallelogram {
+ float base_length = 1;
+ float height = 2;
+}
+
+message AreaResponse {
+ float value = 1;
+}
diff --git a/example/protobuf-sync-message/library/src/.gitignore b/example/protobuf-sync-message/library/src/.gitignore
new file mode 100644
index 000000000..cde8069e1
--- /dev/null
+++ b/example/protobuf-sync-message/library/src/.gitignore
@@ -0,0 +1 @@
+*.php
diff --git a/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json b/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json
new file mode 100644
index 000000000..ab308e252
--- /dev/null
+++ b/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json
@@ -0,0 +1,104 @@
+{
+ "consumer": {
+ "name": "protobufSyncMessageConsumer"
+ },
+ "interactions": [
+ {
+ "description": "request for calculate shape area",
+ "interactionMarkup": {
+ "markup": "```protobuf\nmessage AreaResponse {\n float value = 1;\n}\n```\n",
+ "markupType": "COMMON_MARK"
+ },
+ "pending": false,
+ "pluginConfiguration": {
+ "protobuf": {
+ "descriptorKey": "6b90c212dfe22dc3c119d1c3fe42b5e1",
+ "service": "Calculator/calculate"
+ }
+ },
+ "request": {
+ "contents": {
+ "content": "EgoNAABAQBUAAIBA",
+ "contentType": "application/protobuf;message=ShapeMessage",
+ "contentTypeHint": "BINARY",
+ "encoded": "base64"
+ },
+ "matchingRules": {
+ "body": {
+ "$.rectangle.length": {
+ "combine": "AND",
+ "matchers": [
+ {
+ "match": "number"
+ }
+ ]
+ },
+ "$.rectangle.width": {
+ "combine": "AND",
+ "matchers": [
+ {
+ "match": "number"
+ }
+ ]
+ }
+ }
+ },
+ "metadata": {
+ "contentType": "application/protobuf;message=ShapeMessage"
+ }
+ },
+ "response": [
+ {
+ "contents": {
+ "content": "DQAAQEE=",
+ "contentType": "application/protobuf;message=AreaResponse",
+ "contentTypeHint": "BINARY",
+ "encoded": "base64"
+ },
+ "matchingRules": {
+ "body": {
+ "$.value": {
+ "combine": "AND",
+ "matchers": [
+ {
+ "match": "number"
+ }
+ ]
+ }
+ }
+ },
+ "metadata": {
+ "contentType": "application/protobuf;message=AreaResponse"
+ }
+ }
+ ],
+ "transport": "grpc",
+ "type": "Synchronous/Messages"
+ }
+ ],
+ "metadata": {
+ "pactRust": {
+ "ffi": "0.4.11",
+ "mockserver": "1.2.4",
+ "models": "1.1.12"
+ },
+ "pactSpecification": {
+ "version": "4.0"
+ },
+ "plugins": [
+ {
+ "configuration": {
+ "6b90c212dfe22dc3c119d1c3fe42b5e1": {
+ "protoDescriptors": "CtYFChVhcmVhX2NhbGN1bGF0b3IucHJvdG8SB3BsdWdpbnMikgIKDFNoYXBlTWVzc2FnZRIpCgZzcXVhcmUYASABKAsyDy5wbHVnaW5zLlNxdWFyZUgAUgZzcXVhcmUSMgoJcmVjdGFuZ2xlGAIgASgLMhIucGx1Z2lucy5SZWN0YW5nbGVIAFIJcmVjdGFuZ2xlEikKBmNpcmNsZRgDIAEoCzIPLnBsdWdpbnMuQ2lyY2xlSABSBmNpcmNsZRIvCgh0cmlhbmdsZRgEIAEoCzIRLnBsdWdpbnMuVHJpYW5nbGVIAFIIdHJpYW5nbGUSPgoNcGFyYWxsZWxvZ3JhbRgFIAEoCzIWLnBsdWdpbnMuUGFyYWxsZWxvZ3JhbUgAUg1wYXJhbGxlbG9ncmFtQgcKBXNoYXBlIikKBlNxdWFyZRIfCgtlZGdlX2xlbmd0aBgBIAEoAlIKZWRnZUxlbmd0aCI5CglSZWN0YW5nbGUSFgoGbGVuZ3RoGAEgASgCUgZsZW5ndGgSFAoFd2lkdGgYAiABKAJSBXdpZHRoIiAKBkNpcmNsZRIWCgZyYWRpdXMYASABKAJSBnJhZGl1cyJPCghUcmlhbmdsZRIVCgZlZGdlX2EYASABKAJSBWVkZ2VBEhUKBmVkZ2VfYhgCIAEoAlIFZWRnZUISFQoGZWRnZV9jGAMgASgCUgVlZGdlQyJICg1QYXJhbGxlbG9ncmFtEh8KC2Jhc2VfbGVuZ3RoGAEgASgCUgpiYXNlTGVuZ3RoEhYKBmhlaWdodBgCIAEoAlIGaGVpZ2h0IiQKDEFyZWFSZXNwb25zZRIUCgV2YWx1ZRgBIAEoAlIFdmFsdWUySQoKQ2FsY3VsYXRvchI7CgljYWxjdWxhdGUSFS5wbHVnaW5zLlNoYXBlTWVzc2FnZRoVLnBsdWdpbnMuQXJlYVJlc3BvbnNlIgBCA9ACAWIGcHJvdG8z",
+ "protoFile": "syntax = \"proto3\";\n\npackage plugins;\n\noption php_generic_services = true;\n\nservice Calculator {\n rpc calculate (ShapeMessage) returns (AreaResponse) {}\n}\n\nmessage ShapeMessage {\n oneof shape {\n Square square = 1;\n Rectangle rectangle = 2;\n Circle circle = 3;\n Triangle triangle = 4;\n Parallelogram parallelogram = 5;\n }\n}\n\nmessage Square {\n float edge_length = 1;\n}\n\nmessage Rectangle {\n float length = 1;\n float width = 2;\n}\n\nmessage Circle {\n float radius = 1;\n}\n\nmessage Triangle {\n float edge_a = 1;\n float edge_b = 2;\n float edge_c = 3;\n}\n\nmessage Parallelogram {\n float base_length = 1;\n float height = 2;\n}\n\nmessage AreaResponse {\n float value = 1;\n}\n"
+ }
+ },
+ "name": "protobuf",
+ "version": "0.3.8"
+ }
+ ]
+ },
+ "provider": {
+ "name": "protobufSyncMessageProvider"
+ }
+}
\ No newline at end of file
diff --git a/example/protobuf-sync-message/provider/.rr.yaml b/example/protobuf-sync-message/provider/.rr.yaml
new file mode 100644
index 000000000..25f94af25
--- /dev/null
+++ b/example/protobuf-sync-message/provider/.rr.yaml
@@ -0,0 +1,11 @@
+version: "2.7"
+
+server:
+ command: "php worker.php"
+
+grpc:
+ listen: "tcp://127.0.0.1:9001"
+ proto:
+ - "../library/proto/area_calculator.proto"
+ pool:
+ num_workers: 1
diff --git a/example/protobuf-sync-message/provider/composer.json b/example/protobuf-sync-message/provider/composer.json
new file mode 100644
index 000000000..2eccb6faa
--- /dev/null
+++ b/example/protobuf-sync-message/provider/composer.json
@@ -0,0 +1,30 @@
+{
+ "name": "pact-foundation/example-protobuf-sync-message-provider",
+ "require": {
+ "grpc/grpc": "^1.57",
+ "spiral/roadrunner-grpc": "^2.0",
+ "tienvx/composer-downloads-plugin": "^1.2"
+ },
+ "require-dev": {
+ "ext-grpc": "*"
+ },
+ "extra": {
+ "downloads": {
+ "rr": {
+ "version": "2023.3.8",
+ "variables": {
+ "{$os}": "strtolower(PHP_OS_FAMILY)",
+ "{$architecture}": "php_uname('m') === 'x86_64' ? 'amd64' : strtolower(php_uname('m'))",
+ "{$extension}": "(PHP_OS_FAMILY === 'Windows' || (PHP_OS_FAMILY === 'Darwin' && php_uname('m') === 'x86_64')) ? 'zip' : 'tar.gz'"
+ },
+ "url": "https://github.com/roadrunner-server/roadrunner/releases/download/v{$version}/roadrunner-{$version}-{$os}-{$architecture}.{$extension}",
+ "path": "bin/roadrunner"
+ }
+ }
+ },
+ "config": {
+ "allow-plugins": {
+ "tienvx/composer-downloads-plugin": true
+ }
+ }
+}
diff --git a/example/protobuf-sync-message/provider/phpunit.xml b/example/protobuf-sync-message/provider/phpunit.xml
new file mode 100644
index 000000000..62a9eb007
--- /dev/null
+++ b/example/protobuf-sync-message/provider/phpunit.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ ./tests
+
+
+
+
+
+
diff --git a/example/protobuf-sync-message/provider/src/Service/Calculator.php b/example/protobuf-sync-message/provider/src/Service/Calculator.php
new file mode 100644
index 000000000..6ba7f07ab
--- /dev/null
+++ b/example/protobuf-sync-message/provider/src/Service/Calculator.php
@@ -0,0 +1,71 @@
+getShape()) {
+ case 'square':
+ $area = $this->calculateSquareArea($request->getSquare());
+ break;
+ case 'rectangle':
+ $area = $this->calculateRectangleArea($request->getRectangle());
+ break;
+ case 'circle':
+ $area = $this->calculateCircleArea($request->getCircle());
+ break;
+ case 'triangle':
+ $area = $this->calculateTriangleArea($request->getTriangle());
+ break;
+ case 'parallelogram':
+ $area = $this->calculateParallelogramArea($request->getParallelogram());
+ break;
+ default:
+ throw new Exception(sprintf('Shape %s is not supported', $request->getShape()));
+ }
+
+ return new AreaResponse(['value' => $area]);
+ }
+
+ private function calculateSquareArea(Square $square): float
+ {
+ return pow($square->getEdgeLength(), 2);
+ }
+
+ private function calculateRectangleArea(Rectangle $rectangle): float
+ {
+ return $rectangle->getWidth() * $rectangle->getLength();
+ }
+
+ private function calculateCircleArea(Circle $circle): float
+ {
+ return pi() * pow($circle->getRadius(), 2);
+ }
+
+ /**
+ * Use Heron's formula.
+ */
+ private function calculateTriangleArea(Triangle $triangle): float
+ {
+ $p = ($triangle->getEdgeA() + $triangle->getEdgeB() + $triangle->getEdgeC()) / 2;
+
+ return sqrt($p * ($p - $triangle->getEdgeA()) * ($p - $triangle->getEdgeB()) * ($p - $triangle->getEdgeC()));
+ }
+
+ private function calculateParallelogramArea(Parallelogram $parallelogram): float
+ {
+ return $parallelogram->getBaseLength() * $parallelogram->getHeight();
+ }
+}
diff --git a/example/protobuf-sync-message/provider/src/Service/CalculatorInterface.php b/example/protobuf-sync-message/provider/src/Service/CalculatorInterface.php
new file mode 100644
index 000000000..990b455ad
--- /dev/null
+++ b/example/protobuf-sync-message/provider/src/Service/CalculatorInterface.php
@@ -0,0 +1,15 @@
+process = new Process([__DIR__ . '/../bin/roadrunner/rr', 'serve', '-w', __DIR__ . '/..']);
+ $this->process->setTimeout(120);
+
+ $this->process->start(function (string $type, string $buffer): void {
+ echo "\n$type > $buffer";
+ });
+ $this->process->waitUntil(fn () => is_resource(@fsockopen('127.0.0.1', 9001)));
+ }
+
+ protected function tearDown(): void
+ {
+ $this->process->stop();
+ }
+
+ public function testPactVerifyConsumer(): void
+ {
+ $config = new VerifierConfig();
+ $config->getProviderInfo()
+ ->setName('protobufSyncMessageProvider')
+ ->setHost('127.0.0.1');
+ $providerTransport = new ProviderTransport();
+ $providerTransport
+ ->setProtocol('grpc')
+ ->setScheme('tcp')
+ ->setPort(9001)
+ ->setPath('/')
+ ;
+ $config->addProviderTransport($providerTransport);
+ if ($logLevel = \getenv('PACT_LOGLEVEL')) {
+ $config->setLogLevel($logLevel);
+ }
+
+ $verifier = new Verifier($config);
+ $verifier->addFile(__DIR__ . '/../../pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json');
+
+ $this->assertTrue($verifier->verify());
+ }
+}
diff --git a/example/protobuf-sync-message/provider/worker.php b/example/protobuf-sync-message/provider/worker.php
new file mode 100644
index 000000000..55c59fb38
--- /dev/null
+++ b/example/protobuf-sync-message/provider/worker.php
@@ -0,0 +1,17 @@
+ false, // optional (default: false)
+]);
+
+$server->registerService(CalculatorInterface::class, new Calculator());
+
+$server->serve(Worker::create());
diff --git a/phpunit.xml b/phpunit.xml
index e300d5a8c..b4e7bf885 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -55,6 +55,12 @@
./example/generators/provider/tests
+
+ ./example/protobuf-sync-message/consumer/tests
+
+
+ ./example/protobuf-sync-message/provider/tests
+