Support PHP generic services (#3269)

* Add php_generic_services option

* Generate PHP generic services

* Respect namespaces for generated PHP services

* Test PHP generated services

* Rename PHP generator service method doc comment function

* Correct phpdoc service method case

* Test namespaced PHP generic services

* Always use the FQCN for PHP generic service input/output

* Add generated_service_test to php test.sh

* Add php service test protos to CI

* Add php service files to php_EXTRA_DIST

* Use Interface suffix for php generic services
diff --git a/php/phpunit.xml b/php/phpunit.xml
index 7cb1e2a..637467b 100644
--- a/php/phpunit.xml
+++ b/php/phpunit.xml
@@ -10,6 +10,7 @@
       <file>tests/generated_phpdoc_test.php</file>
       <file>tests/map_field_test.php</file>
       <file>tests/well_known_test.php</file>
+      <file>tests/generated_service_test.php</file>
     </testsuite>
   </testsuites>
 </phpunit>
diff --git a/php/src/GPBMetadata/Google/Protobuf/Internal/Descriptor.php b/php/src/GPBMetadata/Google/Protobuf/Internal/Descriptor.php
index 636a0ad..1e3c745 100644
--- a/php/src/GPBMetadata/Google/Protobuf/Internal/Descriptor.php
+++ b/php/src/GPBMetadata/Google/Protobuf/Internal/Descriptor.php
@@ -139,6 +139,7 @@
             ->optional('cc_generic_services', \Google\Protobuf\Internal\GPBType::BOOL, 16)
             ->optional('java_generic_services', \Google\Protobuf\Internal\GPBType::BOOL, 17)
             ->optional('py_generic_services', \Google\Protobuf\Internal\GPBType::BOOL, 18)
+            ->optional('php_generic_services', \Google\Protobuf\Internal\GPBType::BOOL, 19)
             ->optional('deprecated', \Google\Protobuf\Internal\GPBType::BOOL, 23)
             ->optional('cc_enable_arenas', \Google\Protobuf\Internal\GPBType::BOOL, 31)
             ->optional('objc_class_prefix', \Google\Protobuf\Internal\GPBType::STRING, 36)
diff --git a/php/src/Google/Protobuf/Internal/FileOptions.php b/php/src/Google/Protobuf/Internal/FileOptions.php
index 200ee9d..c7972ca 100644
--- a/php/src/Google/Protobuf/Internal/FileOptions.php
+++ b/php/src/Google/Protobuf/Internal/FileOptions.php
@@ -110,6 +110,11 @@
     private $py_generic_services = false;
     private $has_py_generic_services = false;
     /**
+     * Generated from protobuf field <code>optional bool php_generic_services = 19 [default = false];</code>
+     */
+    private $php_generic_services = false;
+    private $has_php_generic_services = false;
+    /**
      * Is this file deprecated?
      * Depending on the target platform, this can emit Deprecated annotations
      * for everything in the file, or it will be completely ignored; in the very
@@ -549,6 +554,34 @@
     }
 
     /**
+     * Generated from protobuf field <code>optional bool php_generic_services = 19 [default = false];</code>
+     * @return bool
+     */
+    public function getPhpGenericServices()
+    {
+        return $this->php_generic_services;
+    }
+
+    /**
+     * Generated from protobuf field <code>optional bool php_generic_services = 19 [default = false];</code>
+     * @param bool $var
+     * @return $this
+     */
+    public function setPhpGenericServices($var)
+    {
+        GPBUtil::checkBool($var);
+        $this->php_generic_services = $var;
+        $this->has_php_generic_services = true;
+
+        return $this;
+    }
+
+    public function hasPhpGenericServices()
+    {
+        return $this->has_php_generic_services;
+    }
+
+    /**
      * Is this file deprecated?
      * Depending on the target platform, this can emit Deprecated annotations
      * for everything in the file, or it will be completely ignored; in the very
diff --git a/php/tests/generated_service_test.php b/php/tests/generated_service_test.php
new file mode 100644
index 0000000..5407db9
--- /dev/null
+++ b/php/tests/generated_service_test.php
@@ -0,0 +1,110 @@
+<?php
+
+require_once('test_base.php');
+require_once('test_util.php');
+
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\MapField;
+use Google\Protobuf\Internal\GPBType;
+use Foo\Greeter;
+use Foo\HelloRequest;
+use Foo\HelloReply;
+
+class GeneratedServiceTest extends TestBase
+{
+    /**
+     * @var \ReflectionClass
+     */
+    private $serviceClass;
+
+    /**
+     * @var \ReflectionClass
+     */
+    private $namespacedServiceClass;
+
+    /**
+     * @var array
+     */
+    private $methodNames = [
+        'sayHello',
+        'sayHelloAgain'
+    ];
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        $this->serviceClass = new ReflectionClass('Foo\GreeterInterface');
+
+        $this->namespacedServiceClass = new ReflectionClass('Bar\OtherGreeterInterface');
+    }
+
+    public function testIsInterface()
+    {
+        $this->assertTrue($this->serviceClass->isInterface());
+    }
+
+    public function testPhpDocForClass()
+    {
+        $this->assertContains('foo.Greeter', $this->serviceClass->getDocComment());
+    }
+
+    public function testPhpDocForNamespacedClass()
+    {
+        $this->assertContains('foo.OtherGreeter', $this->namespacedServiceClass->getDocComment());
+    }
+
+    public function testServiceMethodsAreGenerated()
+    {
+        $this->assertCount(count($this->methodNames), $this->serviceClass->getMethods());
+        foreach ($this->methodNames as $methodName) {
+            $this->assertTrue($this->serviceClass->hasMethod($methodName));
+        }
+    }
+
+    public function testPhpDocForServiceMethod()
+    {
+        foreach ($this->methodNames as $methodName) {
+            $docComment = $this->serviceClass->getMethod($methodName)->getDocComment();
+            $this->assertContains($methodName, $docComment);
+            $this->assertContains('@param \Foo\HelloRequest $request', $docComment);
+            $this->assertContains('@return \Foo\HelloReply', $docComment);
+        }
+    }
+
+    public function testPhpDocForServiceMethodInNamespacedClass()
+    {
+        foreach ($this->methodNames as $methodName) {
+            $docComment = $this->namespacedServiceClass->getMethod($methodName)->getDocComment();
+            $this->assertContains($methodName, $docComment);
+            $this->assertContains('@param \Foo\HelloRequest $request', $docComment);
+            $this->assertContains('@return \Foo\HelloReply', $docComment);
+        }
+    }
+
+    public function testParamForServiceMethod()
+    {
+        foreach ($this->methodNames as $methodName) {
+            $method = $this->serviceClass->getMethod($methodName);
+            $this->assertCount(1, $method->getParameters());
+            $param = $method->getParameters()[0];
+            $this->assertFalse($param->isOptional());
+            $this->assertSame('request', $param->getName());
+            // ReflectionParameter::getType only exists in PHP 7+, so get the type from __toString
+            $this->assertContains('Foo\HelloRequest $request', (string) $param);
+        }
+    }
+
+    public function testParamForServiceMethodInNamespacedClass()
+    {
+        foreach ($this->methodNames as $methodName) {
+            $method = $this->serviceClass->getMethod($methodName);
+            $this->assertCount(1, $method->getParameters());
+            $param = $method->getParameters()[0];
+            $this->assertFalse($param->isOptional());
+            $this->assertSame('request', $param->getName());
+            // ReflectionParameter::getType only exists in PHP 7+, so get the type from __toString
+            $this->assertContains('Foo\HelloRequest $request', (string) $param);
+        }
+    }
+}
diff --git a/php/tests/proto/test_service.proto b/php/tests/proto/test_service.proto
new file mode 100644
index 0000000..a03dbc4
--- /dev/null
+++ b/php/tests/proto/test_service.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+package foo;
+
+option php_generic_services = true;
+
+service Greeter {
+  rpc SayHello (HelloRequest) returns (HelloReply) {}
+  rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
+}
+
+message HelloRequest {
+  string name = 1;
+}
+
+message HelloReply {
+  string message = 1;
+}
diff --git a/php/tests/proto/test_service_namespace.proto b/php/tests/proto/test_service_namespace.proto
new file mode 100644
index 0000000..719aa48
--- /dev/null
+++ b/php/tests/proto/test_service_namespace.proto
@@ -0,0 +1,13 @@
+syntax = "proto3";
+
+import "proto/test_service.proto";
+
+package foo;
+
+option php_generic_services = true;
+option php_namespace = "Bar";
+
+service OtherGreeter {
+  rpc SayHello (HelloRequest) returns (HelloReply) {}
+  rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
+}
diff --git a/php/tests/test.sh b/php/tests/test.sh
index 6c6edd5..b640c14 100755
--- a/php/tests/test.sh
+++ b/php/tests/test.sh
@@ -8,7 +8,7 @@
 phpize && ./configure CFLAGS='-g -O0' && make
 popd
 
-tests=( array_test.php encode_decode_test.php generated_class_test.php generated_phpdoc_test.php map_field_test.php well_known_test.php )
+tests=( array_test.php encode_decode_test.php generated_class_test.php generated_phpdoc_test.php map_field_test.php well_known_test.php generated_service_test.php )
 
 for t in "${tests[@]}"
 do