1<?php 2/* 3 * 4 * Copyright 2020 gRPC authors. 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 * 18 */ 19 20namespace Grpc; 21 22/** 23 * This is an experimental and incomplete implementation of gRPC server 24 * for PHP. APIs are _definitely_ going to be changed. 25 * 26 * DO NOT USE in production. 27 */ 28 29/** 30 * Class RpcServer 31 * @package Grpc 32 */ 33class RpcServer extends Server 34{ 35 protected $call; 36 // [ <String method_full_path> => [ 37 // 'service' => <Object service>, 38 // 'method' => <String method_name>, 39 // 'request' => <Object request>, 40 // ] ] 41 protected $paths_map; 42 43 private function waitForNextEvent() { 44 return $this->requestCall(); 45 } 46 47 private function loadRequest($request) { 48 if (!$this->call) { 49 throw new Exception("serverCall is not ready"); 50 } 51 $event = $this->call->startBatch([ 52 OP_RECV_MESSAGE => true, 53 ]); 54 if (!$event->message) { 55 throw new Exception("Did not receive a proper message"); 56 } 57 $request->mergeFromString($event->message); 58 return $request; 59 } 60 61 protected function sendOkResponse($response) { 62 if (!$this->call) { 63 throw new Exception("serverCall is not ready"); 64 } 65 $this->call->startBatch([ 66 OP_SEND_INITIAL_METADATA => [], 67 OP_SEND_MESSAGE => ['message' => 68 $response->serializeToString()], 69 OP_SEND_STATUS_FROM_SERVER => [ 70 'metadata' => [], 71 'code' => STATUS_OK, 72 'details' => 'OK', 73 ], 74 OP_RECV_CLOSE_ON_SERVER => true, 75 ]); 76 } 77 78 /** 79 * Add a service to this server 80 * 81 * @param Object $service The service to be added 82 */ 83 public function handle($service) { 84 $rf = new \ReflectionClass($service); 85 86 // If input does not have a parent class, which should be the 87 // generated stub, don't proceeed. This might change in the 88 // future. 89 if (!$rf->getParentClass()) return; 90 91 // The input class name needs to match the service name 92 $service_name = $rf->getName(); 93 $namespace = $rf->getParentClass()->getNamespaceName(); 94 $prefix = ""; 95 if ($namespace) { 96 $parts = explode("\\", $namespace); 97 foreach ($parts as $part) { 98 $prefix .= lcfirst($part) . "."; 99 } 100 } 101 $base_path = "/" . $prefix . $service_name; 102 103 // Right now, assume all the methods in the class are RPC method 104 // implementations. Might change in the future. 105 $methods = $rf->getMethods(); 106 foreach ($methods as $method) { 107 $method_name = $method->getName(); 108 $full_path = $base_path . "/" . ucfirst($method_name); 109 110 $method_params = $method->getParameters(); 111 // RPC should have exactly 1 request param 112 if (count($method_params) != 1) continue; 113 $request_param = $method_params[0]; 114 // Method implementation must have type hint for request param 115 if (!$request_param->getType()) continue; 116 $request_type = $request_param->getType()->getName(); 117 118 // $full_path needs to match the incoming event->method 119 // from requestCall() for us to know how to handle the request 120 $this->paths_map[$full_path] = [ 121 'service' => $service, 122 'method' => $method_name, 123 'request' => new $request_type(), 124 ]; 125 } 126 } 127 128 public function run() { 129 $this->start(); 130 while (true) { 131 // This blocks until the server receives a request 132 $event = $this->waitForNextEvent(); 133 if (!$event) { 134 throw new Exception( 135 "Unexpected error: server->waitForNextEvent delivers" 136 . " an empty event"); 137 } 138 if (!$event->call) { 139 throw new Exception( 140 "Unexpected error: server->waitForNextEvent delivers" 141 . " an event without a call"); 142 } 143 $this->call = $event->call; 144 $full_path = $event->method; 145 146 // TODO: Can send a proper UNIMPLEMENTED response in the future 147 if (!array_key_exists($full_path, $this->paths_map)) continue; 148 149 $service = $this->paths_map[$full_path]['service']; 150 $method = $this->paths_map[$full_path]['method']; 151 $request = $this->paths_map[$full_path]['request']; 152 153 $request = $this->loadRequest($request); 154 if (!$request) { 155 throw new Exception("Unexpected error: fail to parse request"); 156 } 157 if (!method_exists($service, $method)) { 158 // TODO: Can send a proper UNIMPLEMENTED response in the future 159 throw new Exception("Method not implemented"); 160 } 161 162 // Dispatch to actual server logic 163 $response = $service->$method($request); 164 $this->sendOkResponse($response); 165 $this->call = null; 166 } 167 } 168} 169