GapicClientTrait.php 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937
  1. <?php
  2. /*
  3. * Copyright 2018 Google LLC
  4. * All rights reserved.
  5. *
  6. * Redistribution and use in source and binary forms, with or without
  7. * modification, are permitted provided that the following conditions are
  8. * met:
  9. *
  10. * * Redistributions of source code must retain the above copyright
  11. * notice, this list of conditions and the following disclaimer.
  12. * * Redistributions in binary form must reproduce the above
  13. * copyright notice, this list of conditions and the following disclaimer
  14. * in the documentation and/or other materials provided with the
  15. * distribution.
  16. * * Neither the name of Google Inc. nor the names of its
  17. * contributors may be used to endorse or promote products derived from
  18. * this software without specific prior written permission.
  19. *
  20. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31. */
  32. namespace Google\ApiCore;
  33. use Google\ApiCore\LongRunning\OperationsClient;
  34. use Google\ApiCore\Middleware\CredentialsWrapperMiddleware;
  35. use Google\ApiCore\Middleware\FixedHeaderMiddleware;
  36. use Google\ApiCore\Middleware\OperationsMiddleware;
  37. use Google\ApiCore\Middleware\OptionsFilterMiddleware;
  38. use Google\ApiCore\Middleware\PagedMiddleware;
  39. use Google\ApiCore\Middleware\RetryMiddleware;
  40. use Google\ApiCore\Options\CallOptions;
  41. use Google\ApiCore\Options\ClientOptions;
  42. use Google\ApiCore\Options\TransportOptions;
  43. use Google\ApiCore\Transport\GrpcFallbackTransport;
  44. use Google\ApiCore\Transport\GrpcTransport;
  45. use Google\ApiCore\Transport\RestTransport;
  46. use Google\ApiCore\Transport\TransportInterface;
  47. use Google\Auth\FetchAuthTokenInterface;
  48. use Google\LongRunning\Operation;
  49. use Google\Protobuf\Internal\Message;
  50. use GuzzleHttp\Promise\PromiseInterface;
  51. /**
  52. * Common functions used to work with various clients.
  53. *
  54. * @internal
  55. */
  56. trait GapicClientTrait
  57. {
  58. use ClientOptionsTrait;
  59. use ValidationTrait {
  60. ValidationTrait::validate as traitValidate;
  61. }
  62. use GrpcSupportTrait;
  63. private ?TransportInterface $transport = null;
  64. private ?CredentialsWrapper $credentialsWrapper = null;
  65. /** @var RetrySettings[] $retrySettings */
  66. private array $retrySettings = [];
  67. private string $serviceName = '';
  68. private array $agentHeader = [];
  69. private array $descriptors = [];
  70. /** @var array<callable> $middlewareCallables */
  71. private array $middlewareCallables = [];
  72. private array $transportCallMethods = [
  73. Call::UNARY_CALL => 'startUnaryCall',
  74. Call::BIDI_STREAMING_CALL => 'startBidiStreamingCall',
  75. Call::CLIENT_STREAMING_CALL => 'startClientStreamingCall',
  76. Call::SERVER_STREAMING_CALL => 'startServerStreamingCall',
  77. ];
  78. private bool $backwardsCompatibilityMode;
  79. /**
  80. * Add a middleware to the call stack by providing a callable which will be
  81. * invoked at the start of each call, and will return an instance of
  82. * {@see MiddlewareInterface} when invoked.
  83. *
  84. * The callable must have the following method signature:
  85. *
  86. * callable(MiddlewareInterface): MiddlewareInterface
  87. *
  88. * An implementation may look something like this:
  89. * ```
  90. * $client->addMiddleware(function (MiddlewareInterface $handler) {
  91. * return new class ($handler) implements MiddlewareInterface {
  92. * public function __construct(private MiddlewareInterface $handler) {
  93. * }
  94. *
  95. * public function __invoke(Call $call, array $options) {
  96. * // modify call and options (pre-request)
  97. * $response = ($this->handler)($call, $options);
  98. * // modify the response (post-request)
  99. * return $response;
  100. * }
  101. * };
  102. * });
  103. * ```
  104. *
  105. * @param callable $middlewareCallable A callable which returns an instance
  106. * of {@see MiddlewareInterface} when invoked with a
  107. * MiddlewareInterface instance as its first argument.
  108. * @return void
  109. */
  110. public function addMiddleware(callable $middlewareCallable): void
  111. {
  112. $this->middlewareCallables[] = $middlewareCallable;
  113. }
  114. /**
  115. * Initiates an orderly shutdown in which preexisting calls continue but new
  116. * calls are immediately cancelled.
  117. *
  118. * @experimental
  119. */
  120. public function close()
  121. {
  122. $this->transport->close();
  123. }
  124. /**
  125. * Get the transport for the client. This method is protected to support
  126. * use by customized clients.
  127. *
  128. * @access private
  129. * @return TransportInterface
  130. */
  131. protected function getTransport()
  132. {
  133. return $this->transport;
  134. }
  135. /**
  136. * Get the credentials for the client. This method is protected to support
  137. * use by customized clients.
  138. *
  139. * @access private
  140. * @return CredentialsWrapper
  141. */
  142. protected function getCredentialsWrapper()
  143. {
  144. return $this->credentialsWrapper;
  145. }
  146. /**
  147. * Configures the GAPIC client based on an array of options.
  148. *
  149. * @param array $options {
  150. * An array of required and optional arguments.
  151. *
  152. * @type string $apiEndpoint
  153. * The address of the API remote host, for example "example.googleapis.com. May also
  154. * include the port, for example "example.googleapis.com:443"
  155. * @type bool $disableRetries
  156. * Determines whether or not retries defined by the client configuration should be
  157. * disabled. Defaults to `false`.
  158. * @type string|array $clientConfig
  159. * Client method configuration, including retry settings. This option can be either a
  160. * path to a JSON file, or a PHP array containing the decoded JSON data.
  161. * By default this settings points to the default client config file, which is provided
  162. * in the resources folder.
  163. * @type string|array|FetchAuthTokenInterface|CredentialsWrapper $credentials
  164. * The credentials to be used by the client to authorize API calls. This option
  165. * accepts either a path to a credentials file, or a decoded credentials file as a
  166. * PHP array.
  167. * *Advanced usage*: In addition, this option can also accept a pre-constructed
  168. * \Google\Auth\FetchAuthTokenInterface object or \Google\ApiCore\CredentialsWrapper
  169. * object. Note that when one of these objects are provided, any settings in
  170. * $authConfig will be ignored.
  171. * @type array $credentialsConfig
  172. * Options used to configure credentials, including auth token caching, for the client.
  173. * For a full list of supporting configuration options, see
  174. * \Google\ApiCore\CredentialsWrapper::build.
  175. * @type string|TransportInterface $transport
  176. * The transport used for executing network requests. May be either the string `rest`,
  177. * `grpc`, or 'grpc-fallback'. Defaults to `grpc` if gRPC support is detected on the system.
  178. * *Advanced usage*: Additionally, it is possible to pass in an already instantiated
  179. * TransportInterface object. Note that when this objects is provided, any settings in
  180. * $transportConfig, and any `$apiEndpoint` setting, will be ignored.
  181. * @type array $transportConfig
  182. * Configuration options that will be used to construct the transport. Options for
  183. * each supported transport type should be passed in a key for that transport. For
  184. * example:
  185. * $transportConfig = [
  186. * 'grpc' => [...],
  187. * 'rest' => [...],
  188. * 'grpc-fallback' => [...],
  189. * ];
  190. * See the GrpcTransport::build and RestTransport::build
  191. * methods for the supported options.
  192. * @type string $versionFile
  193. * The path to a file which contains the current version of the client.
  194. * @type string $descriptorsConfigPath
  195. * The path to a descriptor configuration file.
  196. * @type string $serviceName
  197. * The name of the service.
  198. * @type string $libName
  199. * The name of the client application.
  200. * @type string $libVersion
  201. * The version of the client application.
  202. * @type string $gapicVersion
  203. * The code generator version of the GAPIC library.
  204. * @type callable $clientCertSource
  205. * A callable which returns the client cert as a string.
  206. * }
  207. * @throws ValidationException
  208. */
  209. private function setClientOptions(array $options)
  210. {
  211. // serviceAddress is now deprecated and acts as an alias for apiEndpoint
  212. if (isset($options['serviceAddress'])) {
  213. $options['apiEndpoint'] = $this->pluck('serviceAddress', $options, false);
  214. }
  215. $this->validateNotNull($options, [
  216. 'apiEndpoint',
  217. 'serviceName',
  218. 'descriptorsConfigPath',
  219. 'clientConfig',
  220. 'disableRetries',
  221. 'credentialsConfig',
  222. 'transportConfig',
  223. ]);
  224. $this->traitValidate($options, [
  225. 'credentials',
  226. 'transport',
  227. 'gapicVersion',
  228. 'libName',
  229. 'libVersion',
  230. ]);
  231. if ($this->isBackwardsCompatibilityMode()) {
  232. if (is_string($options['clientConfig'])) {
  233. // perform validation for V1 surfaces which is done in the
  234. // ClientOptions class for v2 surfaces.
  235. $options['clientConfig'] = json_decode(
  236. file_get_contents($options['clientConfig']),
  237. true
  238. );
  239. self::validateFileExists($options['descriptorsConfigPath']);
  240. }
  241. } else {
  242. // cast to ClientOptions for new surfaces only
  243. $options = new ClientOptions($options);
  244. }
  245. $this->serviceName = $options['serviceName'];
  246. $this->retrySettings = RetrySettings::load(
  247. $this->serviceName,
  248. $options['clientConfig'],
  249. $options['disableRetries']
  250. );
  251. $headerInfo = [
  252. 'libName' => $options['libName'],
  253. 'libVersion' => $options['libVersion'],
  254. 'gapicVersion' => $options['gapicVersion'],
  255. ];
  256. // Edge case: If the client has the gRPC extension installed, but is
  257. // a REST-only library, then the grpcVersion header should not be set.
  258. if ($this->transport instanceof GrpcTransport) {
  259. $headerInfo['grpcVersion'] = phpversion('grpc');
  260. } elseif ($this->transport instanceof RestTransport
  261. || $this->transport instanceof GrpcFallbackTransport) {
  262. $headerInfo['restVersion'] = Version::getApiCoreVersion();
  263. }
  264. $this->agentHeader = AgentHeader::buildAgentHeader($headerInfo);
  265. // Set "client_library_name" depending on client library surface being used
  266. $userAgentHeader = sprintf(
  267. 'gcloud-php-%s/%s',
  268. $this->isBackwardsCompatibilityMode() ? 'legacy' : 'new',
  269. $options['gapicVersion']
  270. );
  271. $this->agentHeader['User-Agent'] = [$userAgentHeader];
  272. self::validateFileExists($options['descriptorsConfigPath']);
  273. $descriptors = require($options['descriptorsConfigPath']);
  274. $this->descriptors = $descriptors['interfaces'][$this->serviceName];
  275. $this->credentialsWrapper = $this->createCredentialsWrapper(
  276. $options['credentials'],
  277. $options['credentialsConfig'],
  278. $options['universeDomain']
  279. );
  280. $transport = $options['transport'] ?: self::defaultTransport();
  281. $this->transport = $transport instanceof TransportInterface
  282. ? $transport
  283. : $this->createTransport(
  284. $options['apiEndpoint'],
  285. $transport,
  286. $options['transportConfig'],
  287. $options['clientCertSource']
  288. );
  289. }
  290. /**
  291. * @param string $apiEndpoint
  292. * @param string $transport
  293. * @param TransportOptions|array $transportConfig
  294. * @param callable $clientCertSource
  295. * @return TransportInterface
  296. * @throws ValidationException
  297. */
  298. private function createTransport(
  299. string $apiEndpoint,
  300. $transport,
  301. $transportConfig,
  302. callable $clientCertSource = null
  303. ) {
  304. if (!is_string($transport)) {
  305. throw new ValidationException(
  306. "'transport' must be a string, instead got:" .
  307. print_r($transport, true)
  308. );
  309. }
  310. $supportedTransports = self::supportedTransports();
  311. if (!in_array($transport, $supportedTransports)) {
  312. throw new ValidationException(sprintf(
  313. 'Unexpected transport option "%s". Supported transports: %s',
  314. $transport,
  315. implode(', ', $supportedTransports)
  316. ));
  317. }
  318. $configForSpecifiedTransport = $transportConfig[$transport] ?? [];
  319. if (is_array($configForSpecifiedTransport)) {
  320. $configForSpecifiedTransport['clientCertSource'] = $clientCertSource;
  321. } else {
  322. $configForSpecifiedTransport->setClientCertSource($clientCertSource);
  323. $configForSpecifiedTransport = $configForSpecifiedTransport->toArray();
  324. }
  325. switch ($transport) {
  326. case 'grpc':
  327. // Setting the user agent for gRPC requires special handling
  328. if (isset($this->agentHeader['User-Agent'])) {
  329. if ($configForSpecifiedTransport['stubOpts']['grpc.primary_user_agent'] ??= '') {
  330. $configForSpecifiedTransport['stubOpts']['grpc.primary_user_agent'] .= ' ';
  331. }
  332. $configForSpecifiedTransport['stubOpts']['grpc.primary_user_agent'] .=
  333. $this->agentHeader['User-Agent'][0];
  334. }
  335. return GrpcTransport::build($apiEndpoint, $configForSpecifiedTransport);
  336. case 'grpc-fallback':
  337. return GrpcFallbackTransport::build($apiEndpoint, $configForSpecifiedTransport);
  338. case 'rest':
  339. if (!isset($configForSpecifiedTransport['restClientConfigPath'])) {
  340. throw new ValidationException(
  341. "The 'restClientConfigPath' config is required for 'rest' transport."
  342. );
  343. }
  344. $restConfigPath = $configForSpecifiedTransport['restClientConfigPath'];
  345. return RestTransport::build($apiEndpoint, $restConfigPath, $configForSpecifiedTransport);
  346. default:
  347. throw new ValidationException(
  348. "Unexpected 'transport' option: $transport. " .
  349. "Supported values: ['grpc', 'rest', 'grpc-fallback']"
  350. );
  351. }
  352. }
  353. /**
  354. * @param array $options
  355. * @return OperationsClient
  356. */
  357. private function createOperationsClient(array $options)
  358. {
  359. $this->pluckArray([
  360. 'serviceName',
  361. 'clientConfig',
  362. 'descriptorsConfigPath',
  363. ], $options);
  364. // User-supplied operations client
  365. if ($operationsClient = $this->pluck('operationsClient', $options, false)) {
  366. return $operationsClient;
  367. }
  368. // operationsClientClass option
  369. $operationsClientClass = $this->pluck('operationsClientClass', $options, false)
  370. ?: OperationsCLient::class;
  371. return new $operationsClientClass($options);
  372. }
  373. /**
  374. * @return string
  375. */
  376. private static function defaultTransport()
  377. {
  378. return self::getGrpcDependencyStatus()
  379. ? 'grpc'
  380. : 'rest';
  381. }
  382. private function validateCallConfig(string $methodName)
  383. {
  384. // Ensure a method descriptor exists for the target method.
  385. if (!isset($this->descriptors[$methodName])) {
  386. throw new ValidationException("Requested method '$methodName' does not exist in descriptor configuration.");
  387. }
  388. $methodDescriptors = $this->descriptors[$methodName];
  389. // Ensure required descriptor configuration exists.
  390. if (!isset($methodDescriptors['callType'])) {
  391. throw new ValidationException("Requested method '$methodName' does not have a callType " .
  392. 'in descriptor configuration.');
  393. }
  394. $callType = $methodDescriptors['callType'];
  395. // Validate various callType specific configurations.
  396. if ($callType == Call::LONGRUNNING_CALL) {
  397. if (!isset($methodDescriptors['longRunning'])) {
  398. throw new ValidationException("Requested method '$methodName' does not have a longRunning config " .
  399. 'in descriptor configuration.');
  400. }
  401. // @TODO: check if the client implements `OperationsClientInterface` instead
  402. if (!method_exists($this, 'getOperationsClient')) {
  403. throw new ValidationException('Client missing required getOperationsClient ' .
  404. "for longrunning call '$methodName'");
  405. }
  406. } elseif ($callType == Call::PAGINATED_CALL) {
  407. if (!isset($methodDescriptors['pageStreaming'])) {
  408. throw new ValidationException("Requested method '$methodName' with callType PAGINATED_CALL does not " .
  409. 'have a pageStreaming in descriptor configuration.');
  410. }
  411. }
  412. // LRO are either Standard LRO response type or custom, which are handled by
  413. // startOperationCall, so no need to validate responseType for those callType.
  414. if ($callType != Call::LONGRUNNING_CALL) {
  415. if (!isset($methodDescriptors['responseType'])) {
  416. throw new ValidationException("Requested method '$methodName' does not have a responseType " .
  417. 'in descriptor configuration.');
  418. }
  419. }
  420. return $methodDescriptors;
  421. }
  422. /**
  423. * @param string $methodName
  424. * @param Message $request
  425. * @param array $optionalArgs {
  426. * Call Options
  427. *
  428. * @type array $headers [optional] key-value array containing headers
  429. * @type int $timeoutMillis [optional] the timeout in milliseconds for the call
  430. * @type array $transportOptions [optional] transport-specific call options
  431. * @type RetrySettings|array $retrySettings [optional] A retry settings override for the call.
  432. * }
  433. *
  434. * @experimental
  435. *
  436. * @return PromiseInterface
  437. */
  438. private function startAsyncCall(
  439. string $methodName,
  440. Message $request,
  441. array $optionalArgs = []
  442. ) {
  443. // Convert method name to the UpperCamelCase of RPC names from lowerCamelCase of GAPIC method names
  444. // in order to find the method in the descriptor config.
  445. $methodName = ucfirst($methodName);
  446. $methodDescriptors = $this->validateCallConfig($methodName);
  447. $callType = $methodDescriptors['callType'];
  448. switch ($callType) {
  449. case Call::PAGINATED_CALL:
  450. return $this->getPagedListResponseAsync(
  451. $methodName,
  452. $optionalArgs,
  453. $methodDescriptors['responseType'],
  454. $request,
  455. $methodDescriptors['interfaceOverride'] ?? $this->serviceName
  456. );
  457. case Call::SERVER_STREAMING_CALL:
  458. case Call::CLIENT_STREAMING_CALL:
  459. case Call::BIDI_STREAMING_CALL:
  460. throw new ValidationException("Call type '$callType' of requested method " .
  461. "'$methodName' is not supported for async execution.");
  462. }
  463. return $this->startApiCall($methodName, $request, $optionalArgs);
  464. }
  465. /**
  466. * @param string $methodName
  467. * @param Message $request
  468. * @param array $optionalArgs {
  469. * Call Options
  470. *
  471. * @type array $headers [optional] key-value array containing headers
  472. * @type int $timeoutMillis [optional] the timeout in milliseconds for the call
  473. * @type array $transportOptions [optional] transport-specific call options
  474. * @type RetrySettings|array $retrySettings [optional] A retry settings
  475. * override for the call.
  476. * }
  477. *
  478. * @experimental
  479. *
  480. * @return PromiseInterface|PagedListResponse|BidiStream|ClientStream|ServerStream
  481. */
  482. private function startApiCall(
  483. string $methodName,
  484. Message $request = null,
  485. array $optionalArgs = []
  486. ) {
  487. $methodDescriptors =$this->validateCallConfig($methodName);
  488. $callType = $methodDescriptors['callType'];
  489. // Prepare request-based headers, merge with user-provided headers,
  490. // which take precedence.
  491. $headerParams = $methodDescriptors['headerParams'] ?? [];
  492. $requestHeaders = $this->buildRequestParamsHeader($headerParams, $request);
  493. $optionalArgs['headers'] = array_merge($requestHeaders, $optionalArgs['headers'] ?? []);
  494. // Default the interface name, if not set, to the client's protobuf service name.
  495. $interfaceName = $methodDescriptors['interfaceOverride'] ?? $this->serviceName;
  496. // Handle call based on call type configured in the method descriptor config.
  497. if ($callType == Call::LONGRUNNING_CALL) {
  498. return $this->startOperationsCall(
  499. $methodName,
  500. $optionalArgs,
  501. $request,
  502. $this->getOperationsClient(),
  503. $interfaceName,
  504. // Custom operations will define their own operation response type, whereas standard
  505. // LRO defaults to the same type.
  506. $methodDescriptors['responseType'] ?? null
  507. );
  508. }
  509. // Fully-qualified name of the response message PHP class.
  510. $decodeType = $methodDescriptors['responseType'];
  511. if ($callType == Call::PAGINATED_CALL) {
  512. return $this->getPagedListResponse($methodName, $optionalArgs, $decodeType, $request, $interfaceName);
  513. }
  514. // Unary, and all Streaming types handled by startCall.
  515. return $this->startCall($methodName, $decodeType, $optionalArgs, $request, $callType, $interfaceName);
  516. }
  517. /**
  518. * @param string $methodName
  519. * @param string $decodeType
  520. * @param array $optionalArgs {
  521. * Call Options
  522. *
  523. * @type array $headers [optional] key-value array containing headers
  524. * @type int $timeoutMillis [optional] the timeout in milliseconds for the call
  525. * @type array $transportOptions [optional] transport-specific call options
  526. * @type RetrySettings|array $retrySettings [optional] A retry settings
  527. * override for the call.
  528. * }
  529. * @param Message $request
  530. * @param int $callType
  531. * @param string $interfaceName
  532. *
  533. * @return PromiseInterface|BidiStream|ClientStream|ServerStream
  534. */
  535. private function startCall(
  536. string $methodName,
  537. string $decodeType,
  538. array $optionalArgs = [],
  539. Message $request = null,
  540. int $callType = Call::UNARY_CALL,
  541. string $interfaceName = null
  542. ) {
  543. $optionalArgs = $this->configureCallOptions($optionalArgs);
  544. $callStack = $this->createCallStack(
  545. $this->configureCallConstructionOptions($methodName, $optionalArgs)
  546. );
  547. $descriptor = $this->descriptors[$methodName]['grpcStreaming'] ?? null;
  548. $call = new Call(
  549. $this->buildMethod($interfaceName, $methodName),
  550. $decodeType,
  551. $request,
  552. $descriptor,
  553. $callType
  554. );
  555. switch ($callType) {
  556. case Call::UNARY_CALL:
  557. $this->modifyUnaryCallable($callStack);
  558. break;
  559. case Call::BIDI_STREAMING_CALL:
  560. case Call::CLIENT_STREAMING_CALL:
  561. case Call::SERVER_STREAMING_CALL:
  562. $this->modifyStreamingCallable($callStack);
  563. break;
  564. }
  565. return $callStack($call, $optionalArgs + array_filter([
  566. 'audience' => self::getDefaultAudience()
  567. ]));
  568. }
  569. /**
  570. * @param array $callConstructionOptions {
  571. * Call Construction Options
  572. *
  573. * @type RetrySettings $retrySettings [optional] A retry settings override
  574. * For the call.
  575. * }
  576. *
  577. * @return callable
  578. */
  579. private function createCallStack(array $callConstructionOptions)
  580. {
  581. $quotaProject = $this->credentialsWrapper->getQuotaProject();
  582. $fixedHeaders = $this->agentHeader;
  583. if ($quotaProject) {
  584. $fixedHeaders += [
  585. 'X-Goog-User-Project' => [$quotaProject]
  586. ];
  587. }
  588. $callStack = function (Call $call, array $options) {
  589. $startCallMethod = $this->transportCallMethods[$call->getCallType()];
  590. return $this->transport->$startCallMethod($call, $options);
  591. };
  592. $callStack = new CredentialsWrapperMiddleware($callStack, $this->credentialsWrapper);
  593. $callStack = new FixedHeaderMiddleware($callStack, $fixedHeaders, true);
  594. $callStack = new RetryMiddleware($callStack, $callConstructionOptions['retrySettings']);
  595. $callStack = new OptionsFilterMiddleware($callStack, [
  596. 'headers',
  597. 'timeoutMillis',
  598. 'transportOptions',
  599. 'metadataCallback',
  600. 'audience',
  601. 'metadataReturnType'
  602. ]);
  603. foreach (\array_reverse($this->middlewareCallables) as $fn) {
  604. /** @var MiddlewareInterface $callStack */
  605. $callStack = $fn($callStack);
  606. }
  607. return $callStack;
  608. }
  609. /**
  610. * @param string $methodName
  611. * @param array $optionalArgs {
  612. * Optional arguments
  613. *
  614. * @type RetrySettings|array $retrySettings [optional] A retry settings
  615. * override for the call.
  616. * }
  617. *
  618. * @return array
  619. */
  620. private function configureCallConstructionOptions(string $methodName, array $optionalArgs)
  621. {
  622. $retrySettings = $this->retrySettings[$methodName];
  623. // Allow for retry settings to be changed at call time
  624. if (isset($optionalArgs['retrySettings'])) {
  625. if ($optionalArgs['retrySettings'] instanceof RetrySettings) {
  626. $retrySettings = $optionalArgs['retrySettings'];
  627. } else {
  628. $retrySettings = $retrySettings->with(
  629. $optionalArgs['retrySettings']
  630. );
  631. }
  632. }
  633. return [
  634. 'retrySettings' => $retrySettings,
  635. ];
  636. }
  637. /**
  638. * @return array
  639. */
  640. private function configureCallOptions(array $optionalArgs): array
  641. {
  642. if ($this->isBackwardsCompatibilityMode()) {
  643. return $optionalArgs;
  644. }
  645. // cast to CallOptions for new surfaces only
  646. return (new CallOptions($optionalArgs))->toArray();
  647. }
  648. /**
  649. * @param string $methodName
  650. * @param array $optionalArgs {
  651. * Call Options
  652. *
  653. * @type array $headers [optional] key-value array containing headers
  654. * @type int $timeoutMillis [optional] the timeout in milliseconds for the call
  655. * @type array $transportOptions [optional] transport-specific call options
  656. * }
  657. * @param Message $request
  658. * @param OperationsClient|object $client
  659. * @param string $interfaceName
  660. * @param string $operationClass If provided, will be used instead of the default
  661. * operation response class of {@see \Google\LongRunning\Operation}.
  662. *
  663. * @return PromiseInterface
  664. */
  665. private function startOperationsCall(
  666. string $methodName,
  667. array $optionalArgs,
  668. Message $request,
  669. $client,
  670. string $interfaceName = null,
  671. string $operationClass = null
  672. ) {
  673. $optionalArgs = $this->configureCallOptions($optionalArgs);
  674. $callStack = $this->createCallStack(
  675. $this->configureCallConstructionOptions($methodName, $optionalArgs)
  676. );
  677. $descriptor = $this->descriptors[$methodName]['longRunning'];
  678. $metadataReturnType = null;
  679. // Call the methods supplied in "additionalArgumentMethods" on the request Message object
  680. // to build the "additionalOperationArguments" option for the operation response.
  681. if (isset($descriptor['additionalArgumentMethods'])) {
  682. $additionalArgs = [];
  683. foreach ($descriptor['additionalArgumentMethods'] as $additionalArgsMethodName) {
  684. $additionalArgs[] = $request->$additionalArgsMethodName();
  685. }
  686. $descriptor['additionalOperationArguments'] = $additionalArgs;
  687. unset($descriptor['additionalArgumentMethods']);
  688. }
  689. if (isset($descriptor['metadataReturnType'])) {
  690. $metadataReturnType = $descriptor['metadataReturnType'];
  691. }
  692. $callStack = new OperationsMiddleware($callStack, $client, $descriptor);
  693. $call = new Call(
  694. $this->buildMethod($interfaceName, $methodName),
  695. $operationClass ?: Operation::class,
  696. $request,
  697. [],
  698. Call::UNARY_CALL
  699. );
  700. $this->modifyUnaryCallable($callStack);
  701. return $callStack($call, $optionalArgs + array_filter([
  702. 'metadataReturnType' => $metadataReturnType,
  703. 'audience' => self::getDefaultAudience()
  704. ]));
  705. }
  706. /**
  707. * @param string $methodName
  708. * @param array $optionalArgs
  709. * @param string $decodeType
  710. * @param Message $request
  711. * @param string $interfaceName
  712. *
  713. * @return PagedListResponse
  714. */
  715. private function getPagedListResponse(
  716. string $methodName,
  717. array $optionalArgs,
  718. string $decodeType,
  719. Message $request,
  720. string $interfaceName = null
  721. ) {
  722. return $this->getPagedListResponseAsync(
  723. $methodName,
  724. $optionalArgs,
  725. $decodeType,
  726. $request,
  727. $interfaceName
  728. )->wait();
  729. }
  730. /**
  731. * @param string $methodName
  732. * @param array $optionalArgs
  733. * @param string $decodeType
  734. * @param Message $request
  735. * @param string $interfaceName
  736. *
  737. * @return PromiseInterface
  738. */
  739. private function getPagedListResponseAsync(
  740. string $methodName,
  741. array $optionalArgs,
  742. string $decodeType,
  743. Message $request,
  744. string $interfaceName = null
  745. ) {
  746. $optionalArgs = $this->configureCallOptions($optionalArgs);
  747. $callStack = $this->createCallStack(
  748. $this->configureCallConstructionOptions($methodName, $optionalArgs)
  749. );
  750. $descriptor = new PageStreamingDescriptor(
  751. $this->descriptors[$methodName]['pageStreaming']
  752. );
  753. $callStack = new PagedMiddleware($callStack, $descriptor);
  754. $call = new Call(
  755. $this->buildMethod($interfaceName, $methodName),
  756. $decodeType,
  757. $request,
  758. [],
  759. Call::UNARY_CALL
  760. );
  761. $this->modifyUnaryCallable($callStack);
  762. return $callStack($call, $optionalArgs + array_filter([
  763. 'audience' => self::getDefaultAudience()
  764. ]));
  765. }
  766. /**
  767. * @param string $interfaceName
  768. * @param string $methodName
  769. *
  770. * @return string
  771. */
  772. private function buildMethod(string $interfaceName = null, string $methodName = null)
  773. {
  774. return sprintf(
  775. '%s/%s',
  776. $interfaceName ?: $this->serviceName,
  777. $methodName
  778. );
  779. }
  780. /**
  781. * @param array $headerParams
  782. * @param Message|null $request
  783. *
  784. * @return array
  785. */
  786. private function buildRequestParamsHeader(array $headerParams, Message $request = null)
  787. {
  788. $headers = [];
  789. // No request message means no request-based headers.
  790. if (!$request) {
  791. return $headers;
  792. }
  793. foreach ($headerParams as $headerParam) {
  794. $msg = $request;
  795. $value = null;
  796. foreach ($headerParam['fieldAccessors'] as $accessor) {
  797. $value = $msg->$accessor();
  798. // In case the field in question is nested in another message,
  799. // skip the header param when the nested message field is unset.
  800. $msg = $value;
  801. if (is_null($msg)) {
  802. break;
  803. }
  804. }
  805. $keyName = $headerParam['keyName'];
  806. // If there are value pattern matchers configured and the target
  807. // field was set, evaluate the matchers in the order that they were
  808. // annotated in with last one matching wins.
  809. $original = $value;
  810. $matchers = isset($headerParam['matchers']) && !is_null($value) ?
  811. $headerParam['matchers'] :
  812. [];
  813. foreach ($matchers as $matcher) {
  814. $matches = [];
  815. if (preg_match($matcher, $original, $matches)) {
  816. $value = $matches[$keyName];
  817. }
  818. }
  819. // If there are no matches or the target field was unset, skip this
  820. // header param.
  821. if (!$value) {
  822. continue;
  823. }
  824. $headers[$keyName] = $value;
  825. }
  826. $requestParams = new RequestParamsHeaderDescriptor($headers);
  827. return $requestParams->getHeader();
  828. }
  829. /**
  830. * The SERVICE_ADDRESS constant is set by GAPIC clients
  831. */
  832. private static function getDefaultAudience()
  833. {
  834. if (!defined('self::SERVICE_ADDRESS')) {
  835. return null;
  836. }
  837. return 'https://' . self::SERVICE_ADDRESS . '/'; // @phpstan-ignore-line
  838. }
  839. /**
  840. * Modify the unary callable.
  841. *
  842. * @param callable $callable
  843. * @access private
  844. */
  845. protected function modifyUnaryCallable(callable &$callable)
  846. {
  847. // Do nothing - this method exists to allow callable modification by partial veneers.
  848. }
  849. /**
  850. * Modify the streaming callable.
  851. *
  852. * @param callable $callable
  853. * @access private
  854. */
  855. protected function modifyStreamingCallable(callable &$callable)
  856. {
  857. // Do nothing - this method exists to allow callable modification by partial veneers.
  858. }
  859. /**
  860. * @internal
  861. */
  862. private function isBackwardsCompatibilityMode(): bool
  863. {
  864. return $this->backwardsCompatibilityMode
  865. ?? $this->backwardsCompatibilityMode = substr(__CLASS__, -11) === 'GapicClient';
  866. }
  867. }