CHANGELOG.md 0000644 00000021452 15174273622 0006372 0 ustar 00 # Changelog
## [1.26.1](https://github.com/googleapis/gax-php/compare/v1.26.0...v1.26.1) (2024-01-04)
### Bug Fixes
* Widen google/longrunning version ([#511](https://github.com/googleapis/gax-php/issues/511)) ([b93096d](https://github.com/googleapis/gax-php/commit/b93096d0e10bde14c50480ea9f0423c292fbd5a6))
## [1.26.0](https://github.com/googleapis/gax-php/compare/v1.25.0...v1.26.0) (2024-01-03)
### Features
* Add support for universe domain ([#502](https://github.com/googleapis/gax-php/issues/502)) ([5a26fac](https://github.com/googleapis/gax-php/commit/5a26facad5c2e5c30945987c422bb78a3fffb9b1))
* Interface and methods for middleware stack ([#473](https://github.com/googleapis/gax-php/issues/473)) ([766da7b](https://github.com/googleapis/gax-php/commit/766da7b369409ec1b29376b533e7f22ee7f745f4))
### Bug Fixes
* Accept throwable for retry settings ([#509](https://github.com/googleapis/gax-php/issues/509)) ([5af9c3c](https://github.com/googleapis/gax-php/commit/5af9c3c650419c8f1a590783e954cd11dc1f0d56))
## [1.25.0](https://github.com/googleapis/gax-php/compare/v1.24.0...v1.25.0) (2023-11-02)
### Features
* Add custom retries ([#489](https://github.com/googleapis/gax-php/issues/489)) ([ef0789b](https://github.com/googleapis/gax-php/commit/ef0789b73ef28d79a08c354d1361a9ccc6206088))
## [1.24.0](https://github.com/googleapis/gax-php/compare/v1.23.0...v1.24.0) (2023-10-10)
### Features
* Ensure NewClientSurface works for consoldiated v2 clients ([#493](https://github.com/googleapis/gax-php/issues/493)) ([cb8706e](https://github.com/googleapis/gax-php/commit/cb8706ef9211a1e43f733d2c8f272a330c2fa792))
## [1.23.0](https://github.com/googleapis/gax-php/compare/v1.22.1...v1.23.0) (2023-09-14)
### Features
* Typesafety for new surface client options ([#450](https://github.com/googleapis/gax-php/issues/450)) ([21550c5](https://github.com/googleapis/gax-php/commit/21550c5bf07f178f2043b0630f3ac34fcc3a05e0))
## [1.22.1](https://github.com/googleapis/gax-php/compare/v1.22.0...v1.22.1) (2023-08-04)
### Bug Fixes
* Deprecation notice while GapicClientTrait->setClientOptions ([#483](https://github.com/googleapis/gax-php/issues/483)) ([1c66d34](https://github.com/googleapis/gax-php/commit/1c66d3445dca4d43831a2f4e26e59b9bd1cb76dd))
## [1.22.0](https://github.com/googleapis/gax-php/compare/v1.21.1...v1.22.0) (2023-07-31)
### Features
* Sets api headers for "gcloud-php-new" and "gcloud-php-legacy" surface versions ([#470](https://github.com/googleapis/gax-php/issues/470)) ([2d8ccff](https://github.com/googleapis/gax-php/commit/2d8ccff419a076ee2fe9d3dc7ecd5509c74afb4c))
## [1.21.1](https://github.com/googleapis/gax-php/compare/v1.21.0...v1.21.1) (2023-06-28)
### Bug Fixes
* Revert "chore: remove unnecessary api endpoint check" ([#476](https://github.com/googleapis/gax-php/issues/476)) ([13e773f](https://github.com/googleapis/gax-php/commit/13e773f5b09f9a99b8425835815746d37e9c1da3))
## [1.21.0](https://github.com/googleapis/gax-php/compare/v1.20.2...v1.21.0) (2023-06-09)
### Features
* Support guzzle/promises:v2 ([753eae9](https://github.com/googleapis/gax-php/commit/753eae9acf638f3356f8149acf84444eb399a699))
## [1.20.2](https://github.com/googleapis/gax-php/compare/v1.20.1...v1.20.2) (2023-05-12)
### Bug Fixes
* Ensure timeout set by RetryMiddleware is int not float ([#462](https://github.com/googleapis/gax-php/issues/462)) ([9d4c7fa](https://github.com/googleapis/gax-php/commit/9d4c7fa89445c63ec0bf4745ed9d98fd185ef51f))
## [1.20.1](https://github.com/googleapis/gax-php/compare/v1.20.0...v1.20.1) (2023-05-12)
### Bug Fixes
* Default value for error message in createFromRequestException ([#463](https://github.com/googleapis/gax-php/issues/463)) ([7552d22](https://github.com/googleapis/gax-php/commit/7552d22241c2f488606e9546efdd6edea356ee9a))
## [1.20.0](https://github.com/googleapis/gax-php/compare/v1.19.1...v1.20.0) (2023-05-01)
### Features
* **deps:** Support google/common-protos 4.0 ([af1db80](https://github.com/googleapis/gax-php/commit/af1db80c22307597f0dfcb9fafa86caf466588ba))
* **deps:** Support google/grpc-gcp 0.3 ([18edc2c](https://github.com/googleapis/gax-php/commit/18edc2ce6a1a615e3ea7c00ede313c32cec4b799))
## [1.19.1](https://github.com/googleapis/gax-php/compare/v1.19.0...v1.19.1) (2023-03-16)
### Bug Fixes
* Simplify ResourceHelperTrait registration ([#447](https://github.com/googleapis/gax-php/issues/447)) ([4949dc0](https://github.com/googleapis/gax-php/commit/4949dc0c4cd5e58af7933a1d2ecab90832c0b036))
## [1.19.0](https://github.com/googleapis/gax-php/compare/v1.18.2...v1.19.0) (2023-01-27)
### Features
* Ensure cache is used in calls to ADC::onGCE ([#441](https://github.com/googleapis/gax-php/issues/441)) ([64a4184](https://github.com/googleapis/gax-php/commit/64a4184ab69d13104d269b15a55d4b8b2515b5a6))
## [1.18.2](https://github.com/googleapis/gax-php/compare/v1.18.1...v1.18.2) (2023-01-06)
### Bug Fixes
* Ensure metadata return type is loaded into descriptor pool ([#439](https://github.com/googleapis/gax-php/issues/439)) ([a40cf8d](https://github.com/googleapis/gax-php/commit/a40cf8d87ac9aa45d18239456e2e4c96653f1a6c))
* Implicit conversion from float to int warning ([#438](https://github.com/googleapis/gax-php/issues/438)) ([1cb62ad](https://github.com/googleapis/gax-php/commit/1cb62ad3d92ace0518017abc972e912b339f1b56))
## [1.18.1](https://github.com/googleapis/gax-php/compare/v1.18.0...v1.18.1) (2022-12-06)
### Bug Fixes
* Message parameters in required querystring ([#430](https://github.com/googleapis/gax-php/issues/430)) ([77bc5e1](https://github.com/googleapis/gax-php/commit/77bc5e1cb8f347601d9237bf5164cf8b8ad8aa0f))
## [1.18.0](https://github.com/googleapis/gax-php/compare/v1.17.0...v1.18.0) (2022-12-05)
### Features
* Add ResourceHelperTrait ([#428](https://github.com/googleapis/gax-php/issues/428)) ([0439efa](https://github.com/googleapis/gax-php/commit/0439efa926865be5fea25b699469b0c1f8c1c768))
## [1.17.0](https://github.com/googleapis/gax-php/compare/v1.16.4...v1.17.0) (2022-09-08)
### Features
* Add startAsyncCall support ([#418](https://github.com/googleapis/gax-php/issues/418)) ([fc90693](https://github.com/googleapis/gax-php/commit/fc9069373c329183e07f8d174084c305b2308209))
## [1.16.4](https://github.com/googleapis/gax-php/compare/v1.16.3...v1.16.4) (2022-08-25)
### Bug Fixes
* use interfaceOverride instead of param ([#419](https://github.com/googleapis/gax-php/issues/419)) ([9dd5bc9](https://github.com/googleapis/gax-php/commit/9dd5bc91c4becfd2a0832288ab2406c3d224618e))
## [1.16.3](https://github.com/googleapis/gax-php/compare/v1.16.2...v1.16.3) (2022-08-23)
### Bug Fixes
* add eager threshold ([#416](https://github.com/googleapis/gax-php/issues/416)) ([99eb172](https://github.com/googleapis/gax-php/commit/99eb172280f301b117fde9dcc92079ca03aa28bd))
## [1.16.2](https://github.com/googleapis/gax-php/compare/v1.16.1...v1.16.2) (2022-08-16)
### Bug Fixes
* use responseType for custom operations ([#413](https://github.com/googleapis/gax-php/issues/413)) ([b643adf](https://github.com/googleapis/gax-php/commit/b643adfc44dd9fe82b0919e5b34edd00c7cdbb1f))
## [1.16.1](https://github.com/googleapis/gax-php/compare/v1.16.0...v1.16.1) (2022-08-11)
### Bug Fixes
* remove typehint from extended method ([#411](https://github.com/googleapis/gax-php/issues/411)) ([fb37f73](https://github.com/googleapis/gax-php/commit/fb37f7365e888465d84fca304ca83360ddbae6c3))
## [1.16.0](https://github.com/googleapis/gax-php/compare/v1.15.0...v1.16.0) (2022-08-10)
### Features
* additional typehinting ([#403](https://github.com/googleapis/gax-php/issues/403)) ([6597a07](https://github.com/googleapis/gax-php/commit/6597a07019665d91e07ea0a016c7d99c8a099cd2))
* drop support for PHP 5.6 ([#397](https://github.com/googleapis/gax-php/issues/397)) ([b888b24](https://github.com/googleapis/gax-php/commit/b888b24e0e223784e22dbbbe27fe0284cdcdfc35))
* introduce startApiCall ([#406](https://github.com/googleapis/gax-php/issues/406)) ([1cfeb62](https://github.com/googleapis/gax-php/commit/1cfeb628070c9c6e57b2dde854b0a973a888a2bc))
### Bug Fixes
* **deps:** update dependency google/longrunning to ^0.2 ([#407](https://github.com/googleapis/gax-php/issues/407)) ([54d4f32](https://github.com/googleapis/gax-php/commit/54d4f32ba5464d1f5da33e1c99a020174cae367c))
## [1.15.0](https://github.com/googleapis/gax-php/compare/v1.14.0...v1.15.0) (2022-08-02)
### Features
* move LongRunning classes to a standalone package ([#401](https://github.com/googleapis/gax-php/issues/401)) ([1747125](https://github.com/googleapis/gax-php/commit/1747125c84dcc6d42390de7e78d2e326884e1073))
## [1.14.0](https://github.com/googleapis/gax-php/compare/v1.13.0...v1.14.0) (2022-07-26)
### Features
* support requesting numeric enum rest encoding ([#395](https://github.com/googleapis/gax-php/issues/395)) ([0d74a48](https://github.com/googleapis/gax-php/commit/0d74a4877c5198cfaf534c4e55d7e418b50bc6ab))
CODE_OF_CONDUCT.md 0000644 00000003675 15174273622 0007367 0 ustar 00 # Contributor Code of Conduct
As contributors and maintainers of this project,
and in the interest of fostering an open and welcoming community,
we pledge to respect all people who contribute through reporting issues,
posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in this project
a harassment-free experience for everyone,
regardless of level of experience, gender, gender identity and expression,
sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information,
such as physical or electronic
addresses, without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct.
By adopting this Code of Conduct,
project maintainers commit themselves to fairly and consistently
applying these principles to every aspect of managing this project.
Project maintainers who do not follow or enforce the Code of Conduct
may be permanently removed from the project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported by opening an issue
or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0,
available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
LICENSE 0000644 00000002700 15174273622 0005561 0 ustar 00 Copyright 2016, Google Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
README.md 0000644 00000005300 15174273622 0006032 0 ustar 00 # Google API Core for PHP

- [Documentation](https://googleapis.github.io/gax-php)
Google API Core for PHP (gax-php) is a set of modules which aids the development
of APIs for clients based on [gRPC][] and Google API conventions.
Application code will rarely need to use most of the classes within this library
directly, but code generated automatically from the API definition files in
[Google APIs][] can use services such as page streaming and retry to provide a
more convenient and idiomatic API surface to callers.
[gRPC]: http://grpc.io
[Google APIs]: https://github.com/googleapis/googleapis/
## PHP Versions
gax-php currently requires PHP 5.6 or higher.
## Contributing
Contributions to this library are always welcome and highly encouraged.
See the [CONTRIBUTING][] documentation for more information on how to get
started.
[CONTRIBUTING]: https://github.com/googleapis/gax-php/blob/main/.github/CONTRIBUTING.md
## Versioning
This library follows [Semantic Versioning][].
This library is considered GA (generally available). As such, it will not
introduce backwards-incompatible changes in any minor or patch releases. We will
address issues and requests with the highest priority.
[Semantic Versioning]: http://semver.org/
## Repository Structure
All code lives under the src/ directory. Handwritten code lives in the
src/ApiCore directory and is contained in the `Google\ApiCore` namespace.
Generated classes for protobuf common types and LongRunning client live under
the src/ directory, in the appropriate directory and namespace.
Code in the metadata/ directory is provided to support generated protobuf
classes, and should not be used directly.
## Development Set-Up
These steps describe the dependencies to install for Linux, and equivalents can
be found for Mac or Windows.
1. Install dependencies.
```sh
> cd ~/
> sudo apt-get install php php-dev libcurl3-openssl-dev php-pear php-bcmath php-xml
> curl -sS https://getcomposer.org/installer | php
> sudo pecl install protobuf
```
2. Set up this repo.
```sh
> cd /path/to/gax-php
> cp ~/composer.phar ./
> php composer.phar install
```
3. Run tests.
```sh
> vendor/bin/phpunit --bootstrap tests/bootstrap.php tests
```
4. Updating dependencies after changing `composer.json`:
```sh
> php composer.phar update
`
```
5. Formatting source:
```sh
> vendor/bin/phpcbf -s --standard=./ruleset.xml
> vendor/bin/phpcs -s --standard=./ruleset.xml
```
## License
BSD - See [LICENSE][] for more information.
[LICENSE]: https://github.com/googleapis/gax-php/blob/main/LICENSE
SECURITY.md 0000644 00000000511 15174273622 0006343 0 ustar 00 # Security Policy
To report a security issue, please use [g.co/vulnz](https://g.co/vulnz).
The Google Security Team will respond within 5 working days of your report on g.co/vulnz.
We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue.
VERSION 0000644 00000000007 15174273622 0005622 0 ustar 00 1.26.1
composer.json 0000644 00000002310 15174273622 0007273 0 ustar 00 {
"name": "google/gax",
"type": "library",
"description": "Google API Core for PHP",
"keywords": ["google"],
"homepage": "https://github.com/googleapis/gax-php",
"license": "BSD-3-Clause",
"require": {
"php": ">=7.4",
"google/auth": "^1.34.0",
"google/grpc-gcp": "^0.2||^0.3",
"grpc/grpc": "^1.13",
"google/protobuf": "^3.21.4",
"guzzlehttp/promises": "^1.4||^2.0",
"guzzlehttp/psr7": "^2.0",
"google/common-protos": "^3.0||^4.0",
"google/longrunning": "~0.2"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "3.*",
"phpspec/prophecy-phpunit": "^2.0"
},
"conflict": {
"ext-protobuf": "<3.7.0"
},
"autoload": {
"psr-4": {
"Google\\ApiCore\\": "src",
"GPBMetadata\\ApiCore\\": "metadata/ApiCore"
}
},
"autoload-dev": {
"psr-4": {
"Google\\ApiCore\\Dev\\": "dev/src",
"Google\\ApiCore\\": "tests",
"GPBMetadata\\Google\\": "metadata/Google"
}
},
"scripts": {
"regenerate-test-protos": "dev/sh/regenerate-test-protos.sh"
}
}
metadata/ApiCore/Testing/Mocks.php 0000644 00000003704 15174273622 0013065 0 ustar 00 internalAddGeneratedFile(
'
–
Testing/mocks.protogoogle.apicore.testinggoogle/protobuf/timestamp.protogoogle/protobuf/duration.protogoogle/protobuf/struct.protogoogle/protobuf/wrappers.proto"4
MockRequest
page_token (
page_size ("á
MockResponse
name (
number (
resources_list (
next_page_token ( M
resources_map (26.google.apicore.testing.MockResponse.ResourcesMapEntry3
ResourcesMapEntry
key (
value ( :8"Œ
MockRequestBody
name (
number (
repeated_field ( ?
nested_message (2\'.google.apicore.testing.MockRequestBody0
bytes_value (2.google.protobuf.BytesValue1
duration_value (2.google.protobuf.Duration.
field_mask (2.google.protobuf.FieldMask0
int64_value (2.google.protobuf.Int64Value.
list_value (2.google.protobuf.ListValue2
string_value
(2.google.protobuf.StringValue-
struct_value (2.google.protobuf.Struct3
timestamp_value (2.google.protobuf.Timestamp+
value_value
(2.google.protobuf.Value
field_1 ( H
field_2 ( H
field_3 ( H B
oneof_fieldB7ÊGoogle\\ApiCore\\TestingâGPBMetadata\\ApiCore\\Testingbproto3'
, true);
static::$is_initialized = true;
}
}
metadata/Google/ApiCore/Tests/Unit/Example.php 0000644 00000001534 15174273622 0015223 0 ustar 00 internalAddGeneratedFile(hex2bin(
"0a85010a0d6578616d706c652e70726f746f1219676f6f676c652e617069" .
"636f72652e74657374732e756e6974220b0a094d794d6573736167654244" .
"ca0219476f6f676c655c417069436f72655c54657374735c556e6974e202" .
"254750424d657461646174615c476f6f676c655c417069436f72655c5465" .
"7374735c556e6974620670726f746f33"
));
static::$is_initialized = true;
}
}
metadata/README.md 0000644 00000000243 15174273622 0007613 0 ustar 00 # Google Protobuf Metadata Classes
## WARNING!
These classes are not intended for direct use - they exist only to support
the generated protobuf classes in src/
phpstan.neon.dist 0000644 00000000123 15174273622 0010051 0 ustar 00 parameters:
treatPhpDocTypesAsCertain: false
level: 5
paths:
- src
phpunit.xml.dist 0000644 00000000723 15174273622 0007732 0 ustar 00
src/ApiCore
tests/Tests/Unit
renovate.json 0000644 00000000151 15174273622 0007270 0 ustar 00 {
"extends": [
"config:base",
":preserveSemverRanges",
":disableDependencyDashboard"
]
}
src/AgentHeader.php 0000644 00000013532 15174273622 0010230 0 ustar 00 $value) {
$metricsList[] = $key . "/" . $value;
}
return [self::AGENT_HEADER_KEY => [implode(" ", $metricsList)]];
}
/**
* Reads the gapic version string from a VERSION file. In order to determine the file
* location, this method follows this procedure:
* - accepts a class name $callingClass
* - identifies the file defining that class
* - searches up the directory structure for the 'src' directory
* - looks in the directory above 'src' for a file named VERSION
*
* @param string $callingClass
* @return string the gapic version
* @throws \ReflectionException
*/
public static function readGapicVersionFromFile(string $callingClass)
{
$callingClassFile = (new \ReflectionClass($callingClass))->getFileName();
$versionFile = substr(
$callingClassFile,
0,
strrpos($callingClassFile, DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR)
) . DIRECTORY_SEPARATOR . 'VERSION';
return Version::readVersionFile($versionFile);
}
}
src/ApiException.php 0000644 00000025613 15174273622 0010454 0 ustar 00 null,
'metadata' => null,
'basicMessage' => $message,
];
parent::__construct($message, $code, $optionalArgs['previous']);
$this->status = $status;
$this->metadata = $optionalArgs['metadata'];
$this->basicMessage = $optionalArgs['basicMessage'];
if ($this->metadata) {
$this->decodedMetadataErrorInfo = self::decodeMetadataErrorInfo($this->metadata);
}
}
public function getStatus()
{
return $this->status;
}
/**
* Returns null if metadata does not contain error info, or returns containsErrorInfo() array
* if the metadata does contain error info.
* @param array $metadata
* @return array $details {
* @type string|null $reason
* @type string|null $domain
* @type array|null $errorInfoMetadata
* }
*/
private static function decodeMetadataErrorInfo(array $metadata)
{
$details = [];
// ApiExceptions created from RPC status have metadata that is an array of objects.
if (is_object(reset($metadata))) {
$metadataRpcStatus = Serializer::decodeAnyMessages($metadata);
$details = self::containsErrorInfo($metadataRpcStatus);
} elseif (self::containsErrorInfo($metadata)) {
$details = self::containsErrorInfo($metadata);
} else {
// For GRPC-based responses, the $metadata needs to be decoded.
$metadataGrpc = Serializer::decodeMetadata($metadata);
$details = self::containsErrorInfo($metadataGrpc);
}
return $details;
}
/**
* Returns the `reason` in ErrorInfo for an exception, or null if there is no ErrorInfo.
* @return string|null $reason
*/
public function getReason()
{
return ($this->decodedMetadataErrorInfo) ? $this->decodedMetadataErrorInfo['reason'] : null;
}
/**
* Returns the `domain` in ErrorInfo for an exception, or null if there is no ErrorInfo.
* @return string|null $domain
*/
public function getDomain()
{
return ($this->decodedMetadataErrorInfo) ? $this->decodedMetadataErrorInfo['domain'] : null;
}
/**
* Returns the `metadata` in ErrorInfo for an exception, or null if there is no ErrorInfo.
* @return array|null $errorInfoMetadata
*/
public function getErrorInfoMetadata()
{
return ($this->decodedMetadataErrorInfo) ? $this->decodedMetadataErrorInfo['errorInfoMetadata'] : null;
}
/**
* @param stdClass $status
* @return ApiException
*/
public static function createFromStdClass(stdClass $status)
{
$metadata = property_exists($status, 'metadata') ? $status->metadata : null;
return self::create(
$status->details,
$status->code,
$metadata,
Serializer::decodeMetadata((array) $metadata)
);
}
/**
* @param string $basicMessage
* @param int $rpcCode
* @param array|null $metadata
* @param Exception $previous
* @return ApiException
*/
public static function createFromApiResponse(
$basicMessage,
$rpcCode,
array $metadata = null,
Exception $previous = null
) {
return self::create(
$basicMessage,
$rpcCode,
$metadata,
Serializer::decodeMetadata((array) $metadata),
$previous
);
}
/**
* For REST-based responses, the metadata does not need to be decoded.
*
* @param string $basicMessage
* @param int $rpcCode
* @param array|null $metadata
* @param Exception $previous
* @return ApiException
*/
public static function createFromRestApiResponse(
$basicMessage,
$rpcCode,
array $metadata = null,
Exception $previous = null
) {
return self::create(
$basicMessage,
$rpcCode,
$metadata,
is_null($metadata) ? [] : $metadata,
$previous
);
}
/**
* Checks if decoded metadata includes errorInfo message.
* If errorInfo is set, it will always contain `reason`, `domain`, and `metadata` keys.
* @param array $decodedMetadata
* @return array {
* @type string $reason
* @type string $domain
* @type array $errorInfoMetadata
* }
*/
private static function containsErrorInfo(array $decodedMetadata)
{
if (empty($decodedMetadata)) {
return [];
}
foreach ($decodedMetadata as $value) {
$isErrorInfoArray = isset($value['reason']) && isset($value['domain']) && isset($value['metadata']);
if ($isErrorInfoArray) {
return [
'reason' => $value['reason'],
'domain' => $value['domain'],
'errorInfoMetadata' => $value['metadata'],
];
}
}
return [];
}
/**
* Construct an ApiException with a useful message, including decoded metadata.
* If the decoded metadata includes an errorInfo message, then the domain, reason,
* and metadata fields from that message are hoisted directly into the error.
*
* @param string $basicMessage
* @param int $rpcCode
* @param iterable|null $metadata
* @param array $decodedMetadata
* @param Exception|null $previous
* @return ApiException
*/
private static function create(
string $basicMessage,
int $rpcCode,
$metadata,
array $decodedMetadata,
Exception $previous = null
) {
$containsErrorInfo = self::containsErrorInfo($decodedMetadata);
$rpcStatus = ApiStatus::statusFromRpcCode($rpcCode);
$messageData = [
'message' => $basicMessage,
'code' => $rpcCode,
'status' => $rpcStatus,
'details' => $decodedMetadata
];
if ($containsErrorInfo) {
$messageData = array_merge($containsErrorInfo, $messageData);
}
$message = json_encode($messageData, JSON_PRETTY_PRINT);
if ($metadata instanceof RepeatedField) {
$metadata = iterator_to_array($metadata);
}
return new ApiException($message, $rpcCode, $rpcStatus, [
'previous' => $previous,
'metadata' => $metadata,
'basicMessage' => $basicMessage,
]);
}
/**
* @param Status $status
* @return ApiException
*/
public static function createFromRpcStatus(Status $status)
{
return self::create(
$status->getMessage(),
$status->getCode(),
$status->getDetails(),
Serializer::decodeAnyMessages($status->getDetails())
);
}
/**
* Creates an ApiException from a GuzzleHttp RequestException.
*
* @param RequestException $ex
* @param boolean $isStream
* @return ApiException
* @throws ValidationException
*/
public static function createFromRequestException(RequestException $ex, bool $isStream = false)
{
$res = $ex->getResponse();
$body = (string) $res->getBody();
$decoded = json_decode($body, true);
// A streaming response body will return one error in an array. Parse
// that first (and only) error message, if provided.
if ($isStream && isset($decoded[0])) {
$decoded = $decoded[0];
}
if (isset($decoded['error']) && $decoded['error']) {
$error = $decoded['error'];
$basicMessage = $error['message'] ?? '';
$code = isset($error['status'])
? ApiStatus::rpcCodeFromStatus($error['status'])
: $ex->getCode();
$metadata = $error['details'] ?? null;
return static::createFromRestApiResponse($basicMessage, $code, $metadata);
}
// Use the RPC code instead of the HTTP Status Code.
$code = ApiStatus::rpcCodeFromHttpStatusCode($res->getStatusCode());
return static::createFromApiResponse($body, $code);
}
/**
* @return null|string
*/
public function getBasicMessage()
{
return $this->basicMessage;
}
/**
* @return mixed[]
*/
public function getMetadata()
{
return $this->metadata;
}
/**
* String representation of ApiException
* @return string
*/
public function __toString()
{
return __CLASS__ . ": $this->message\n";
}
}
src/ApiStatus.php 0000644 00000015006 15174273622 0007774 0 ustar 00 Code::OK,
ApiStatus::CANCELLED => Code::CANCELLED,
ApiStatus::UNKNOWN => Code::UNKNOWN,
ApiStatus::INVALID_ARGUMENT => Code::INVALID_ARGUMENT,
ApiStatus::DEADLINE_EXCEEDED => Code::DEADLINE_EXCEEDED,
ApiStatus::NOT_FOUND => Code::NOT_FOUND,
ApiStatus::ALREADY_EXISTS => Code::ALREADY_EXISTS,
ApiStatus::PERMISSION_DENIED => Code::PERMISSION_DENIED,
ApiStatus::RESOURCE_EXHAUSTED => Code::RESOURCE_EXHAUSTED,
ApiStatus::FAILED_PRECONDITION => Code::FAILED_PRECONDITION,
ApiStatus::ABORTED => Code::ABORTED,
ApiStatus::OUT_OF_RANGE => Code::OUT_OF_RANGE,
ApiStatus::UNIMPLEMENTED => Code::UNIMPLEMENTED,
ApiStatus::INTERNAL => Code::INTERNAL,
ApiStatus::UNAVAILABLE => Code::UNAVAILABLE,
ApiStatus::DATA_LOSS => Code::DATA_LOSS,
ApiStatus::UNAUTHENTICATED => Code::UNAUTHENTICATED,
];
private static $codeToApiStatusMap = [
Code::OK => ApiStatus::OK,
Code::CANCELLED => ApiStatus::CANCELLED,
Code::UNKNOWN => ApiStatus::UNKNOWN,
Code::INVALID_ARGUMENT => ApiStatus::INVALID_ARGUMENT,
Code::DEADLINE_EXCEEDED => ApiStatus::DEADLINE_EXCEEDED,
Code::NOT_FOUND => ApiStatus::NOT_FOUND,
Code::ALREADY_EXISTS => ApiStatus::ALREADY_EXISTS,
Code::PERMISSION_DENIED => ApiStatus::PERMISSION_DENIED,
Code::RESOURCE_EXHAUSTED => ApiStatus::RESOURCE_EXHAUSTED,
Code::FAILED_PRECONDITION => ApiStatus::FAILED_PRECONDITION,
Code::ABORTED => ApiStatus::ABORTED,
Code::OUT_OF_RANGE => ApiStatus::OUT_OF_RANGE,
Code::UNIMPLEMENTED => ApiStatus::UNIMPLEMENTED,
Code::INTERNAL => ApiStatus::INTERNAL,
Code::UNAVAILABLE => ApiStatus::UNAVAILABLE,
Code::DATA_LOSS => ApiStatus::DATA_LOSS,
Code::UNAUTHENTICATED => ApiStatus::UNAUTHENTICATED,
];
private static $httpStatusCodeToRpcCodeMap = [
400 => Code::INVALID_ARGUMENT,
401 => Code::UNAUTHENTICATED,
403 => Code::PERMISSION_DENIED,
404 => Code::NOT_FOUND,
409 => Code::ABORTED,
416 => Code::OUT_OF_RANGE,
429 => Code::RESOURCE_EXHAUSTED,
499 => Code::CANCELLED,
501 => Code::UNIMPLEMENTED,
503 => Code::UNAVAILABLE,
504 => Code::DEADLINE_EXCEEDED,
];
/**
* @param string $status
* @return bool
*/
public static function isValidStatus(string $status)
{
return array_key_exists($status, self::$apiStatusToCodeMap);
}
/**
* @param int $code
* @return string
*/
public static function statusFromRpcCode(int $code)
{
if (array_key_exists($code, self::$codeToApiStatusMap)) {
return self::$codeToApiStatusMap[$code];
}
return ApiStatus::UNRECOGNIZED_STATUS;
}
/**
* @param string $status
* @return int
*/
public static function rpcCodeFromStatus(string $status)
{
if (array_key_exists($status, self::$apiStatusToCodeMap)) {
return self::$apiStatusToCodeMap[$status];
}
return ApiStatus::UNRECOGNIZED_CODE;
}
/**
* Maps HTTP status codes to Google\Rpc\Code codes.
* Some codes are left out because they map to multiple gRPC codes (e.g. 500).
*
* @param int $httpStatusCode
* @return int
*/
public static function rpcCodeFromHttpStatusCode(int $httpStatusCode)
{
if (array_key_exists($httpStatusCode, self::$httpStatusCodeToRpcCodeMap)) {
return self::$httpStatusCodeToRpcCodeMap[$httpStatusCode];
}
// All 2xx
if ($httpStatusCode >= 200 && $httpStatusCode < 300) {
return Code::OK;
}
// All 4xx
if ($httpStatusCode >= 400 && $httpStatusCode < 500) {
return Code::FAILED_PRECONDITION;
}
// All 5xx
if ($httpStatusCode >= 500 && $httpStatusCode < 600) {
return Code::INTERNAL;
}
// Everything else (We cannot change this to Code::UNKNOWN because it would break BC)
return ApiStatus::UNRECOGNIZED_CODE;
}
}
src/ArrayTrait.php 0000644 00000007214 15174273622 0010143 0 ustar 00 pluck($key, $arr, false);
}
}
return $values;
}
/**
* Determine whether given array is associative.
*
* @param array $arr
* @return bool
*/
private function isAssoc(array $arr)
{
return array_keys($arr) !== range(0, count($arr) - 1);
}
/**
* Just like array_filter(), but preserves falsey values except null.
*
* @param array $arr
* @return array
*/
private function arrayFilterRemoveNull(array $arr)
{
return array_filter($arr, function ($element) {
if (!is_null($element)) {
return true;
}
return false;
});
}
/**
* Return a subset of an array, like pluckArray, without modifying the original array.
*
* @param array $keys
* @param array $arr
* @return array
*/
private function subsetArray(array $keys, array $arr)
{
return array_intersect_key(
$arr,
array_flip($keys)
);
}
}
src/BidiStream.php 0000644 00000013524 15174273622 0010105 0 ustar 00 call = $bidiStreamingCall;
if (array_key_exists('resourcesGetMethod', $streamingDescriptor)) {
$this->resourcesGetMethod = $streamingDescriptor['resourcesGetMethod'];
}
}
/**
* Write request to the server.
*
* @param mixed $request The request to write
* @throws ValidationException
*/
public function write($request)
{
if ($this->isComplete) {
throw new ValidationException("Cannot call write() after streaming call is complete.");
}
if ($this->writesClosed) {
throw new ValidationException("Cannot call write() after calling closeWrite().");
}
$this->call->write($request);
}
/**
* Write all requests in $requests.
*
* @param iterable $requests An Iterable of request objects to write to the server
*
* @throws ValidationException
*/
public function writeAll($requests = [])
{
foreach ($requests as $request) {
$this->write($request);
}
}
/**
* Inform the server that no more requests will be written. The write() function cannot be
* called after closeWrite() is called.
* @throws ValidationException
*/
public function closeWrite()
{
if ($this->isComplete) {
throw new ValidationException(
"Cannot call closeWrite() after streaming call is complete."
);
}
if (!$this->writesClosed) {
$this->call->writesDone();
$this->writesClosed = true;
}
}
/**
* Read the next response from the server. Returns null if the streaming call completed
* successfully. Throws an ApiException if the streaming call failed.
*
* @throws ValidationException
* @throws ApiException
* @return mixed
*/
public function read()
{
if ($this->isComplete) {
throw new ValidationException("Cannot call read() after streaming call is complete.");
}
$resourcesGetMethod = $this->resourcesGetMethod;
if (!is_null($resourcesGetMethod)) {
if (count($this->pendingResources) === 0) {
$response = $this->call->read();
if (!is_null($response)) {
$pendingResources = [];
foreach ($response->$resourcesGetMethod() as $resource) {
$pendingResources[] = $resource;
}
$this->pendingResources = array_reverse($pendingResources);
}
}
$result = array_pop($this->pendingResources);
} else {
$result = $this->call->read();
}
if (is_null($result)) {
$status = $this->call->getStatus();
$this->isComplete = true;
if (!($status->code == Code::OK)) {
throw ApiException::createFromStdClass($status);
}
}
return $result;
}
/**
* Call closeWrite(), and read all responses from the server, until the streaming call is
* completed. Throws an ApiException if the streaming call failed.
*
* @throws ValidationException
* @throws ApiException
* @return \Generator|mixed[]
*/
public function closeWriteAndReadAll()
{
$this->closeWrite();
$response = $this->read();
while (!is_null($response)) {
yield $response;
$response = $this->read();
}
}
/**
* Return the underlying gRPC call object
*
* @return \Grpc\BidiStreamingCall|mixed
*/
public function getBidiStreamingCall()
{
return $this->call;
}
}
src/Call.php 0000644 00000006721 15174273622 0006736 0 ustar 00 method = $method;
$this->decodeType = $decodeType;
$this->message = $message;
$this->descriptor = $descriptor;
$this->callType = $callType;
}
/**
* @return string
*/
public function getMethod()
{
return $this->method;
}
/**
* @return int
*/
public function getCallType()
{
return $this->callType;
}
/**
* @return string
*/
public function getDecodeType()
{
return $this->decodeType;
}
/**
* @return mixed|Message
*/
public function getMessage()
{
return $this->message;
}
/**
* @return array|null
*/
public function getDescriptor()
{
return $this->descriptor;
}
/**
* @param mixed|Message $message
* @return Call
*/
public function withMessage($message)
{
// @phpstan-ignore-next-line
return new static(
$this->method,
$this->decodeType,
$message,
$this->descriptor,
$this->callType
);
}
}
src/ClientStream.php 0000644 00000006644 15174273622 0010461 0 ustar 00 call = $clientStreamingCall;
}
/**
* Write request to the server.
*
* @param mixed $request The request to write
*/
public function write($request)
{
$this->call->write($request);
}
/**
* Read the response from the server, completing the streaming call.
*
* @throws ApiException
* @return mixed The response object from the server
*/
public function readResponse()
{
list($response, $status) = $this->call->wait();
if ($status->code == Code::OK) {
return $response;
} else {
throw ApiException::createFromStdClass($status);
}
}
/**
* Write all data in $dataArray and read the response from the server, completing the streaming
* call.
*
* @param mixed[] $requests An iterator of request objects to write to the server
* @return mixed The response object from the server
*/
public function writeAllAndReadResponse(array $requests)
{
foreach ($requests as $request) {
$this->write($request);
}
return $this->readResponse();
}
/**
* Return the underlying gRPC call object
*
* @return \Grpc\ClientStreamingCall|mixed
*/
public function getClientStreamingCall()
{
return $this->call;
}
}
src/CredentialsWrapper.php 0000644 00000031625 15174273622 0011662 0 ustar 00 credentialsFetcher = $credentialsFetcher;
$this->authHttpHandler = $authHttpHandler ?: self::buildHttpHandlerFactory();
if (empty($universeDomain)) {
throw new ValidationException('The universe domain cannot be empty');
}
$this->universeDomain = $universeDomain;
}
/**
* Factory method to create a CredentialsWrapper from an array of options.
*
* @param array $args {
* An array of optional arguments.
*
* @type string|array $keyFile
* Credentials to be used. Accepts either a path to a credentials file, or a decoded
* credentials file as a PHP array. If this is not specified, application default
* credentials will be used.
* @type string[] $scopes
* A string array of scopes to use when acquiring credentials.
* @type callable $authHttpHandler
* A handler used to deliver PSR-7 requests specifically
* for authentication. Should match a signature of
* `function (RequestInterface $request, array $options) : ResponseInterface`.
* @type bool $enableCaching
* Enable caching of access tokens. Defaults to true.
* @type CacheItemPoolInterface $authCache
* A cache for storing access tokens. Defaults to a simple in memory implementation.
* @type array $authCacheOptions
* Cache configuration options.
* @type string $quotaProject
* Specifies a user project to bill for access charges associated with the request.
* @type string[] $defaultScopes
* A string array of default scopes to use when acquiring
* credentials.
* @type bool $useJwtAccessWithScope
* Ensures service account credentials use JWT Access (also known as self-signed
* JWTs), even when user-defined scopes are supplied.
* }
* @param string $universeDomain The expected universe of the credentials. Defaults to
* "googleapis.com"
* @return CredentialsWrapper
* @throws ValidationException
*/
public static function build(
array $args = [],
string $universeDomain = GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN
) {
$args += [
'keyFile' => null,
'scopes' => null,
'authHttpHandler' => null,
'enableCaching' => true,
'authCache' => null,
'authCacheOptions' => [],
'quotaProject' => null,
'defaultScopes' => null,
'useJwtAccessWithScope' => true,
];
$keyFile = $args['keyFile'];
$authHttpHandler = $args['authHttpHandler'] ?: self::buildHttpHandlerFactory();
if (is_null($keyFile)) {
$loader = self::buildApplicationDefaultCredentials(
$args['scopes'],
$authHttpHandler,
$args['authCacheOptions'],
$args['authCache'],
$args['quotaProject'],
$args['defaultScopes']
);
if ($loader instanceof FetchAuthTokenCache) {
$loader = $loader->getFetcher();
}
} else {
if (is_string($keyFile)) {
if (!file_exists($keyFile)) {
throw new ValidationException("Could not find keyfile: $keyFile");
}
$keyFile = json_decode(file_get_contents($keyFile), true);
}
if (isset($args['quotaProject'])) {
$keyFile['quota_project_id'] = $args['quotaProject'];
}
$loader = CredentialsLoader::makeCredentials(
$args['scopes'],
$keyFile,
$args['defaultScopes']
);
}
if ($loader instanceof ServiceAccountCredentials && $args['useJwtAccessWithScope']) {
// Ensures the ServiceAccountCredentials uses JWT Access, also known
// as self-signed JWTs, even when user-defined scopes are supplied.
$loader->useJwtAccessWithScope();
}
if ($args['enableCaching']) {
$authCache = $args['authCache'] ?: new MemoryCacheItemPool();
$loader = new FetchAuthTokenCache(
$loader,
$args['authCacheOptions'],
$authCache
);
}
return new CredentialsWrapper($loader, $authHttpHandler, $universeDomain);
}
/**
* @return string|null The quota project associated with the credentials.
*/
public function getQuotaProject()
{
if ($this->credentialsFetcher instanceof GetQuotaProjectInterface) {
return $this->credentialsFetcher->getQuotaProject();
}
return null;
}
/**
* @deprecated
* @return string Bearer string containing access token.
*/
public function getBearerString()
{
$token = $this->credentialsFetcher->getLastReceivedToken();
if (self::isExpired($token)) {
$this->checkUniverseDomain();
$token = $this->credentialsFetcher->fetchAuthToken($this->authHttpHandler);
if (!self::isValid($token)) {
return '';
}
}
return empty($token['access_token']) ? '' : 'Bearer ' . $token['access_token'];
}
/**
* @param string $audience optional audience for self-signed JWTs.
* @return callable Callable function that returns an authorization header.
*/
public function getAuthorizationHeaderCallback($audience = null)
{
// NOTE: changes to this function should be treated carefully and tested thoroughly. It will
// be passed into the gRPC c extension, and changes have the potential to trigger very
// difficult-to-diagnose segmentation faults.
return function () use ($audience) {
$token = $this->credentialsFetcher->getLastReceivedToken();
if (self::isExpired($token)) {
$this->checkUniverseDomain();
// Call updateMetadata to take advantage of self-signed JWTs
if ($this->credentialsFetcher instanceof UpdateMetadataInterface) {
return $this->credentialsFetcher->updateMetadata([], $audience);
}
// In case a custom fetcher is provided (unlikely) which doesn't
// implement UpdateMetadataInterface
$token = $this->credentialsFetcher->fetchAuthToken($this->authHttpHandler);
if (!self::isValid($token)) {
return [];
}
}
$tokenString = $token['access_token'];
if (!empty($tokenString)) {
return ['authorization' => ["Bearer $tokenString"]];
}
return [];
};
}
/**
* Verify that the expected universe domain matches the universe domain from the credentials.
*/
private function checkUniverseDomain()
{
if (false === $this->hasCheckedUniverse) {
$credentialsUniverse = $this->credentialsFetcher instanceof GetUniverseDomainInterface
? $this->credentialsFetcher->getUniverseDomain()
: GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN;
if ($credentialsUniverse !== $this->universeDomain) {
throw new ValidationException(sprintf(
'The configured universe domain (%s) does not match the credential universe domain (%s)',
$this->universeDomain,
$credentialsUniverse
));
}
$this->hasCheckedUniverse = true;
}
}
/**
* @return Guzzle6HttpHandler|Guzzle7HttpHandler
* @throws ValidationException
*/
private static function buildHttpHandlerFactory()
{
try {
return HttpHandlerFactory::build();
} catch (Exception $ex) {
throw new ValidationException("Failed to build HttpHandler", $ex->getCode(), $ex);
}
}
/**
* @param array $scopes
* @param callable $authHttpHandler
* @param array $authCacheOptions
* @param CacheItemPoolInterface $authCache
* @param string $quotaProject
* @param array $defaultScopes
* @return FetchAuthTokenInterface
* @throws ValidationException
*/
private static function buildApplicationDefaultCredentials(
array $scopes = null,
callable $authHttpHandler = null,
array $authCacheOptions = null,
CacheItemPoolInterface $authCache = null,
$quotaProject = null,
array $defaultScopes = null
) {
try {
return ApplicationDefaultCredentials::getCredentials(
$scopes,
$authHttpHandler,
$authCacheOptions,
$authCache,
$quotaProject,
$defaultScopes
);
} catch (DomainException $ex) {
throw new ValidationException("Could not construct ApplicationDefaultCredentials", $ex->getCode(), $ex);
}
}
/**
* @param mixed $token
*/
private static function isValid($token)
{
return is_array($token)
&& array_key_exists('access_token', $token);
}
/**
* @param mixed $token
*/
private static function isExpired($token)
{
return !(self::isValid($token)
&& array_key_exists('expires_at', $token)
&& $token['expires_at'] > time() + self::$eagerRefreshThresholdSeconds);
}
}
src/FixedSizeCollection.php 0000644 00000014415 15174273622 0011770 0 ustar 00 0. collectionSize: $collectionSize"
);
}
if ($collectionSize < $initialPage->getPageElementCount()) {
$ipc = $initialPage->getPageElementCount();
throw new InvalidArgumentException(
"collectionSize must be greater than or equal to the number of " .
"elements in initialPage. collectionSize: $collectionSize, " .
"initialPage size: $ipc"
);
}
$this->collectionSize = $collectionSize;
$this->pageList = FixedSizeCollection::createPageArray($initialPage, $collectionSize);
}
/**
* Returns the number of elements in the collection. This will be
* equal to the collectionSize parameter used at construction
* unless there are no elements remaining to be retrieved.
*
* @return int
*/
public function getCollectionSize()
{
$size = 0;
foreach ($this->pageList as $page) {
$size += $page->getPageElementCount();
}
return $size;
}
/**
* Returns true if there are more elements that can be retrieved
* from the API.
*
* @return bool
*/
public function hasNextCollection()
{
return $this->getLastPage()->hasNextPage();
}
/**
* Returns a page token that can be passed into the API list
* method to retrieve additional elements.
*
* @return string
*/
public function getNextPageToken()
{
return $this->getLastPage()->getNextPageToken();
}
/**
* Retrieves the next FixedSizeCollection using one or more API calls.
*
* @return FixedSizeCollection
*/
public function getNextCollection()
{
$lastPage = $this->getLastPage();
$nextPage = $lastPage->getNextPage($this->collectionSize);
return new FixedSizeCollection($nextPage, $this->collectionSize);
}
/**
* Returns an iterator over the elements of the collection.
*
* @return Generator
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
foreach ($this->pageList as $page) {
foreach ($page as $element) {
yield $element;
}
}
}
/**
* Returns an iterator over FixedSizeCollections, starting with this
* and making API calls as required until all of the elements have
* been retrieved.
*
* @return Generator|FixedSizeCollection[]
*/
public function iterateCollections()
{
$currentCollection = $this;
yield $this;
while ($currentCollection->hasNextCollection()) {
$currentCollection = $currentCollection->getNextCollection();
yield $currentCollection;
}
}
private function getLastPage()
{
$pageList = $this->pageList;
// Get last element in array...
$lastPage = end($pageList);
reset($pageList);
return $lastPage;
}
/**
* @param Page $initialPage
* @param int $collectionSize
* @return Page[]
*/
private static function createPageArray(Page $initialPage, int $collectionSize)
{
$pageList = [$initialPage];
$currentPage = $initialPage;
$itemCount = $currentPage->getPageElementCount();
while ($itemCount < $collectionSize && $currentPage->hasNextPage()) {
$remainingCount = $collectionSize - $itemCount;
$currentPage = $currentPage->getNextPage($remainingCount);
$rxElementCount = $currentPage->getPageElementCount();
if ($rxElementCount > $remainingCount) {
throw new LengthException("API returned a number of elements " .
"exceeding the specified page size limit. page size: " .
"$remainingCount, elements received: $rxElementCount");
}
array_push($pageList, $currentPage);
$itemCount += $rxElementCount;
}
return $pageList;
}
}
src/GPBLabel.php 0000644 00000003507 15174273622 0007432 0 ustar 00 $middlewareCallables */
private array $middlewareCallables = [];
private array $transportCallMethods = [
Call::UNARY_CALL => 'startUnaryCall',
Call::BIDI_STREAMING_CALL => 'startBidiStreamingCall',
Call::CLIENT_STREAMING_CALL => 'startClientStreamingCall',
Call::SERVER_STREAMING_CALL => 'startServerStreamingCall',
];
private bool $isNewClient;
/**
* Add a middleware to the call stack by providing a callable which will be
* invoked at the start of each call, and will return an instance of
* {@see MiddlewareInterface} when invoked.
*
* The callable must have the following method signature:
*
* callable(MiddlewareInterface): MiddlewareInterface
*
* An implementation may look something like this:
* ```
* $client->addMiddleware(function (MiddlewareInterface $handler) {
* return new class ($handler) implements MiddlewareInterface {
* public function __construct(private MiddlewareInterface $handler) {
* }
*
* public function __invoke(Call $call, array $options) {
* // modify call and options (pre-request)
* $response = ($this->handler)($call, $options);
* // modify the response (post-request)
* return $response;
* }
* };
* });
* ```
*
* @param callable $middlewareCallable A callable which returns an instance
* of {@see MiddlewareInterface} when invoked with a
* MiddlewareInterface instance as its first argument.
* @return void
*/
public function addMiddleware(callable $middlewareCallable): void
{
$this->middlewareCallables[] = $middlewareCallable;
}
/**
* Initiates an orderly shutdown in which preexisting calls continue but new
* calls are immediately cancelled.
*
* @experimental
*/
public function close()
{
$this->transport->close();
}
/**
* Get the transport for the client. This method is protected to support
* use by customized clients.
*
* @access private
* @return TransportInterface
*/
protected function getTransport()
{
return $this->transport;
}
/**
* Get the credentials for the client. This method is protected to support
* use by customized clients.
*
* @access private
* @return CredentialsWrapper
*/
protected function getCredentialsWrapper()
{
return $this->credentialsWrapper;
}
private static function getGapicVersion(array $options)
{
if (isset($options['libVersion'])) {
return $options['libVersion'];
} else {
if (!isset(self::$gapicVersionFromFile)) {
self::$gapicVersionFromFile = AgentHeader::readGapicVersionFromFile(__CLASS__);
}
return self::$gapicVersionFromFile;
}
}
private static function initGrpcGcpConfig(string $hostName, string $confPath)
{
$apiConfig = new ApiConfig();
$apiConfig->mergeFromJsonString(file_get_contents($confPath));
$config = new Config($hostName, $apiConfig);
return $config;
}
/**
* Get default options. This function should be "overridden" by clients using late static
* binding to provide default options to the client.
*
* @return array
* @access private
*/
private static function getClientDefaults()
{
return [];
}
private function buildClientOptions(array $options)
{
// Build $defaultOptions starting from top level
// variables, then going into deeper nesting, so that
// we will not encounter missing keys
$defaultOptions = self::getClientDefaults();
$defaultOptions += [
'disableRetries' => false,
'credentials' => null,
'credentialsConfig' => [],
'transport' => null,
'transportConfig' => [],
'gapicVersion' => self::getGapicVersion($options),
'libName' => null,
'libVersion' => null,
'apiEndpoint' => null,
'clientCertSource' => null,
// if the universe domain hasn't been explicitly set, assume GDU ("googleapis.com")
'universeDomain' => GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN,
];
$supportedTransports = $this->supportedTransports();
foreach ($supportedTransports as $transportName) {
if (!array_key_exists($transportName, $defaultOptions['transportConfig'])) {
$defaultOptions['transportConfig'][$transportName] = [];
}
}
if (in_array('grpc', $supportedTransports)) {
$defaultOptions['transportConfig']['grpc'] = [
'stubOpts' => ['grpc.service_config_disable_resolution' => 1]
];
}
// Keep track of the API Endpoint
$apiEndpoint = $options['apiEndpoint'] ?? null;
// Merge defaults into $options starting from top level
// variables, then going into deeper nesting, so that
// we will not encounter missing keys
$options += $defaultOptions;
$options['credentialsConfig'] += $defaultOptions['credentialsConfig'];
$options['transportConfig'] += $defaultOptions['transportConfig']; // @phpstan-ignore-line
if (isset($options['transportConfig']['grpc'])) {
$options['transportConfig']['grpc'] += $defaultOptions['transportConfig']['grpc'];
$options['transportConfig']['grpc']['stubOpts'] += $defaultOptions['transportConfig']['grpc']['stubOpts'];
}
if (isset($options['transportConfig']['rest'])) {
$options['transportConfig']['rest'] += $defaultOptions['transportConfig']['rest'];
}
// These calls do not apply to "New Surface" clients.
if (!$this->isNewClientSurface()) {
$preModifiedOptions = $options;
$this->modifyClientOptions($options);
// NOTE: this is required to ensure backwards compatiblity with $options['apiEndpoint']
if ($options['apiEndpoint'] !== $preModifiedOptions['apiEndpoint']) {
$apiEndpoint = $options['apiEndpoint'];
}
// serviceAddress is now deprecated and acts as an alias for apiEndpoint
if (isset($options['serviceAddress'])) {
$apiEndpoint = $this->pluck('serviceAddress', $options, false);
}
}
// If an API endpoint is different form the default, ensure the "audience" does not conflict
// with the custom endpoint by setting "user defined" scopes.
if ($apiEndpoint
&& $apiEndpoint != $defaultOptions['apiEndpoint']
&& empty($options['credentialsConfig']['scopes'])
&& !empty($options['credentialsConfig']['defaultScopes'])
) {
$options['credentialsConfig']['scopes'] = $options['credentialsConfig']['defaultScopes'];
}
// mTLS: detect and load the default clientCertSource if the environment variable
// "GOOGLE_API_USE_CLIENT_CERTIFICATE" is true, and the cert source is available
if (empty($options['clientCertSource']) && CredentialsLoader::shouldLoadClientCertSource()) {
if ($defaultCertSource = CredentialsLoader::getDefaultClientCertSource()) {
$options['clientCertSource'] = function () use ($defaultCertSource) {
$cert = call_user_func($defaultCertSource);
// the key and the cert are returned in one string
return [$cert, $cert];
};
}
}
// mTLS: If no apiEndpoint has been supplied by the user, and either
// GOOGLE_API_USE_MTLS_ENDPOINT tells us to, or mTLS is available, use the mTLS endpoint.
if (is_null($apiEndpoint) && $this->shouldUseMtlsEndpoint($options)) {
$apiEndpoint = self::determineMtlsEndpoint($options['apiEndpoint']);
}
// mTLS: It is not valid to configure mTLS outside of "googleapis.com" (yet)
if (isset($options['clientCertSource'])
&& $options['universeDomain'] !== GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN
) {
throw new ValidationException(
'mTLS is not supported outside the "googleapis.com" universe'
);
}
if (is_null($apiEndpoint)) {
if (defined('self::SERVICE_ADDRESS_TEMPLATE')) {
// Derive the endpoint from the service address template and the universe domain
$apiEndpoint = str_replace(
'UNIVERSE_DOMAIN',
$options['universeDomain'],
self::SERVICE_ADDRESS_TEMPLATE
);
} else {
// For older clients, the service address template does not exist. Use the default
// endpoint instead.
$apiEndpoint = $defaultOptions['apiEndpoint'];
}
}
if (extension_loaded('sysvshm')
&& isset($options['gcpApiConfigPath'])
&& file_exists($options['gcpApiConfigPath'])
&& !empty($apiEndpoint)
) {
$grpcGcpConfig = self::initGrpcGcpConfig(
$apiEndpoint,
$options['gcpApiConfigPath']
);
if (!array_key_exists('stubOpts', $options['transportConfig']['grpc'])) {
$options['transportConfig']['grpc']['stubOpts'] = [];
}
$options['transportConfig']['grpc']['stubOpts'] += [
'grpc_call_invoker' => $grpcGcpConfig->callInvoker()
];
}
$options['apiEndpoint'] = $apiEndpoint;
return $options;
}
private function shouldUseMtlsEndpoint(array $options)
{
$mtlsEndpointEnvVar = getenv('GOOGLE_API_USE_MTLS_ENDPOINT');
if ('always' === $mtlsEndpointEnvVar) {
return true;
}
if ('never' === $mtlsEndpointEnvVar) {
return false;
}
// For all other cases, assume "auto" and return true if clientCertSource exists
return !empty($options['clientCertSource']);
}
private static function determineMtlsEndpoint(string $apiEndpoint)
{
$parts = explode('.', $apiEndpoint);
if (count($parts) < 3) {
return $apiEndpoint; // invalid endpoint!
}
return sprintf('%s.mtls.%s', array_shift($parts), implode('.', $parts));
}
/**
* Configures the GAPIC client based on an array of options.
*
* @param array $options {
* An array of required and optional arguments.
*
* @type string $apiEndpoint
* The address of the API remote host, for example "example.googleapis.com. May also
* include the port, for example "example.googleapis.com:443"
* @type bool $disableRetries
* Determines whether or not retries defined by the client configuration should be
* disabled. Defaults to `false`.
* @type string|array $clientConfig
* Client method configuration, including retry settings. This option can be either a
* path to a JSON file, or a PHP array containing the decoded JSON data.
* By default this settings points to the default client config file, which is provided
* in the resources folder.
* @type string|array|FetchAuthTokenInterface|CredentialsWrapper $credentials
* The credentials to be used by the client to authorize API calls. This option
* accepts either a path to a credentials file, or a decoded credentials file as a
* PHP array.
* *Advanced usage*: In addition, this option can also accept a pre-constructed
* \Google\Auth\FetchAuthTokenInterface object or \Google\ApiCore\CredentialsWrapper
* object. Note that when one of these objects are provided, any settings in
* $authConfig will be ignored.
* @type array $credentialsConfig
* Options used to configure credentials, including auth token caching, for the client.
* For a full list of supporting configuration options, see
* \Google\ApiCore\CredentialsWrapper::build.
* @type string|TransportInterface $transport
* The transport used for executing network requests. May be either the string `rest`,
* `grpc`, or 'grpc-fallback'. Defaults to `grpc` if gRPC support is detected on the system.
* *Advanced usage*: Additionally, it is possible to pass in an already instantiated
* TransportInterface object. Note that when this objects is provided, any settings in
* $transportConfig, and any `$apiEndpoint` setting, will be ignored.
* @type array $transportConfig
* Configuration options that will be used to construct the transport. Options for
* each supported transport type should be passed in a key for that transport. For
* example:
* $transportConfig = [
* 'grpc' => [...],
* 'rest' => [...],
* 'grpc-fallback' => [...],
* ];
* See the GrpcTransport::build and RestTransport::build
* methods for the supported options.
* @type string $versionFile
* The path to a file which contains the current version of the client.
* @type string $descriptorsConfigPath
* The path to a descriptor configuration file.
* @type string $serviceName
* The name of the service.
* @type string $libName
* The name of the client application.
* @type string $libVersion
* The version of the client application.
* @type string $gapicVersion
* The code generator version of the GAPIC library.
* @type callable $clientCertSource
* A callable which returns the client cert as a string.
* }
* @throws ValidationException
*/
private function setClientOptions(array $options)
{
// serviceAddress is now deprecated and acts as an alias for apiEndpoint
if (isset($options['serviceAddress'])) {
$options['apiEndpoint'] = $this->pluck('serviceAddress', $options, false);
}
$this->validateNotNull($options, [
'apiEndpoint',
'serviceName',
'descriptorsConfigPath',
'clientConfig',
'disableRetries',
'credentialsConfig',
'transportConfig',
]);
$this->traitValidate($options, [
'credentials',
'transport',
'gapicVersion',
'libName',
'libVersion',
]);
if ($this->isNewClientSurface()) {
// cast to ClientOptions for new surfaces only
$options = new ClientOptions($options);
} elseif (is_string($options['clientConfig'])) {
// perform validation for V1 surfaces which is done in the
// ClientOptions class for v2 surfaces.
$options['clientConfig'] = json_decode(
file_get_contents($options['clientConfig']),
true
);
self::validateFileExists($options['descriptorsConfigPath']);
}
$this->serviceName = $options['serviceName'];
$this->retrySettings = RetrySettings::load(
$this->serviceName,
$options['clientConfig'],
$options['disableRetries']
);
$headerInfo = [
'libName' => $options['libName'],
'libVersion' => $options['libVersion'],
'gapicVersion' => $options['gapicVersion'],
];
// Edge case: If the client has the gRPC extension installed, but is
// a REST-only library, then the grpcVersion header should not be set.
if ($this->transport instanceof GrpcTransport) {
$headerInfo['grpcVersion'] = phpversion('grpc');
} elseif ($this->transport instanceof RestTransport
|| $this->transport instanceof GrpcFallbackTransport) {
$headerInfo['restVersion'] = Version::getApiCoreVersion();
}
$this->agentHeader = AgentHeader::buildAgentHeader($headerInfo);
// Set "client_library_name" depending on client library surface being used
$userAgentHeader = sprintf(
'gcloud-php-%s/%s',
$this->isNewClientSurface() ? 'new' : 'legacy',
$options['gapicVersion']
);
$this->agentHeader['User-Agent'] = [$userAgentHeader];
self::validateFileExists($options['descriptorsConfigPath']);
$descriptors = require($options['descriptorsConfigPath']);
$this->descriptors = $descriptors['interfaces'][$this->serviceName];
$this->credentialsWrapper = $this->createCredentialsWrapper(
$options['credentials'],
$options['credentialsConfig'],
$options['universeDomain']
);
$transport = $options['transport'] ?: self::defaultTransport();
$this->transport = $transport instanceof TransportInterface
? $transport
: $this->createTransport(
$options['apiEndpoint'],
$transport,
$options['transportConfig'],
$options['clientCertSource']
);
}
/**
* @param mixed $credentials
* @param array $credentialsConfig
* @return CredentialsWrapper
* @throws ValidationException
*/
private function createCredentialsWrapper($credentials, array $credentialsConfig, string $universeDomain)
{
if (is_null($credentials)) {
return CredentialsWrapper::build($credentialsConfig, $universeDomain);
} elseif (is_string($credentials) || is_array($credentials)) {
return CredentialsWrapper::build(['keyFile' => $credentials] + $credentialsConfig, $universeDomain);
} elseif ($credentials instanceof FetchAuthTokenInterface) {
$authHttpHandler = $credentialsConfig['authHttpHandler'] ?? null;
return new CredentialsWrapper($credentials, $authHttpHandler, $universeDomain);
} elseif ($credentials instanceof CredentialsWrapper) {
return $credentials;
} else {
throw new ValidationException(
'Unexpected value in $auth option, got: ' .
print_r($credentials, true)
);
}
}
/**
* @param string $apiEndpoint
* @param string $transport
* @param TransportOptions|array $transportConfig
* @param callable $clientCertSource
* @return TransportInterface
* @throws ValidationException
*/
private function createTransport(
string $apiEndpoint,
$transport,
$transportConfig,
callable $clientCertSource = null
) {
if (!is_string($transport)) {
throw new ValidationException(
"'transport' must be a string, instead got:" .
print_r($transport, true)
);
}
$supportedTransports = self::supportedTransports();
if (!in_array($transport, $supportedTransports)) {
throw new ValidationException(sprintf(
'Unexpected transport option "%s". Supported transports: %s',
$transport,
implode(', ', $supportedTransports)
));
}
$configForSpecifiedTransport = $transportConfig[$transport] ?? [];
if (is_array($configForSpecifiedTransport)) {
$configForSpecifiedTransport['clientCertSource'] = $clientCertSource;
} else {
$configForSpecifiedTransport->setClientCertSource($clientCertSource);
$configForSpecifiedTransport = $configForSpecifiedTransport->toArray();
}
switch ($transport) {
case 'grpc':
// Setting the user agent for gRPC requires special handling
if (isset($this->agentHeader['User-Agent'])) {
if ($configForSpecifiedTransport['stubOpts']['grpc.primary_user_agent'] ??= '') {
$configForSpecifiedTransport['stubOpts']['grpc.primary_user_agent'] .= ' ';
}
$configForSpecifiedTransport['stubOpts']['grpc.primary_user_agent'] .=
$this->agentHeader['User-Agent'][0];
}
return GrpcTransport::build($apiEndpoint, $configForSpecifiedTransport);
case 'grpc-fallback':
return GrpcFallbackTransport::build($apiEndpoint, $configForSpecifiedTransport);
case 'rest':
if (!isset($configForSpecifiedTransport['restClientConfigPath'])) {
throw new ValidationException(
"The 'restClientConfigPath' config is required for 'rest' transport."
);
}
$restConfigPath = $configForSpecifiedTransport['restClientConfigPath'];
return RestTransport::build($apiEndpoint, $restConfigPath, $configForSpecifiedTransport);
default:
throw new ValidationException(
"Unexpected 'transport' option: $transport. " .
"Supported values: ['grpc', 'rest', 'grpc-fallback']"
);
}
}
/**
* @param array $options
* @return OperationsClient
*/
private function createOperationsClient(array $options)
{
$this->pluckArray([
'serviceName',
'clientConfig',
'descriptorsConfigPath',
], $options);
// User-supplied operations client
if ($operationsClient = $this->pluck('operationsClient', $options, false)) {
return $operationsClient;
}
// operationsClientClass option
$operationsClientClass = $this->pluck('operationsClientClass', $options, false)
?: OperationsCLient::class;
return new $operationsClientClass($options);
}
/**
* @return string
*/
private static function defaultTransport()
{
return self::getGrpcDependencyStatus()
? 'grpc'
: 'rest';
}
private function validateCallConfig(string $methodName)
{
// Ensure a method descriptor exists for the target method.
if (!isset($this->descriptors[$methodName])) {
throw new ValidationException("Requested method '$methodName' does not exist in descriptor configuration.");
}
$methodDescriptors = $this->descriptors[$methodName];
// Ensure required descriptor configuration exists.
if (!isset($methodDescriptors['callType'])) {
throw new ValidationException("Requested method '$methodName' does not have a callType " .
"in descriptor configuration.");
}
$callType = $methodDescriptors['callType'];
// Validate various callType specific configurations.
if ($callType == Call::LONGRUNNING_CALL) {
if (!isset($methodDescriptors['longRunning'])) {
throw new ValidationException("Requested method '$methodName' does not have a longRunning config " .
"in descriptor configuration.");
}
// @TODO: check if the client implements `OperationsClientInterface` instead
if (!method_exists($this, 'getOperationsClient')) {
throw new ValidationException("Client missing required getOperationsClient " .
"for longrunning call '$methodName'");
}
} elseif ($callType == Call::PAGINATED_CALL) {
if (!isset($methodDescriptors['pageStreaming'])) {
throw new ValidationException("Requested method '$methodName' with callType PAGINATED_CALL does not " .
"have a pageStreaming in descriptor configuration.");
}
}
// LRO are either Standard LRO response type or custom, which are handled by
// startOperationCall, so no need to validate responseType for those callType.
if ($callType != Call::LONGRUNNING_CALL) {
if (!isset($methodDescriptors['responseType'])) {
throw new ValidationException("Requested method '$methodName' does not have a responseType " .
"in descriptor configuration.");
}
}
return $methodDescriptors;
}
/**
* @param string $methodName
* @param Message $request
* @param array $optionalArgs {
* Call Options
*
* @type array $headers [optional] key-value array containing headers
* @type int $timeoutMillis [optional] the timeout in milliseconds for the call
* @type array $transportOptions [optional] transport-specific call options
* @type RetrySettings|array $retrySettings [optional] A retry settings override for the call.
* }
*
* @experimental
*
* @return PromiseInterface
*/
private function startAsyncCall(
string $methodName,
Message $request,
array $optionalArgs = []
) {
// Convert method name to the UpperCamelCase of RPC names from lowerCamelCase of GAPIC method names
// in order to find the method in the descriptor config.
$methodName = ucfirst($methodName);
$methodDescriptors = $this->validateCallConfig($methodName);
$callType = $methodDescriptors['callType'];
switch ($callType) {
case Call::PAGINATED_CALL:
return $this->getPagedListResponseAsync(
$methodName,
$optionalArgs,
$methodDescriptors['responseType'],
$request,
$methodDescriptors['interfaceOverride'] ?? $this->serviceName
);
case Call::SERVER_STREAMING_CALL:
case Call::CLIENT_STREAMING_CALL:
case Call::BIDI_STREAMING_CALL:
throw new ValidationException("Call type '$callType' of requested method " .
"'$methodName' is not supported for async execution.");
}
return $this->startApiCall($methodName, $request, $optionalArgs);
}
/**
* @param string $methodName
* @param Message $request
* @param array $optionalArgs {
* Call Options
*
* @type array $headers [optional] key-value array containing headers
* @type int $timeoutMillis [optional] the timeout in milliseconds for the call
* @type array $transportOptions [optional] transport-specific call options
* @type RetrySettings|array $retrySettings [optional] A retry settings
* override for the call.
* }
*
* @experimental
*
* @return PromiseInterface|PagedListResponse|BidiStream|ClientStream|ServerStream
*/
private function startApiCall(
string $methodName,
Message $request = null,
array $optionalArgs = []
) {
$methodDescriptors =$this->validateCallConfig($methodName);
$callType = $methodDescriptors['callType'];
// Prepare request-based headers, merge with user-provided headers,
// which take precedence.
$headerParams = $methodDescriptors['headerParams'] ?? [];
$requestHeaders = $this->buildRequestParamsHeader($headerParams, $request);
$optionalArgs['headers'] = array_merge($requestHeaders, $optionalArgs['headers'] ?? []);
// Default the interface name, if not set, to the client's protobuf service name.
$interfaceName = $methodDescriptors['interfaceOverride'] ?? $this->serviceName;
// Handle call based on call type configured in the method descriptor config.
if ($callType == Call::LONGRUNNING_CALL) {
return $this->startOperationsCall(
$methodName,
$optionalArgs,
$request,
$this->getOperationsClient(),
$interfaceName,
// Custom operations will define their own operation response type, whereas standard
// LRO defaults to the same type.
$methodDescriptors['responseType'] ?? null
);
}
// Fully-qualified name of the response message PHP class.
$decodeType = $methodDescriptors['responseType'];
if ($callType == Call::PAGINATED_CALL) {
return $this->getPagedListResponse($methodName, $optionalArgs, $decodeType, $request, $interfaceName);
}
// Unary, and all Streaming types handled by startCall.
return $this->startCall($methodName, $decodeType, $optionalArgs, $request, $callType, $interfaceName);
}
/**
* @param string $methodName
* @param string $decodeType
* @param array $optionalArgs {
* Call Options
*
* @type array $headers [optional] key-value array containing headers
* @type int $timeoutMillis [optional] the timeout in milliseconds for the call
* @type array $transportOptions [optional] transport-specific call options
* @type RetrySettings|array $retrySettings [optional] A retry settings
* override for the call.
* }
* @param Message $request
* @param int $callType
* @param string $interfaceName
*
* @return PromiseInterface|BidiStream|ClientStream|ServerStream
*/
private function startCall(
string $methodName,
string $decodeType,
array $optionalArgs = [],
Message $request = null,
int $callType = Call::UNARY_CALL,
string $interfaceName = null
) {
$optionalArgs = $this->configureCallOptions($optionalArgs);
$callStack = $this->createCallStack(
$this->configureCallConstructionOptions($methodName, $optionalArgs)
);
$descriptor = $this->descriptors[$methodName]['grpcStreaming'] ?? null;
$call = new Call(
$this->buildMethod($interfaceName, $methodName),
$decodeType,
$request,
$descriptor,
$callType
);
switch ($callType) {
case Call::UNARY_CALL:
$this->modifyUnaryCallable($callStack);
break;
case Call::BIDI_STREAMING_CALL:
case Call::CLIENT_STREAMING_CALL:
case Call::SERVER_STREAMING_CALL:
$this->modifyStreamingCallable($callStack);
break;
}
return $callStack($call, $optionalArgs + array_filter([
'audience' => self::getDefaultAudience()
]));
}
/**
* @param array $callConstructionOptions {
* Call Construction Options
*
* @type RetrySettings $retrySettings [optional] A retry settings override
* For the call.
* }
*
* @return callable
*/
private function createCallStack(array $callConstructionOptions)
{
$quotaProject = $this->credentialsWrapper->getQuotaProject();
$fixedHeaders = $this->agentHeader;
if ($quotaProject) {
$fixedHeaders += [
'X-Goog-User-Project' => [$quotaProject]
];
}
$callStack = function (Call $call, array $options) {
$startCallMethod = $this->transportCallMethods[$call->getCallType()];
return $this->transport->$startCallMethod($call, $options);
};
$callStack = new CredentialsWrapperMiddleware($callStack, $this->credentialsWrapper);
$callStack = new FixedHeaderMiddleware($callStack, $fixedHeaders, true);
$callStack = new RetryMiddleware($callStack, $callConstructionOptions['retrySettings']);
$callStack = new OptionsFilterMiddleware($callStack, [
'headers',
'timeoutMillis',
'transportOptions',
'metadataCallback',
'audience',
'metadataReturnType'
]);
foreach (\array_reverse($this->middlewareCallables) as $fn) {
/** @var MiddlewareInterface $callStack */
$callStack = $fn($callStack);
}
return $callStack;
}
/**
* @param string $methodName
* @param array $optionalArgs {
* Optional arguments
*
* @type RetrySettings|array $retrySettings [optional] A retry settings
* override for the call.
* }
*
* @return array
*/
private function configureCallConstructionOptions(string $methodName, array $optionalArgs)
{
$retrySettings = $this->retrySettings[$methodName];
// Allow for retry settings to be changed at call time
if (isset($optionalArgs['retrySettings'])) {
if ($optionalArgs['retrySettings'] instanceof RetrySettings) {
$retrySettings = $optionalArgs['retrySettings'];
} else {
$retrySettings = $retrySettings->with(
$optionalArgs['retrySettings']
);
}
}
return [
'retrySettings' => $retrySettings,
];
}
/**
* @return array
*/
private function configureCallOptions(array $optionalArgs): array
{
if ($this->isNewClientSurface()) {
// cast to CallOptions for new surfaces only
return (new CallOptions($optionalArgs))->toArray();
}
return $optionalArgs;
}
/**
* @param string $methodName
* @param array $optionalArgs {
* Call Options
*
* @type array $headers [optional] key-value array containing headers
* @type int $timeoutMillis [optional] the timeout in milliseconds for the call
* @type array $transportOptions [optional] transport-specific call options
* }
* @param Message $request
* @param OperationsClient|object $client
* @param string $interfaceName
* @param string $operationClass If provided, will be used instead of the default
* operation response class of {@see \Google\LongRunning\Operation}.
*
* @return PromiseInterface
*/
private function startOperationsCall(
string $methodName,
array $optionalArgs,
Message $request,
$client,
string $interfaceName = null,
string $operationClass = null
) {
$optionalArgs = $this->configureCallOptions($optionalArgs);
$callStack = $this->createCallStack(
$this->configureCallConstructionOptions($methodName, $optionalArgs)
);
$descriptor = $this->descriptors[$methodName]['longRunning'];
$metadataReturnType = null;
// Call the methods supplied in "additionalArgumentMethods" on the request Message object
// to build the "additionalOperationArguments" option for the operation response.
if (isset($descriptor['additionalArgumentMethods'])) {
$additionalArgs = [];
foreach ($descriptor['additionalArgumentMethods'] as $additionalArgsMethodName) {
$additionalArgs[] = $request->$additionalArgsMethodName();
}
$descriptor['additionalOperationArguments'] = $additionalArgs;
unset($descriptor['additionalArgumentMethods']);
}
if (isset($descriptor['metadataReturnType'])) {
$metadataReturnType = $descriptor['metadataReturnType'];
}
$callStack = new OperationsMiddleware($callStack, $client, $descriptor);
$call = new Call(
$this->buildMethod($interfaceName, $methodName),
$operationClass ?: Operation::class,
$request,
[],
Call::UNARY_CALL
);
$this->modifyUnaryCallable($callStack);
return $callStack($call, $optionalArgs + array_filter([
'metadataReturnType' => $metadataReturnType,
'audience' => self::getDefaultAudience()
]));
}
/**
* @param string $methodName
* @param array $optionalArgs
* @param string $decodeType
* @param Message $request
* @param string $interfaceName
*
* @return PagedListResponse
*/
private function getPagedListResponse(
string $methodName,
array $optionalArgs,
string $decodeType,
Message $request,
string $interfaceName = null
) {
return $this->getPagedListResponseAsync(
$methodName,
$optionalArgs,
$decodeType,
$request,
$interfaceName
)->wait();
}
/**
* @param string $methodName
* @param array $optionalArgs
* @param string $decodeType
* @param Message $request
* @param string $interfaceName
*
* @return PromiseInterface
*/
private function getPagedListResponseAsync(
string $methodName,
array $optionalArgs,
string $decodeType,
Message $request,
string $interfaceName = null
) {
$optionalArgs = $this->configureCallOptions($optionalArgs);
$callStack = $this->createCallStack(
$this->configureCallConstructionOptions($methodName, $optionalArgs)
);
$descriptor = new PageStreamingDescriptor(
$this->descriptors[$methodName]['pageStreaming']
);
$callStack = new PagedMiddleware($callStack, $descriptor);
$call = new Call(
$this->buildMethod($interfaceName, $methodName),
$decodeType,
$request,
[],
Call::UNARY_CALL
);
$this->modifyUnaryCallable($callStack);
return $callStack($call, $optionalArgs + array_filter([
'audience' => self::getDefaultAudience()
]));
}
/**
* @param string $interfaceName
* @param string $methodName
*
* @return string
*/
private function buildMethod(string $interfaceName = null, string $methodName = null)
{
return sprintf(
'%s/%s',
$interfaceName ?: $this->serviceName,
$methodName
);
}
/**
* @param array $headerParams
* @param Message|null $request
*
* @return array
*/
private function buildRequestParamsHeader(array $headerParams, Message $request = null)
{
$headers = [];
// No request message means no request-based headers.
if (!$request) {
return $headers;
}
foreach ($headerParams as $headerParam) {
$msg = $request;
$value = null;
foreach ($headerParam['fieldAccessors'] as $accessor) {
$value = $msg->$accessor();
// In case the field in question is nested in another message,
// skip the header param when the nested message field is unset.
$msg = $value;
if (is_null($msg)) {
break;
}
}
$keyName = $headerParam['keyName'];
// If there are value pattern matchers configured and the target
// field was set, evaluate the matchers in the order that they were
// annotated in with last one matching wins.
$original = $value;
$matchers = isset($headerParam['matchers']) && !is_null($value) ?
$headerParam['matchers'] :
[];
foreach ($matchers as $matcher) {
$matches = [];
if (preg_match($matcher, $original, $matches)) {
$value = $matches[$keyName];
}
}
// If there are no matches or the target field was unset, skip this
// header param.
if (!$value) {
continue;
}
$headers[$keyName] = $value;
}
$requestParams = new RequestParamsHeaderDescriptor($headers);
return $requestParams->getHeader();
}
/**
* The SERVICE_ADDRESS constant is set by GAPIC clients
*/
private static function getDefaultAudience()
{
if (!defined('self::SERVICE_ADDRESS')) {
return null;
}
return 'https://' . self::SERVICE_ADDRESS . '/'; // @phpstan-ignore-line
}
/**
* This defaults to all three transports, which One-Platform supports.
* Discovery clients should define this function and only return ['rest'].
*/
private static function supportedTransports()
{
return ['grpc', 'grpc-fallback', 'rest'];
}
// Gapic Client Extension Points
// The methods below provide extension points that can be used to customize client
// functionality. These extension points are currently considered
// private and may change at any time.
/**
* Modify options passed to the client before calling setClientOptions.
*
* @param array $options
* @access private
* @internal
*/
protected function modifyClientOptions(array &$options)
{
// Do nothing - this method exists to allow option modification by partial veneers.
}
/**
* Modify the unary callable.
*
* @param callable $callable
* @access private
*/
protected function modifyUnaryCallable(callable &$callable)
{
// Do nothing - this method exists to allow callable modification by partial veneers.
}
/**
* Modify the streaming callable.
*
* @param callable $callable
* @access private
*/
protected function modifyStreamingCallable(callable &$callable)
{
// Do nothing - this method exists to allow callable modification by partial veneers.
}
/**
* @internal
*/
private function isNewClientSurface(): bool
{
return $this->isNewClient ?? $this->isNewClient = substr(__CLASS__, -11) !== 'GapicClient';
}
}
src/GrpcSupportTrait.php 0000644 00000004372 15174273622 0011357 0 ustar 00 nextHandler = $nextHandler;
$this->credentialsWrapper = $credentialsWrapper;
}
public function __invoke(Call $call, array $options)
{
$next = $this->nextHandler;
return $next(
$call,
$options + ['credentialsWrapper' => $this->credentialsWrapper]
);
}
}
src/Middleware/FixedHeaderMiddleware.php 0000644 00000005100 15174273622 0014274 0 ustar 00 nextHandler = $nextHandler;
$this->headers = $headers;
$this->overrideUserHeaders = $overrideUserHeaders;
}
public function __invoke(Call $call, array $options)
{
$userHeaders = $options['headers'] ?? [];
if ($this->overrideUserHeaders) {
$options['headers'] = $this->headers + $userHeaders;
} else {
$options['headers'] = $userHeaders + $this->headers;
}
$next = $this->nextHandler;
return $next(
$call,
$options
);
}
}
src/Middleware/MiddlewareInterface.php 0000644 00000006650 15174273622 0014037 0 ustar 00 handler = $handler;
* }
* public function __invoke(Call $call, array $options)
* {
* echo "Logging info about the call: " . $call->getMethod();
* return ($this->handler)($call, $options);
* }
* }
* ```
*
* Next, add the middleware to any class implementing `GapicClientTrait` by passing in a
* callable which returns the new middleware:
*
* ```
* $client = new ExampleGoogleApiServiceClient();
* $client->addMiddleware(function (MiddlewareInterface $handler) {
* return new MyTestMiddleware($handler);
* });
* ```
*/
interface MiddlewareInterface
{
/**
* Modify or observe the API call request and response.
* The returned value must include the result of the next MiddlewareInterface invocation in the
* chain.
*
* @param Call $call
* @param array $options
* @return PromiseInterface|ClientStream|ServerStream|BidiStream
*/
public function __invoke(Call $call, array $options);
}
src/Middleware/OperationsMiddleware.php 0000644 00000005464 15174273622 0014264 0 ustar 00 nextHandler = $nextHandler;
$this->operationsClient = $operationsClient;
$this->descriptor = $descriptor;
}
public function __invoke(Call $call, array $options)
{
$next = $this->nextHandler;
return $next(
$call,
$options
)->then(function (Message $response) {
$options = $this->descriptor + [
'lastProtoResponse' => $response
];
$operationNameMethod = $options['operationNameMethod'] ?? 'getName';
$operationName = call_user_func([$response, $operationNameMethod]);
return new OperationResponse($operationName, $this->operationsClient, $options);
});
}
}
src/Middleware/OptionsFilterMiddleware.php 0000644 00000004546 15174273622 0014742 0 ustar 00 nextHandler = $nextHandler;
$this->permittedOptions = $permittedOptions;
}
public function __invoke(Call $call, array $options)
{
$next = $this->nextHandler;
$filteredOptions = $this->pluckArray($this->permittedOptions, $options);
return $next(
$call,
$filteredOptions
);
}
}
src/Middleware/PagedMiddleware.php 0000644 00000005544 15174273622 0013160 0 ustar 00 nextHandler = $nextHandler;
$this->descriptor = $descriptor;
}
public function __invoke(Call $call, array $options)
{
$next = $this->nextHandler;
$descriptor = $this->descriptor;
return $next($call, $options)->then(
function (Message $response) use ($call, $next, $options, $descriptor) {
$page = new Page(
$call,
$options,
$next,
$descriptor,
$response
);
return new PagedListResponse($page);
}
);
}
}
src/Middleware/ResponseMetadataMiddleware.php 0000644 00000005250 15174273622 0015371 0 ustar 00 nextHandler = $nextHandler;
}
public function __invoke(Call $call, array $options)
{
$metadataReceiver = new Promise();
$options['metadataCallback'] = function ($metadata) use ($metadataReceiver) {
$metadataReceiver->resolve($metadata);
};
$next = $this->nextHandler;
return $next($call, $options)->then(
function ($response) use ($metadataReceiver) {
if ($metadataReceiver->getState() === PromiseInterface::FULFILLED) {
return [$response, $metadataReceiver->wait()];
} else {
return [$response, []];
}
}
);
}
}
src/Middleware/RetryMiddleware.php 0000644 00000015645 15174273622 0013250 0 ustar 00 nextHandler = $nextHandler;
$this->retrySettings = $retrySettings;
$this->deadlineMs = $deadlineMs;
$this->retryAttempts = $retryAttempts;
}
/**
* @param Call $call
* @param array $options
*
* @return PromiseInterface
*/
public function __invoke(Call $call, array $options)
{
$nextHandler = $this->nextHandler;
if (!isset($options['timeoutMillis'])) {
// default to "noRetriesRpcTimeoutMillis" when retries are disabled, otherwise use "initialRpcTimeoutMillis"
if (!$this->retrySettings->retriesEnabled() && $this->retrySettings->getNoRetriesRpcTimeoutMillis() > 0) {
$options['timeoutMillis'] = $this->retrySettings->getNoRetriesRpcTimeoutMillis();
} elseif ($this->retrySettings->getInitialRpcTimeoutMillis() > 0) {
$options['timeoutMillis'] = $this->retrySettings->getInitialRpcTimeoutMillis();
}
}
// Call the handler immediately if retry settings are disabled.
if (!$this->retrySettings->retriesEnabled()) {
return $nextHandler($call, $options);
}
return $nextHandler($call, $options)->then(null, function ($e) use ($call, $options) {
$retryFunction = $this->getRetryFunction();
// If the number of retries has surpassed the max allowed retries
// then throw the exception as we normally would.
// If the maxRetries is set to 0, then we don't check this condition.
if (0 !== $this->retrySettings->getMaxRetries()
&& $this->retryAttempts >= $this->retrySettings->getMaxRetries()
) {
throw $e;
}
// If the retry function returns false then throw the
// exception as we normally would.
if (!$retryFunction($e, $options)) {
throw $e;
}
// Retry function returned true, so we attempt another retry
return $this->retry($call, $options, $e->getStatus());
});
}
/**
* @param Call $call
* @param array $options
* @param string $status
*
* @return PromiseInterface
* @throws ApiException
*/
private function retry(Call $call, array $options, string $status)
{
$delayMult = $this->retrySettings->getRetryDelayMultiplier();
$maxDelayMs = $this->retrySettings->getMaxRetryDelayMillis();
$timeoutMult = $this->retrySettings->getRpcTimeoutMultiplier();
$maxTimeoutMs = $this->retrySettings->getMaxRpcTimeoutMillis();
$totalTimeoutMs = $this->retrySettings->getTotalTimeoutMillis();
$delayMs = $this->retrySettings->getInitialRetryDelayMillis();
$timeoutMs = $options['timeoutMillis'];
$currentTimeMs = $this->getCurrentTimeMs();
$deadlineMs = $this->deadlineMs ?: $currentTimeMs + $totalTimeoutMs;
if ($currentTimeMs >= $deadlineMs) {
throw new ApiException(
'Retry total timeout exceeded.',
\Google\Rpc\Code::DEADLINE_EXCEEDED,
ApiStatus::DEADLINE_EXCEEDED
);
}
$delayMs = min($delayMs * $delayMult, $maxDelayMs);
$timeoutMs = (int) min(
$timeoutMs * $timeoutMult,
$maxTimeoutMs,
$deadlineMs - $this->getCurrentTimeMs()
);
$nextHandler = new RetryMiddleware(
$this->nextHandler,
$this->retrySettings->with([
'initialRetryDelayMillis' => $delayMs,
]),
$deadlineMs,
$this->retryAttempts + 1
);
// Set the timeout for the call
$options['timeoutMillis'] = $timeoutMs;
return $nextHandler(
$call,
$options
);
}
protected function getCurrentTimeMs()
{
return microtime(true) * 1000.0;
}
/**
* This is the default retry behaviour.
*/
private function getRetryFunction()
{
return $this->retrySettings->getRetryFunction() ??
function (\Throwable $e, array $options): bool {
// This is the default retry behaviour, i.e. we don't retry an ApiException
// and for other exception types, we only retry when the error code is in
// the list of retryable error codes.
if (!$e instanceof ApiException) {
return false;
}
if (!in_array($e->getStatus(), $this->retrySettings->getRetryableCodes())) {
return false;
}
return true;
};
}
}
src/OperationResponse.php 0000644 00000044772 15174273622 0011552 0 ustar 00 self::DEFAULT_POLLING_INTERVAL,
'pollDelayMultiplier' => self::DEFAULT_POLLING_MULTIPLIER,
'maxPollDelayMillis' => self::DEFAULT_MAX_POLLING_INTERVAL,
'totalPollTimeoutMillis' => self::DEFAULT_MAX_POLLING_DURATION,
];
private ?object $lastProtoResponse;
private bool $deleted = false;
private array $additionalArgs;
private string $getOperationMethod;
private ?string $cancelOperationMethod;
private ?string $deleteOperationMethod;
private string $operationStatusMethod;
/** @var mixed */
private $operationStatusDoneValue;
private ?string $operationErrorCodeMethod;
private ?string $operationErrorMessageMethod;
/**
* OperationResponse constructor.
*
* @param string $operationName
* @param object $operationsClient
* @param array $options {
* Optional. Options for configuring the operation response object.
*
* @type string $operationReturnType The return type of the longrunning operation.
* @type string $metadataReturnType The type of the metadata returned in the operation response.
* @type int $initialPollDelayMillis The initial polling interval to use, in milliseconds.
* @type int $pollDelayMultiplier Multiplier applied to the polling interval on each retry.
* @type int $maxPollDelayMillis The maximum polling interval to use, in milliseconds.
* @type int $totalPollTimeoutMillis The maximum amount of time to continue polling.
* @type object $lastProtoResponse A response already received from the server.
* @type string $getOperationMethod The method on $operationsClient to get the operation.
* @type string $cancelOperationMethod The method on $operationsClient to cancel the operation.
* @type string $deleteOperationMethod The method on $operationsClient to delete the operation.
* @type string $operationStatusMethod The method on the operation to get the status.
* @type string $operationStatusDoneValue The method on the operation to determine if the status is done.
* @type array $additionalOperationArguments Additional arguments to pass to $operationsClient methods.
* @type string $operationErrorCodeMethod The method on the operation to get the error code
* @type string $operationErrorMessageMethod The method on the operation to get the error status
* }
*/
public function __construct(string $operationName, $operationsClient, array $options = [])
{
$this->operationName = $operationName;
$this->operationsClient = $operationsClient;
$options += [
'operationReturnType' => null,
'metadataReturnType' => null,
'lastProtoResponse' => null,
'getOperationMethod' => 'getOperation',
'cancelOperationMethod' => 'cancelOperation',
'deleteOperationMethod' => 'deleteOperation',
'operationStatusMethod' => 'getDone',
'operationStatusDoneValue' => true,
'additionalOperationArguments' => [],
'operationErrorCodeMethod' => null,
'operationErrorMessageMethod' => null,
];
$this->operationReturnType = $options['operationReturnType'];
$this->metadataReturnType = $options['metadataReturnType'];
$this->lastProtoResponse = $options['lastProtoResponse'];
$this->getOperationMethod = $options['getOperationMethod'];
$this->cancelOperationMethod = $options['cancelOperationMethod'];
$this->deleteOperationMethod = $options['deleteOperationMethod'];
$this->additionalArgs = $options['additionalOperationArguments'];
$this->operationStatusMethod = $options['operationStatusMethod'];
$this->operationStatusDoneValue = $options['operationStatusDoneValue'];
$this->operationErrorCodeMethod = $options['operationErrorCodeMethod'];
$this->operationErrorMessageMethod = $options['operationErrorMessageMethod'];
if (isset($options['initialPollDelayMillis'])) {
$this->defaultPollSettings['initialPollDelayMillis'] = $options['initialPollDelayMillis'];
}
if (isset($options['pollDelayMultiplier'])) {
$this->defaultPollSettings['pollDelayMultiplier'] = $options['pollDelayMultiplier'];
}
if (isset($options['maxPollDelayMillis'])) {
$this->defaultPollSettings['maxPollDelayMillis'] = $options['maxPollDelayMillis'];
}
if (isset($options['totalPollTimeoutMillis'])) {
$this->defaultPollSettings['totalPollTimeoutMillis'] = $options['totalPollTimeoutMillis'];
}
}
/**
* Check whether the operation has completed.
*
* @return bool
*/
public function isDone()
{
if (!$this->hasProtoResponse()) {
return false;
}
$status = call_user_func([$this->lastProtoResponse, $this->operationStatusMethod]);
if (is_null($status)) {
return false;
}
return $status === $this->operationStatusDoneValue;
}
/**
* Check whether the operation completed successfully. If the operation is not complete, or if the operation
* failed, return false.
*
* @return bool
*/
public function operationSucceeded()
{
if (!$this->hasProtoResponse()) {
return false;
}
if (!$this->canHaveResult()) {
// For Operations which do not have a result, we consider a successful
// operation when the operation has completed without errors.
return $this->isDone() && !$this->hasErrors();
}
return !is_null($this->getResult());
}
/**
* Check whether the operation failed. If the operation is not complete, or if the operation
* succeeded, return false.
*
* @return bool
*/
public function operationFailed()
{
return $this->hasErrors();
}
/**
* Get the formatted name of the operation
*
* @return string The formatted name of the operation
*/
public function getName()
{
return $this->operationName;
}
/**
* Poll the server in a loop until the operation is complete.
*
* Return true if the operation completed, otherwise return false. If the
* $options['totalPollTimeoutMillis'] setting is not set (or set <= 0) then
* pollUntilComplete will continue polling until the operation completes,
* and therefore will always return true.
*
* @param array $options {
* Options for configuring the polling behaviour.
*
* @type int $initialPollDelayMillis The initial polling interval to use, in milliseconds.
* @type int $pollDelayMultiplier Multiplier applied to the polling interval on each retry.
* @type int $maxPollDelayMillis The maximum polling interval to use, in milliseconds.
* @type int $totalPollTimeoutMillis The maximum amount of time to continue polling, in milliseconds.
* }
* @throws ApiException If an API call fails.
* @throws ValidationException
* @return bool Indicates if the operation completed.
*/
public function pollUntilComplete(array $options = [])
{
if ($this->isDone()) {
return true;
}
$pollSettings = array_merge($this->defaultPollSettings, $options);
return $this->poll(function () {
$this->reload();
return $this->isDone();
}, $pollSettings);
}
/**
* Reload the status of the operation with a request to the service.
*
* @throws ApiException If the API call fails.
* @throws ValidationException If called on a deleted operation.
*/
public function reload()
{
if ($this->deleted) {
throw new ValidationException("Cannot call reload() on a deleted operation");
}
$this->lastProtoResponse = $this->operationsCall(
$this->getOperationMethod,
$this->getName(),
$this->additionalArgs
);
}
/**
* Return the result of the operation. If operationSucceeded() is false, return null.
*
* @return mixed|null The result of the operation, or null if operationSucceeded() is false
*/
public function getResult()
{
if (!$this->hasProtoResponse()) {
return null;
}
if (!$this->canHaveResult()) {
return null;
}
if (!$this->isDone()) {
return null;
}
/** @var Any|null $anyResponse */
$anyResponse = $this->lastProtoResponse->getResponse();
if (is_null($anyResponse)) {
return null;
}
if (is_null($this->operationReturnType)) {
return $anyResponse;
}
$operationReturnType = $this->operationReturnType;
/** @var Message $response */
$response = new $operationReturnType();
$response->mergeFromString($anyResponse->getValue());
return $response;
}
/**
* If the operation failed, return the status. If operationFailed() is false, return null.
*
* @return Status|null The status of the operation in case of failure, or null if
* operationFailed() is false.
*/
public function getError()
{
if (!$this->hasProtoResponse() || !$this->isDone()) {
return null;
}
if ($this->operationErrorCodeMethod || $this->operationErrorMessageMethod) {
$errorCode = $this->operationErrorCodeMethod
? call_user_func([$this->lastProtoResponse, $this->operationErrorCodeMethod])
: null;
$errorMessage = $this->operationErrorMessageMethod
? call_user_func([$this->lastProtoResponse, $this->operationErrorMessageMethod])
: null;
return (new Status())
->setCode(ApiStatus::rpcCodeFromHttpStatusCode($errorCode))
->setMessage($errorMessage);
}
if (method_exists($this->lastProtoResponse, 'getError')) {
return $this->lastProtoResponse->getError();
}
return null;
}
/**
* Get an array containing the values of 'operationReturnType', 'metadataReturnType', and
* the polling options `initialPollDelayMillis`, `pollDelayMultiplier`, `maxPollDelayMillis`,
* and `totalPollTimeoutMillis`. The array can be passed as the $options argument to the
* constructor when creating another OperationResponse object.
*
* @return array
*/
public function getDescriptorOptions()
{
return [
'operationReturnType' => $this->operationReturnType,
'metadataReturnType' => $this->metadataReturnType,
] + $this->defaultPollSettings;
}
/**
* @return Operation|mixed|null The last Operation object received from the server.
*/
public function getLastProtoResponse()
{
return $this->lastProtoResponse;
}
/**
* @return object The OperationsClient object used to make
* requests to the operations API.
*/
public function getOperationsClient()
{
return $this->operationsClient;
}
/**
* Cancel the long-running operation.
*
* For operations of type Google\LongRunning\Operation, this method starts
* asynchronous cancellation on a long-running operation. The server
* makes a best effort to cancel the operation, but success is not
* guaranteed. If the server doesn't support this method, it will throw an
* ApiException with code \Google\Rpc\Code::UNIMPLEMENTED. Clients can continue
* to use reload and pollUntilComplete methods to check whether the cancellation
* succeeded or whether the operation completed despite cancellation.
* On successful cancellation, the operation is not deleted; instead, it becomes
* an operation with a getError() value with a \Google\Rpc\Status code of 1,
* corresponding to \Google\Rpc\Code::CANCELLED.
*
* @throws ApiException If the API call fails.
* @throws LogicException If the API call method has not been configured
*/
public function cancel()
{
if (is_null($this->cancelOperationMethod)) {
throw new LogicException('The cancel operation is not supported by this API');
}
$this->operationsCall($this->cancelOperationMethod, $this->getName(), $this->additionalArgs);
}
/**
* Delete the long-running operation.
*
* For operations of type Google\LongRunning\Operation, this method
* indicates that the client is no longer interested in the operation result.
* It does not cancel the operation. If the server doesn't support this method,
* it will throw an ApiException with code \Google\Rpc\Code::UNIMPLEMENTED.
*
* @throws ApiException If the API call fails.
* @throws LogicException If the API call method has not been configured
*/
public function delete()
{
if (is_null($this->deleteOperationMethod)) {
throw new LogicException('The delete operation is not supported by this API');
}
$this->operationsCall($this->deleteOperationMethod, $this->getName(), $this->additionalArgs);
$this->deleted = true;
}
/**
* Get the metadata returned with the last proto response. If a metadata type was provided, then
* the return value will be of that type - otherwise, the return value will be of type Any. If
* no metadata object is available, returns null.
*
* @return mixed The metadata returned from the server in the last response.
*/
public function getMetadata()
{
if (!$this->hasProtoResponse()) {
return null;
}
if (!method_exists($this->lastProtoResponse, 'getMetadata')) {
// The call to getMetadata is only for OnePlatform LROs, and is not
// supported by other LRO GAPIC clients (e.g. Compute)
return null;
}
/** @var Any|null $any */
$any = $this->lastProtoResponse->getMetadata();
if (is_null($this->metadataReturnType)) {
return $any;
}
if (is_null($any)) {
return null;
}
// @TODO: This is probably not doing anything and can be removed in the next release.
if (is_null($any->getValue())) {
return null;
}
$metadataReturnType = $this->metadataReturnType;
/** @var Message $metadata */
$metadata = new $metadataReturnType();
$metadata->mergeFromString($any->getValue());
return $metadata;
}
private function operationsCall($method, $name, array $additionalArgs)
{
$args = array_merge([$name], $additionalArgs);
return call_user_func_array([$this->operationsClient, $method], $args);
}
private function canHaveResult()
{
// The call to getResponse is only for OnePlatform LROs, and is not
// supported by other LRO GAPIC clients (e.g. Compute)
return method_exists($this->lastProtoResponse, 'getResponse');
}
private function hasErrors()
{
if (!$this->hasProtoResponse()) {
return false;
}
if (method_exists($this->lastProtoResponse, 'getError')) {
return !empty($this->lastProtoResponse->getError());
}
if ($this->operationErrorCodeMethod) {
$errorCode = call_user_func([$this->lastProtoResponse, $this->operationErrorCodeMethod]);
return !empty($errorCode);
}
// This should never happen unless an API is misconfigured
throw new LogicException('Unable to determine operation error status for this service');
}
private function hasProtoResponse()
{
return !is_null($this->lastProtoResponse);
}
}
src/Options/CallOptions.php 0000644 00000012774 15174273622 0011752 0 ustar 00 fromArray($options);
}
/**
* Sets the array of options as class properites.
*
* @param array $arr See the constructor for the list of supported options.
*/
private function fromArray(array $arr): void
{
$this->setHeaders($arr['headers'] ?? []);
$this->setTimeoutMillis($arr['timeoutMillis'] ?? null);
$this->setTransportSpecificOptions($arr['transportOptions'] ?? []);
$this->setRetrySettings($arr['retrySettings'] ?? null);
}
/**
* @param array $headers
*/
public function setHeaders(array $headers)
{
$this->headers = $headers;
}
/**
* @param int|null $timeoutMillis
*/
public function setTimeoutMillis(?int $timeoutMillis)
{
$this->timeoutMillis = $timeoutMillis;
}
/**
* @param array $transportSpecificOptions {
* Transport-specific call-time options.
*
* @type array $grpcOptions
* Key-value pairs for gRPC-specific options passed as the `$options` argument to {@see \Grpc\BaseStub}
* request methods. Current options are `call_credentials_callback` and `timeout`.
* **NOTE**: This library sets `call_credentials_callback` using {@see CredentialsWrapper}, and `timeout`
* using the `timeoutMillis` call option, so these options are not very useful.
* @type array $grpcFallbackOptions
* Key-value pairs for gRPC fallback specific options passed as the `$options` argument to the
* `$httpHandler` callable. By default these are passed to {@see \GuzzleHttp\Client} as request options.
* See {@link https://docs.guzzlephp.org/en/stable/request-options.html}.
* @type array $restOptions
* Key-value pairs for REST-specific options passed as the `$options` argument to the `$httpHandler`
* callable. By default these are passed to {@see \GuzzleHttp\Client} as request options.
* See {@link https://docs.guzzlephp.org/en/stable/request-options.html}.
* }
*/
public function setTransportSpecificOptions(array $transportSpecificOptions)
{
$this->transportSpecificOptions = $transportSpecificOptions;
}
/**
* @param RetrySettings|array|null $retrySettings
*/
public function setRetrySettings($retrySettings)
{
$this->retrySettings = $retrySettings;
}
}
src/Options/ClientOptions.php 0000644 00000026262 15174273622 0012312 0 ustar 00 '/path/to/my/credentials.json'
* ]);
* $secretManager = new SecretManagerClient($options->toArray());
* ```
*
* Note: It's possible to pass an associative array to the API clients as well,
* as ClientOptions will still be used internally for validation.
*/
class ClientOptions implements ArrayAccess
{
use OptionsTrait;
private ?string $apiEndpoint;
private bool $disableRetries;
private array $clientConfig;
/** @var string|array|FetchAuthTokenInterface|CredentialsWrapper|null */
private $credentials;
private array $credentialsConfig;
/** @var string|TransportInterface|null $transport */
private $transport;
private TransportOptions $transportConfig;
private ?string $versionFile;
private ?string $descriptorsConfigPath;
private ?string $serviceName;
private ?string $libName;
private ?string $libVersion;
private ?string $gapicVersion;
private ?Closure $clientCertSource;
private ?string $universeDomain;
/**
* @param array $options {
* @type string $apiEndpoint
* The address of the API remote host, for example "example.googleapis.com. May also
* include the port, for example "example.googleapis.com:443"
* @type bool $disableRetries
* Determines whether or not retries defined by the client configuration should be
* disabled. Defaults to `false`.
* @type string|array $clientConfig
* Client method configuration, including retry settings. This option can be either a
* path to a JSON file, or a PHP array containing the decoded JSON data.
* By default this settings points to the default client config file, which is provided
* in the resources folder.
* @type string|array|FetchAuthTokenInterface|CredentialsWrapper $credentials
* The credentials to be used by the client to authorize API calls. This option
* accepts either a path to a credentials file, or a decoded credentials file as a
* PHP array.
* *Advanced usage*: In addition, this option can also accept a pre-constructed
* \Google\Auth\FetchAuthTokenInterface object or \Google\ApiCore\CredentialsWrapper
* object. Note that when one of these objects are provided, any settings in
* $authConfig will be ignored.
* @type array $credentialsConfig
* Options used to configure credentials, including auth token caching, for the client.
* For a full list of supporting configuration options, see
* \Google\ApiCore\CredentialsWrapper::build.
* @type string|TransportInterface|null $transport
* The transport used for executing network requests. May be either the string `rest`,
* `grpc`, or 'grpc-fallback'. Defaults to `grpc` if gRPC support is detected on the system.
* *Advanced usage*: Additionally, it is possible to pass in an already instantiated
* TransportInterface object. Note that when this objects is provided, any settings in
* $transportConfig, and any `$apiEndpoint` setting, will be ignored.
* @type array $transportConfig
* Configuration options that will be used to construct the transport. Options for
* each supported transport type should be passed in a key for that transport. For
* example:
* $transportConfig = [
* 'grpc' => [...],
* 'rest' => [...],
* 'grpc-fallback' => [...],
* ];
* See the GrpcTransport::build and RestTransport::build
* methods for the supported options.
* @type string $versionFile
* The path to a file which contains the current version of the client.
* @type string $descriptorsConfigPath
* The path to a descriptor configuration file.
* @type string $serviceName
* The name of the service.
* @type string $libName
* The name of the client application.
* @type string $libVersion
* The version of the client application.
* @type string $gapicVersion
* The code generator version of the GAPIC library.
* @type callable $clientCertSource
* A callable which returns the client cert as a string.
* @type string $universeDomain
* The default service domain for a given Cloud universe.
* }
*/
public function __construct(array $options)
{
$this->fromArray($options);
}
/**
* Sets the array of options as class properites.
*
* @param array $arr See the constructor for the list of supported options.
*/
private function fromArray(array $arr): void
{
$this->setApiEndpoint($arr['apiEndpoint'] ?? null);
$this->setDisableRetries($arr['disableRetries'] ?? false);
$this->setClientConfig($arr['clientConfig'] ?? []);
$this->setCredentials($arr['credentials']);
$this->setCredentialsConfig($arr['credentialsConfig'] ?? []);
$this->setTransport($arr['transport'] ?? null);
$this->setTransportConfig(new TransportOptions($arr['transportConfig'] ?? []));
$this->setVersionFile($arr['versionFile'] ?? null);
$this->setDescriptorsConfigPath($arr['descriptorsConfigPath']);
$this->setServiceName($arr['serviceName'] ?? null);
$this->setLibName($arr['libName'] ?? null);
$this->setLibVersion($arr['libVersion'] ?? null);
$this->setGapicVersion($arr['gapicVersion'] ?? null);
$this->setClientCertSource($arr['clientCertSource'] ?? null);
$this->setUniverseDomain($arr['universeDomain'] ?? null);
}
/**
* @param ?string $apiEndpoint
*/
public function setApiEndpoint(?string $apiEndpoint): void
{
$this->apiEndpoint = $apiEndpoint;
}
/**
* @param bool $disableRetries
*/
public function setDisableRetries(bool $disableRetries): void
{
$this->disableRetries = $disableRetries;
}
/**
* @param string|array $clientConfig
* @throws InvalidArgumentException
*/
public function setClientConfig($clientConfig): void
{
if (is_string($clientConfig)) {
$this->clientConfig = json_decode(file_get_contents($clientConfig), true);
} elseif (is_array($clientConfig)) {
$this->clientConfig = $clientConfig;
} else {
throw new InvalidArgumentException('Invalid client config');
}
}
/**
* @param string|array|FetchAuthTokenInterface|CredentialsWrapper|null $credentials
*/
public function setCredentials($credentials): void
{
$this->credentials = $credentials;
}
/**
* @param array $credentialsConfig
*/
public function setCredentialsConfig(array $credentialsConfig): void
{
$this->credentialsConfig = $credentialsConfig;
}
/**
* @param string|TransportInterface|null $transport
*/
public function setTransport($transport): void
{
$this->transport = $transport;
}
/**
* @param TransportOptions $transportConfig
*/
public function setTransportConfig(TransportOptions $transportConfig): void
{
$this->transportConfig = $transportConfig;
}
/**
* @param ?string $versionFile
*/
public function setVersionFile(?string $versionFile): void
{
$this->versionFile = $versionFile;
}
/**
* @param ?string $descriptorsConfigPath
*/
private function setDescriptorsConfigPath(?string $descriptorsConfigPath)
{
if (!is_null($descriptorsConfigPath)) {
self::validateFileExists($descriptorsConfigPath);
}
$this->descriptorsConfigPath = $descriptorsConfigPath;
}
/**
* @param ?string $serviceName
*/
public function setServiceName(?string $serviceName): void
{
$this->serviceName = $serviceName;
}
/**
* @param ?string $libName
*/
public function setLibName(?string $libName): void
{
$this->libName = $libName;
}
/**
* @param ?string $libVersion
*/
public function setLibVersion(?string $libVersion): void
{
$this->libVersion = $libVersion;
}
/**
* @param ?string $gapicVersion
*/
public function setGapicVersion(?string $gapicVersion): void
{
$this->gapicVersion = $gapicVersion;
}
/**
* @param ?callable $clientCertSource
*/
public function setClientCertSource(?callable $clientCertSource)
{
if (!is_null($clientCertSource)) {
$this->clientCertSource = Closure::fromCallable($clientCertSource);
}
$this->clientCertSource = $clientCertSource;
}
/**
* @param string $universeDomain
*/
public function setUniverseDomain(?string $universeDomain)
{
$this->universeDomain = $universeDomain;
}
}
src/Options/OptionsTrait.php 0000644 00000005720 15174273622 0012153 0 ustar 00 $offset);
}
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->$offset;
}
/**
* @throws BadMethodCallException
*/
public function offsetSet($offset, $value): void
{
throw new BadMethodCallException('Cannot set options through array access. Use the setters instead');
}
/**
* @throws BadMethodCallException
*/
public function offsetUnset($offset): void
{
throw new BadMethodCallException('Cannot unset options through array access. Use the setters instead');
}
public function toArray(): array
{
$arr = [];
foreach (get_object_vars($this) as $key => $value) {
$arr[$key] = $value;
}
return $arr;
}
}
src/Options/TransportOptions.php 0000644 00000006121 15174273622 0013060 0 ustar 00 fromArray($options);
}
/**
* Sets the array of options as class properites.
*
* @param array $arr See the constructor for the list of supported options.
*/
private function fromArray(array $arr): void
{
$this->setGrpc(new GrpcTransportOptions($arr['grpc'] ?? []));
$this->setGrpcFallback(new GrpcFallbackTransportOptions($arr['grpc-fallack'] ?? []));
$this->setRest(new RestTransportOptions($arr['rest'] ?? []));
}
public function setGrpc(GrpcTransportOptions $grpc): void
{
$this->grpc = $grpc;
}
public function setGrpcFallback(GrpcFallbackTransportOptions $grpcFallback): void
{
$this->grpcFallback = $grpcFallback;
}
public function setRest(RestTransportOptions $rest): void
{
$this->rest = $rest;
}
}
src/Options/TransportOptions/GrpcFallbackTransportOptions.php 0000644 00000006465 15174273622 0020677 0 ustar 00 fromArray($options);
}
/**
* Sets the array of options as class properites.
*
* @param array $arr See the constructor for the list of supported options.
*/
private function fromArray(array $arr): void
{
$this->setClientCertSource($arr['clientCertSource'] ?? null);
$this->setHttpHandler($arr['httpHandler'] ?? null);
}
public function setHttpHandler(?callable $httpHandler)
{
if (!is_null($httpHandler)) {
$this->httpHandler = Closure::fromCallable($httpHandler);
}
$this->httpHandler = $httpHandler;
}
/**
* @param ?callable $clientCertSource
*/
public function setClientCertSource(?callable $clientCertSource)
{
if (!is_null($clientCertSource)) {
$this->clientCertSource = Closure::fromCallable($clientCertSource);
}
$this->clientCertSource = $clientCertSource;
}
}
src/Options/TransportOptions/GrpcTransportOptions.php 0000644 00000011047 15174273622 0017247 0 ustar 00 fromArray($options);
}
/**
* Sets the array of options as class properites.
*
* @param array $arr See the constructor for the list of supported options.
*/
private function fromArray(array $arr): void
{
$this->setStubOpts($arr['stubOpts'] ?? []);
$this->setChannel($arr['channel'] ?? null);
$this->setInterceptors($arr['interceptors'] ?? []);
$this->setClientCertSource($arr['clientCertSource'] ?? null);
}
/**
* @param array $stubOpts
*/
public function setStubOpts(array $stubOpts)
{
$this->stubOpts = $stubOpts;
}
/**
* @param ?Channel $channel
*/
public function setChannel(?Channel $channel)
{
$this->channel = $channel;
}
/**
* @param Interceptor[]|UnaryInterceptorInterface[] $interceptors
*/
public function setInterceptors(array $interceptors)
{
$this->interceptors = $interceptors;
}
/**
* @param ?callable $clientCertSource
*/
public function setClientCertSource(?callable $clientCertSource)
{
if (!is_null($clientCertSource)) {
$this->clientCertSource = Closure::fromCallable($clientCertSource);
}
$this->clientCertSource = $clientCertSource;
}
}
src/Options/TransportOptions/RestTransportOptions.php 0000644 00000007372 15174273622 0017277 0 ustar 00 fromArray($options);
}
/**
* Sets the array of options as class properites.
*
* @param array $arr See the constructor for the list of supported options.
*/
private function fromArray(array $arr): void
{
$this->setHttpHandler($arr['httpHandler'] ?? null);
$this->setClientCertSource($arr['clientCertSource'] ?? null);
$this->setRestClientConfigPath($arr['restClientConfigPath'] ?? null);
}
/**
* @param ?callable $httpHandler
*/
public function setHttpHandler(?callable $httpHandler)
{
if (!is_null($httpHandler)) {
$this->httpHandler = Closure::fromCallable($httpHandler);
}
$this->httpHandler = $httpHandler;
}
/**
* @param ?callable $clientCertSource
*/
public function setClientCertSource(?callable $clientCertSource)
{
if (!is_null($clientCertSource)) {
$this->clientCertSource = Closure::fromCallable($clientCertSource);
}
$this->clientCertSource = $clientCertSource;
}
/**
* @param ?string $restClientConfigPath
*/
public function setRestClientConfigPath(?string $restClientConfigPath)
{
$this->restClientConfigPath = $restClientConfigPath;
}
}
src/Page.php 0000644 00000022345 15174273622 0006737 0 ustar 00 call = $call;
$this->options = $options;
$this->callable = $callable;
$this->pageStreamingDescriptor = $pageStreamingDescriptor;
$this->response = $response;
$requestPageTokenGetMethod = $this->pageStreamingDescriptor->getRequestPageTokenGetMethod();
$this->pageToken = $this->call->getMessage()->$requestPageTokenGetMethod();
}
/**
* Returns true if there are more pages that can be retrieved from the
* API.
*
* @return bool
*/
public function hasNextPage()
{
return strcmp($this->getNextPageToken(), Page::FINAL_PAGE_TOKEN) != 0;
}
/**
* Returns the next page token from the response.
*
* @return string
*/
public function getNextPageToken()
{
$responsePageTokenGetMethod = $this->pageStreamingDescriptor->getResponsePageTokenGetMethod();
return $this->getResponseObject()->$responsePageTokenGetMethod();
}
/**
* Retrieves the next Page object using the next page token.
*
* @param int|null $pageSize
* @throws ValidationException if there are no pages remaining, or if pageSize is supplied but
* is not supported by the API
* @throws ApiException if the call to fetch the next page fails.
* @return Page
*/
public function getNextPage(int $pageSize = null)
{
if (!$this->hasNextPage()) {
throw new ValidationException(
'Could not complete getNextPage operation: ' .
'there are no more pages to retrieve.'
);
}
$oldRequest = $this->getRequestObject();
$requestClass = get_class($oldRequest);
$newRequest = new $requestClass();
$newRequest->mergeFrom($oldRequest);
$requestPageTokenSetMethod = $this->pageStreamingDescriptor->getRequestPageTokenSetMethod();
$newRequest->$requestPageTokenSetMethod($this->getNextPageToken());
if (isset($pageSize)) {
if (!$this->pageStreamingDescriptor->requestHasPageSizeField()) {
throw new ValidationException(
'pageSize argument was defined, but the method does not ' .
'support a page size parameter in the optional array argument'
);
}
$requestPageSizeSetMethod = $this->pageStreamingDescriptor->getRequestPageSizeSetMethod();
$newRequest->$requestPageSizeSetMethod($pageSize);
}
$this->call = $this->call->withMessage($newRequest);
$callable = $this->callable;
$response = $callable(
$this->call,
$this->options
)->wait();
return new Page(
$this->call,
$this->options,
$this->callable,
$this->pageStreamingDescriptor,
$response
);
}
/**
* Return the number of elements in the response.
*
* @return int
*/
public function getPageElementCount()
{
$resourcesGetMethod = $this->pageStreamingDescriptor->getResourcesGetMethod();
return count($this->getResponseObject()->$resourcesGetMethod());
}
/**
* Return an iterator over the elements in the response.
*
* @return Generator
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
$resourcesGetMethod = $this->pageStreamingDescriptor->getResourcesGetMethod();
$items = $this->getResponseObject()->$resourcesGetMethod();
foreach ($items as $key => $element) {
if ($items instanceof MapField) {
yield $key => $element;
} else {
yield $element;
}
}
}
/**
* Return an iterator over Page objects, beginning with this object.
* Additional Page objects are retrieved lazily via API calls until
* all elements have been retrieved.
*
* @return Generator|array
* @throws ValidationException
* @throws ApiException
*/
public function iteratePages()
{
$currentPage = $this;
yield $this;
while ($currentPage->hasNextPage()) {
$currentPage = $currentPage->getNextPage();
yield $currentPage;
}
}
/**
* Gets the request object used to generate the Page.
*
* @return mixed|Message
*/
public function getRequestObject()
{
return $this->call->getMessage();
}
/**
* Gets the API response object.
*
* @return mixed|Message
*/
public function getResponseObject()
{
return $this->response;
}
/**
* Returns a collection of elements with a fixed size set by
* the collectionSize parameter. The collection will only contain
* fewer than collectionSize elements if there are no more
* pages to be retrieved from the server.
*
* NOTE: it is an error to call this method if an optional parameter
* to set the page size is not supported or has not been set in the
* API call that was used to create this page. It is also an error
* if the collectionSize parameter is less than the page size that
* has been set.
*
* @param int $collectionSize
* @throws ValidationException if a FixedSizeCollection of the specified size cannot be constructed
* @return FixedSizeCollection
*/
public function expandToFixedSizeCollection($collectionSize)
{
if (!$this->pageStreamingDescriptor->requestHasPageSizeField()) {
throw new ValidationException(
"FixedSizeCollection is not supported for this method, because " .
"the method does not support an optional argument to set the " .
"page size."
);
}
$request = $this->getRequestObject();
$pageSizeGetMethod = $this->pageStreamingDescriptor->getRequestPageSizeGetMethod();
$pageSize = $request->$pageSizeGetMethod();
if (is_null($pageSize)) {
throw new ValidationException(
"Error while expanding Page to FixedSizeCollection: No page size " .
"parameter found. The page size parameter must be set in the API " .
"optional arguments array, and must be less than the collectionSize " .
"parameter, in order to create a FixedSizeCollection object."
);
}
if ($pageSize > $collectionSize) {
throw new ValidationException(
"Error while expanding Page to FixedSizeCollection: collectionSize " .
"parameter is less than the page size optional argument specified in " .
"the API call. collectionSize: $collectionSize, page size: $pageSize"
);
}
return new FixedSizeCollection($this, $collectionSize);
}
}
src/PageStreamingDescriptor.php 0000644 00000014264 15174273622 0012651 0 ustar 00 descriptor = $descriptor;
}
/**
* @param array $fields {
* Required.
*
* @type string $requestPageTokenField the page token field in the request object.
* @type string $responsePageTokenField the page token field in the response object.
* @type string $resourceField the resource field in the response object.
*
* Optional.
* @type string $requestPageSizeField the page size field in the request object.
* }
* @return PageStreamingDescriptor
*/
public static function createFromFields(array $fields)
{
$requestPageToken = $fields['requestPageTokenField'];
$responsePageToken = $fields['responsePageTokenField'];
$resources = $fields['resourceField'];
$descriptor = [
'requestPageTokenGetMethod' => PageStreamingDescriptor::getMethod($requestPageToken),
'requestPageTokenSetMethod' => PageStreamingDescriptor::setMethod($requestPageToken),
'responsePageTokenGetMethod' => PageStreamingDescriptor::getMethod($responsePageToken),
'resourcesGetMethod' => PageStreamingDescriptor::getMethod($resources),
];
if (isset($fields['requestPageSizeField'])) {
$requestPageSize = $fields['requestPageSizeField'];
$descriptor['requestPageSizeGetMethod'] = PageStreamingDescriptor::getMethod($requestPageSize);
$descriptor['requestPageSizeSetMethod'] = PageStreamingDescriptor::setMethod($requestPageSize);
}
return new PageStreamingDescriptor($descriptor);
}
private static function getMethod(string $field)
{
return 'get' . ucfirst($field);
}
private static function setMethod(string $field)
{
return 'set' . ucfirst($field);
}
/**
* @return string The page token get method on the request object
*/
public function getRequestPageTokenGetMethod()
{
return $this->descriptor['requestPageTokenGetMethod'];
}
/**
* @return string The page size get method on the request object
*/
public function getRequestPageSizeGetMethod()
{
return $this->descriptor['requestPageSizeGetMethod'];
}
/**
* @return bool True if the request object has a page size field
*/
public function requestHasPageSizeField()
{
return array_key_exists('requestPageSizeGetMethod', $this->descriptor);
}
/**
* @return string The page token get method on the response object
*/
public function getResponsePageTokenGetMethod()
{
return $this->descriptor['responsePageTokenGetMethod'];
}
/**
* @return string The resources get method on the response object
*/
public function getResourcesGetMethod()
{
return $this->descriptor['resourcesGetMethod'];
}
/**
* @return string The page token set method on the request object
*/
public function getRequestPageTokenSetMethod()
{
return $this->descriptor['requestPageTokenSetMethod'];
}
/**
* @return string The page size set method on the request object
*/
public function getRequestPageSizeSetMethod()
{
return $this->descriptor['requestPageSizeSetMethod'];
}
private static function validate(array $descriptor)
{
$requiredFields = [
'requestPageTokenGetMethod',
'requestPageTokenSetMethod',
'responsePageTokenGetMethod',
'resourcesGetMethod',
];
foreach ($requiredFields as $field) {
if (empty($descriptor[$field])) {
throw new InvalidArgumentException(
"$field is required for PageStreamingDescriptor"
);
}
}
}
}
src/PagedListResponse.php 0000644 00000015434 15174273622 0011457 0 ustar 00 getList(...);
* foreach ($pagedListResponse as $element) {
* // doSomethingWith($element);
* }
* ```
*
* Example of iterating over each page of elements:
* ```
* $pagedListResponse = $client->getList(...);
* foreach ($pagedListResponse->iteratePages() as $page) {
* foreach ($page as $element) {
* // doSomethingWith($element);
* }
* }
* ```
*
* Example of accessing the current page, and manually iterating
* over pages:
* ```
* $pagedListResponse = $client->getList(...);
* $page = $pagedListResponse->getPage();
* // doSomethingWith($page);
* while ($page->hasNextPage()) {
* $page = $page->getNextPage();
* // doSomethingWith($page);
* }
* ```
*/
class PagedListResponse implements IteratorAggregate
{
private $firstPage;
/**
* PagedListResponse constructor.
*
* @param Page $firstPage A page containing response details.
*/
public function __construct(
Page $firstPage
) {
$this->firstPage = $firstPage;
}
/**
* Returns an iterator over the full list of elements. If the
* API response contains a (non-empty) next page token, then
* the PagedListResponse object will make calls to the underlying
* API to retrieve additional elements as required.
*
* NOTE: The result of this method is the same as getIterator().
* Prefer using getIterator(), or iterate directly on the
* PagedListResponse object.
*
* @return Generator
* @throws ValidationException
*/
public function iterateAllElements()
{
return $this->getIterator();
}
/**
* Returns an iterator over the full list of elements. If the
* API response contains a (non-empty) next page token, then
* the PagedListResponse object will make calls to the underlying
* API to retrieve additional elements as required.
*
* @return Generator
* @throws ValidationException
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
foreach ($this->iteratePages() as $page) {
foreach ($page as $key => $element) {
yield $key => $element;
}
}
}
/**
* Return the current page of results.
*
* @return Page
*/
public function getPage()
{
return $this->firstPage;
}
/**
* Returns an iterator over pages of results. The pages are
* retrieved lazily from the underlying API.
*
* @return Page[]
* @throws ValidationException
*/
public function iteratePages()
{
return $this->getPage()->iteratePages();
}
/**
* Returns a collection of elements with a fixed size set by
* the collectionSize parameter. The collection will only contain
* fewer than collectionSize elements if there are no more
* pages to be retrieved from the server.
*
* NOTE: it is an error to call this method if an optional parameter
* to set the page size is not supported or has not been set in the
* original API call. It is also an error if the collectionSize parameter
* is less than the page size that has been set.
*
* @param int $collectionSize
* @throws ValidationException if a FixedSizeCollection of the specified size cannot be constructed
* @return FixedSizeCollection
*/
public function expandToFixedSizeCollection(int $collectionSize)
{
return $this->getPage()->expandToFixedSizeCollection($collectionSize);
}
/**
* Returns an iterator over fixed size collections of results.
* The collections are retrieved lazily from the underlying API.
*
* Each collection will have collectionSize elements, with the
* exception of the final collection which may contain fewer
* elements.
*
* NOTE: it is an error to call this method if an optional parameter
* to set the page size is not supported or has not been set in the
* original API call. It is also an error if the collectionSize parameter
* is less than the page size that has been set.
*
* @param int $collectionSize
* @throws ValidationException if a FixedSizeCollection of the specified size cannot be constructed
* @return Generator|FixedSizeCollection[]
*/
public function iterateFixedSizeCollections(int $collectionSize)
{
return $this->expandToFixedSizeCollection($collectionSize)->iterateCollections();
}
}
src/PathTemplate.php 0000644 00000010073 15174273622 0010446 0 ustar 00 resourceTemplate = new AbsoluteResourceTemplate($path);
} else {
$this->resourceTemplate = new RelativeResourceTemplate($path);
}
}
/**
* @return string A string representation of the path template
*/
public function __toString()
{
return $this->resourceTemplate->__toString();
}
/**
* Renders a path template using the provided bindings.
*
* @param array $bindings An array matching var names to binding strings.
* @throws ValidationException if a key isn't provided or if a sub-template
* can't be parsed.
* @return string A rendered representation of this path template.
*/
public function render(array $bindings)
{
return $this->resourceTemplate->render($bindings);
}
/**
* Check if $path matches a resource string.
*
* @param string $path A resource string.
* @return bool
*/
public function matches(string $path)
{
return $this->resourceTemplate->matches($path);
}
/**
* Matches a fully qualified path template string.
*
* @param string $path A fully qualified path template string.
* @throws ValidationException if path can't be matched to the template.
* @return array Array matching var names to binding values.
*/
public function match(string $path)
{
return $this->resourceTemplate->match($path);
}
}
src/PollingTrait.php 0000644 00000007230 15174273622 0010467 0 ustar 00 0.0;
$endTime = $this->getCurrentTimeMillis() + $totalPollTimeoutMillis;
while (true) {
if ($hasTotalPollTimeout && $this->getCurrentTimeMillis() > $endTime) {
return false;
}
$this->sleepMillis($currentPollDelayMillis);
if ($pollCallable()) {
return true;
}
$currentPollDelayMillis = (int) min([
$currentPollDelayMillis * $pollDelayMultiplier,
$maxPollDelayMillis
]);
}
}
/**
* Protected to allow overriding for tests
*
* @return float Current time in milliseconds
*/
protected function getCurrentTimeMillis()
{
return microtime(true) * 1000.0;
}
/**
* Protected to allow overriding for tests
*
* @param int $millis
*/
protected function sleepMillis(int $millis)
{
usleep($millis * 1000);
}
}
src/RequestBuilder.php 0000644 00000023377 15174273622 0011030 0 ustar 00 baseUri = $baseUri;
$this->restConfig = require($restConfigPath);
}
/**
* @param string $path
* @return bool
*/
public function pathExists(string $path)
{
list($interface, $method) = explode('/', $path);
return isset($this->restConfig['interfaces'][$interface][$method]);
}
/**
* @param string $path
* @param Message $message
* @param array $headers
* @return RequestInterface
* @throws ValidationException
*/
public function build(string $path, Message $message, array $headers = [])
{
list($interface, $method) = explode('/', $path);
if (!isset($this->restConfig['interfaces'][$interface][$method])) {
throw new ValidationException(
"Failed to build request, as the provided path ($path) was not found in the configuration."
);
}
$numericEnums = isset($this->restConfig['numericEnums']) && $this->restConfig['numericEnums'];
$methodConfig = $this->restConfig['interfaces'][$interface][$method] + [
'placeholders' => [],
'body' => null,
'additionalBindings' => null,
];
$bindings = $this->buildBindings($methodConfig['placeholders'], $message);
$uriTemplateConfigs = $this->getConfigsForUriTemplates($methodConfig);
foreach ($uriTemplateConfigs as $config) {
$pathTemplate = $this->tryRenderPathTemplate($config['uriTemplate'], $bindings);
if ($pathTemplate) {
// We found a valid uriTemplate - now build and return the Request
list($body, $queryParams) = $this->constructBodyAndQueryParameters($message, $config);
// Request enum fields will be encoded as numbers rather than strings (in the response).
if ($numericEnums) {
$queryParams['$alt'] = "json;enum-encoding=int";
}
$uri = $this->buildUri($pathTemplate, $queryParams);
return new Request(
$config['method'],
$uri,
['Content-Type' => 'application/json'] + $headers,
$body
);
}
}
// No valid uriTemplate found - construct an exception
$uriTemplates = [];
foreach ($uriTemplateConfigs as $config) {
$uriTemplates[] = $config['uriTemplate'];
}
throw new ValidationException("Could not map bindings for $path to any Uri template.\n" .
"Bindings: " . print_r($bindings, true) .
"UriTemplates: " . print_r($uriTemplates, true));
}
/**
* Create a list of all possible configs using the additionalBindings
*
* @param array $config
* @return array[] An array of configs
*/
private function getConfigsForUriTemplates(array $config)
{
$configs = [$config];
if ($config['additionalBindings']) {
foreach ($config['additionalBindings'] as $additionalBinding) {
$configs[] = $additionalBinding + $config;
}
}
return $configs;
}
/**
* @param Message $message
* @param array $config
* @return array Tuple [$body, $queryParams]
*/
private function constructBodyAndQueryParameters(Message $message, array $config)
{
$messageDataJson = $message->serializeToJsonString();
if ($config['body'] === '*') {
return [$messageDataJson, []];
}
$body = null;
$queryParams = [];
$messageData = json_decode($messageDataJson, true);
foreach ($messageData as $name => $value) {
if (array_key_exists($name, $config['placeholders'])) {
continue;
}
if (Serializer::toSnakeCase($name) === $config['body']) {
if (($bodyMessage = $message->{"get$name"}()) instanceof Message) {
$body = $bodyMessage->serializeToJsonString();
} else {
$body = json_encode($value);
}
continue;
}
if (is_array($value) && $this->isAssoc($value)) {
foreach ($value as $key => $value2) {
$queryParams[$name . '.' . $key] = $value2;
}
} else {
$queryParams[$name] = $value;
}
}
// Ensures required query params with default values are always sent
// over the wire.
if (isset($config['queryParams'])) {
foreach ($config['queryParams'] as $requiredQueryParam) {
$requiredQueryParam = Serializer::toCamelCase($requiredQueryParam);
if (!array_key_exists($requiredQueryParam, $queryParams)) {
$getter = Serializer::getGetter($requiredQueryParam);
$queryParamValue = $message->$getter();
if ($queryParamValue instanceof Message) {
// Decode message for the query parameter.
$queryParamValue = json_decode($queryParamValue->serializeToJsonString(), true);
}
if (is_array($queryParamValue)) {
// If the message has properties, add them as nested querystring values.
// NOTE: This only supports nesting at one level of depth.
foreach ($queryParamValue as $key => $value) {
$queryParams[$requiredQueryParam . '.' . $key] = $value;
}
} else {
$queryParams[$requiredQueryParam] = $queryParamValue;
}
}
}
}
return [$body, $queryParams];
}
/**
* @param array $placeholders
* @param Message $message
* @return array Bindings from path template fields to values from message
*/
private function buildBindings(array $placeholders, Message $message)
{
$bindings = [];
foreach ($placeholders as $placeholder => $metadata) {
$value = array_reduce(
$metadata['getters'],
function (Message $result = null, $getter) {
if ($result) {
return $result->$getter();
}
},
$message
);
$bindings[$placeholder] = $value;
}
return $bindings;
}
/**
* Try to render the resource name. The rendered resource name will always contain a leading '/'
*
* @param string $uriTemplate
* @param array $bindings
* @return null|string
* @throws ValidationException
*/
private function tryRenderPathTemplate(string $uriTemplate, array $bindings)
{
$template = new AbsoluteResourceTemplate($uriTemplate);
try {
return $template->render($bindings);
} catch (ValidationException $e) {
return null;
}
}
/**
* @param string $path
* @param array $queryParams
* @return UriInterface
*/
private function buildUri(string $path, array $queryParams)
{
$uri = Utils::uriFor(
sprintf(
'https://%s%s',
$this->baseUri,
$path
)
);
if ($queryParams) {
$uri = $this->buildUriWithQuery(
$uri,
$queryParams
);
}
return $uri;
}
}
src/RequestParamsHeaderDescriptor.php 0000644 00000005114 15174273622 0014022 0 ustar 00 value].
*/
public function __construct(array $requestParams)
{
$headerKey = self::HEADER_KEY;
$headerValue = '';
foreach ($requestParams as $key => $value) {
if ('' !== $headerValue) {
$headerValue .= '&';
}
$headerValue .= $key . '=' . urlencode(strval($value));
}
$this->header = [$headerKey => [$headerValue]];
}
/**
* Returns an associative array that contains request params header metadata.
*
* @return array
*/
public function getHeader()
{
return $this->header;
}
}
src/ResourceHelperTrait.php 0000644 00000007735 15174273622 0012024 0 ustar 00 $template) {
self::$templateMap[$name] = new PathTemplate($template);
}
}
private static function getPathTemplate(string $key)
{
// TODO: Add nullable return type reference once PHP 7.1 is minimum.
if (is_null(self::$templateMap)) {
self::registerPathTemplates();
}
return self::$templateMap[$key] ?? null;
}
private static function parseFormattedName(string $formattedName, string $template = null): array
{
if (is_null(self::$templateMap)) {
self::registerPathTemplates();
}
if ($template) {
if (!isset(self::$templateMap[$template])) {
throw new ValidationException("Template name $template does not exist");
}
return self::$templateMap[$template]->match($formattedName);
}
foreach (self::$templateMap as $templateName => $pathTemplate) {
try {
return $pathTemplate->match($formattedName);
} catch (ValidationException $ex) {
// Swallow the exception to continue trying other path templates
}
}
throw new ValidationException("Input did not match any known format. Input: $formattedName");
}
}
src/ResourceTemplate/AbsoluteResourceTemplate.php 0000644 00000011737 15174273622 0016333 0 ustar 00 ").
*
* Examples:
* /projects
* /projects/{project}
* /foo/{bar=**}/fizz/*:action
*
* Templates use the syntax of the API platform; see
* https://github.com/googleapis/api-common-protos/blob/master/google/api/http.proto
* for details. A template consists of a sequence of literals, wildcards, and variable bindings,
* where each binding can have a sub-path. A string representation can be parsed into an
* instance of AbsoluteResourceTemplate, which can then be used to perform matching and instantiation.
*
* @internal
*/
class AbsoluteResourceTemplate implements ResourceTemplateInterface
{
private RelativeResourceTemplate $resourceTemplate;
/** @var string|bool */
private $verb;
/**
* AbsoluteResourceTemplate constructor.
* @param string $path
* @throws ValidationException
*/
public function __construct(string $path)
{
if (empty($path)) {
throw new ValidationException('Cannot construct AbsoluteResourceTemplate from empty string');
}
if ($path[0] !== '/') {
throw new ValidationException(
"Could not construct AbsoluteResourceTemplate from '$path': must begin with '/'"
);
}
$verbSeparatorPos = $this->verbSeparatorPos($path);
$this->resourceTemplate = new RelativeResourceTemplate(substr($path, 1, $verbSeparatorPos - 1));
$this->verb = substr($path, $verbSeparatorPos + 1);
}
/**
* @inheritdoc
*/
public function __toString()
{
return sprintf("/%s%s", $this->resourceTemplate, $this->renderVerb());
}
/**
* @inheritdoc
*/
public function render(array $bindings)
{
return sprintf("/%s%s", $this->resourceTemplate->render($bindings), $this->renderVerb());
}
/**
* @inheritdoc
*/
public function matches(string $path)
{
try {
$this->match($path);
return true;
} catch (ValidationException $ex) {
return false;
}
}
/**
* @inheritdoc
*/
public function match(string $path)
{
if (empty($path)) {
throw $this->matchException($path, "path cannot be empty");
}
if ($path[0] !== '/') {
throw $this->matchException($path, "missing leading '/'");
}
$verbSeparatorPos = $this->verbSeparatorPos($path);
if (substr($path, $verbSeparatorPos + 1) !== $this->verb) {
throw $this->matchException($path, "trailing verb did not match '{$this->verb}'");
}
return $this->resourceTemplate->match(substr($path, 1, $verbSeparatorPos - 1));
}
private function matchException(string $path, string $reason)
{
return new ValidationException("Could not match path '$path' to template '$this': $reason");
}
private function renderVerb()
{
return $this->verb ? ':' . $this->verb : '';
}
private function verbSeparatorPos(string $path)
{
$finalSeparatorPos = strrpos($path, '/');
$verbSeparatorPos = strrpos($path, ':', $finalSeparatorPos);
if ($verbSeparatorPos === false) {
$verbSeparatorPos = strlen($path);
}
return $verbSeparatorPos;
}
}
src/ResourceTemplate/Parser.php 0000644 00000021331 15174273622 0012574 0 ustar 00 = strlen($path)) {
// A trailing '/' has caused the index to exceed the bounds
// of the string - provide a helpful error message.
throw self::parseError($path, strlen($path) - 1, "invalid trailing '/'");
}
if ($path[$index] === '{') {
// Validate that the { has a matching }
$closingBraceIndex = strpos($path, '}', $index);
if ($closingBraceIndex === false) {
throw self::parseError(
$path,
strlen($path),
"Expected '}' to match '{' at index $index, got end of string"
);
}
$segmentStringLengthWithoutBraces = $closingBraceIndex - $index - 1;
$segmentStringWithoutBraces = substr($path, $index + 1, $segmentStringLengthWithoutBraces);
$index = $closingBraceIndex + 1;
$nextLiteral = '/';
$remainingPath = substr($path, $index);
if (!empty($remainingPath)) {
// Find the firstnon-slash separator seen, if any.
$nextSlashIndex = strpos($remainingPath, '/', 0);
$nonSlashSeparators = ['-', '_', '~', '.'];
foreach ($nonSlashSeparators as $nonSlashSeparator) {
$nonSlashSeparatorIndex = strpos($remainingPath, $nonSlashSeparator, 0);
$nextOpenBraceIndex = strpos($remainingPath, '{', 0);
if ($nonSlashSeparatorIndex !== false && $nonSlashSeparatorIndex === $nextOpenBraceIndex - 1) {
$index += $nonSlashSeparatorIndex;
$nextLiteral = $nonSlashSeparator;
break;
}
}
}
return self::parseVariableSegment($segmentStringWithoutBraces, $nextLiteral);
} else {
$nextSlash = strpos($path, '/', $index);
if ($nextSlash === false) {
$nextSlash = strlen($path);
}
$segmentString = substr($path, $index, $nextSlash - $index);
$nextLiteral = '/';
$index = $nextSlash;
return self::parse($segmentString, $path, $index);
}
}
/**
* @param string $segmentString
* @param string $path
* @param int $index
* @return Segment
* @throws ValidationException
*/
private static function parse(string $segmentString, string $path, int $index)
{
if ($segmentString === '*') {
return new Segment(Segment::WILDCARD_SEGMENT);
} elseif ($segmentString === '**') {
return new Segment(Segment::DOUBLE_WILDCARD_SEGMENT);
} else {
if (!self::isValidLiteral($segmentString)) {
if (empty($segmentString)) {
// Create user friendly message in case of empty segment
throw self::parseError($path, $index, "Unexpected empty segment (consecutive '/'s are invalid)");
} else {
throw self::parseError($path, $index, "Unexpected characters in literal segment $segmentString");
}
}
return new Segment(Segment::LITERAL_SEGMENT, $segmentString);
}
}
/**
* @param string $segmentStringWithoutBraces
* @param string $separatorLiteral
* @return Segment
* @throws ValidationException
*/
private static function parseVariableSegment(string $segmentStringWithoutBraces, string $separatorLiteral)
{
// Validate there are no nested braces
$nestedOpenBracket = strpos($segmentStringWithoutBraces, '{');
if ($nestedOpenBracket !== false) {
throw new ValidationException(
"Unexpected '{' parsing segment $segmentStringWithoutBraces at index $nestedOpenBracket"
);
}
$equalsIndex = strpos($segmentStringWithoutBraces, '=');
if ($equalsIndex === false) {
// If the variable does not contain '=', we assume the pattern is '*' as per google.rpc.Http
$variableKey = $segmentStringWithoutBraces;
$nestedResource = new RelativeResourceTemplate("*");
} else {
$variableKey = substr($segmentStringWithoutBraces, 0, $equalsIndex);
$nestedResourceString = substr($segmentStringWithoutBraces, $equalsIndex + 1);
$nestedResource = new RelativeResourceTemplate($nestedResourceString);
}
if (!self::isValidLiteral($variableKey)) {
throw new ValidationException(
"Unexpected characters in variable name $variableKey"
);
}
return new Segment(Segment::VARIABLE_SEGMENT, null, $variableKey, $nestedResource, $separatorLiteral);
}
/**
* @param string $literal
* @param string $path
* @param int $index
* @return string
* @throws ValidationException
*/
private static function parseLiteralFromPath(string $literal, string $path, int &$index)
{
$literalLength = strlen($literal);
if (strlen($path) < ($index + $literalLength)) {
throw self::parseError($path, $index, "expected '$literal'");
}
$consumedLiteral = substr($path, $index, $literalLength);
if ($consumedLiteral !== $literal) {
throw self::parseError($path, $index, "expected '$literal'");
}
$index += $literalLength;
return $consumedLiteral;
}
private static function parseError(string $path, int $index, string $reason)
{
return new ValidationException("Error parsing '$path' at index $index: $reason");
}
/**
* Check if $literal is a valid segment literal. Segment literals may only contain numbers,
* letters, and any of the following: .-~_
*
* @param string $literal
* @return bool
*/
private static function isValidLiteral(string $literal)
{
return preg_match("/^[0-9a-zA-Z\\.\\-~_]+$/", $literal) === 1;
}
}
src/ResourceTemplate/RelativeResourceTemplate.php 0000644 00000037272 15174273622 0016332 0 ustar 00 ").
*
* Examples:
* projects
* projects/{project}
* foo/{bar=**}/fizz/*
*
* Templates use the syntax of the API platform; see
* https://github.com/googleapis/api-common-protos/blob/master/google/api/http.proto
* for details. A template consists of a sequence of literals, wildcards, and variable bindings,
* where each binding can have a sub-path. A string representation can be parsed into an
* instance of AbsoluteResourceTemplate, which can then be used to perform matching and instantiation.
*
* @internal
*/
class RelativeResourceTemplate implements ResourceTemplateInterface
{
/** @var Segment[] */
private array $segments;
/**
* RelativeResourceTemplate constructor.
*
* @param string $path
* @throws ValidationException
*/
public function __construct(string $path)
{
if (empty($path)) {
throw new ValidationException('Cannot construct RelativeResourceTemplate from empty string');
}
$this->segments = Parser::parseSegments($path);
$doubleWildcardCount = self::countDoubleWildcards($this->segments);
if ($doubleWildcardCount > 1) {
throw new ValidationException(
"Cannot parse '$path': cannot contain more than one path wildcard"
);
}
// Check for duplicate keys
$keys = [];
foreach ($this->segments as $segment) {
if ($segment->getSegmentType() === Segment::VARIABLE_SEGMENT) {
if (isset($keys[$segment->getKey()])) {
throw new ValidationException(
"Duplicate key '{$segment->getKey()}' in path $path"
);
}
$keys[$segment->getKey()] = true;
}
}
}
/**
* @inheritdoc
*/
public function __toString()
{
return self::renderSegments($this->segments);
}
/**
* @inheritdoc
*/
public function render(array $bindings)
{
$literalSegments = [];
$keySegmentTuples = self::buildKeySegmentTuples($this->segments);
foreach ($keySegmentTuples as list($key, $segment)) {
/** @var Segment $segment */
if ($segment->getSegmentType() === Segment::LITERAL_SEGMENT) {
$literalSegments[] = $segment;
continue;
}
if (!array_key_exists($key, $bindings)) {
throw $this->renderingException($bindings, "missing required binding '$key' for segment '$segment'");
}
$value = $bindings[$key];
if (!is_null($value) && $segment->matches($value)) {
$literalSegments[] = new Segment(
Segment::LITERAL_SEGMENT,
$value,
$segment->getValue(),
$segment->getTemplate(),
$segment->getSeparator()
);
} else {
$valueString = is_null($value) ? "null" : "'$value'";
throw $this->renderingException(
$bindings,
"expected binding '$key' to match segment '$segment', instead got $valueString"
);
}
}
return self::renderSegments($literalSegments);
}
/**
* @inheritdoc
*/
public function matches(string $path)
{
try {
$this->match($path);
return true;
} catch (ValidationException $ex) {
return false;
}
}
/**
* @inheritdoc
*/
public function match(string $path)
{
// High level strategy for matching:
// - Build a list of Segments from our template, where any variable segments are
// flattened into a single, non-nested list
// - Break $path into pieces based on '/'.
// - Use the segments to further subdivide the pieces using any applicable non-slash separators.
// - Match pieces of the path with Segments in the flattened list
// In order to build correct bindings after we match the $path against our template, we
// need to (a) calculate the correct positional keys for our wildcards, and (b) maintain
// information about the variable identifier of any flattened segments. To do this, we
// build a list of [string, Segment] tuples, where the string component is the appropriate
// key.
$keySegmentTuples = self::buildKeySegmentTuples($this->segments);
$flattenedKeySegmentTuples = self::flattenKeySegmentTuples($keySegmentTuples);
$flattenedKeySegmentTuplesCount = count($flattenedKeySegmentTuples);
assert($flattenedKeySegmentTuplesCount > 0);
$slashPathPieces = explode('/', $path);
$pathPieces = [];
$pathPiecesIndex = 0;
$startIndex = 0;
$slashPathPiecesCount = count($slashPathPieces);
$doubleWildcardPieceCount = $slashPathPiecesCount - $flattenedKeySegmentTuplesCount + 1;
for ($i = 0; $i < count($flattenedKeySegmentTuples); $i++) {
$segmentKey = $flattenedKeySegmentTuples[$i][0];
$segment = $flattenedKeySegmentTuples[$i][1];
// In our flattened list of segments, we should never encounter a variable segment
assert($segment->getSegmentType() !== Segment::VARIABLE_SEGMENT);
if ($segment->getSegmentType() == Segment::DOUBLE_WILDCARD_SEGMENT) {
$pathPiecesForSegment = array_slice($slashPathPieces, $pathPiecesIndex, $doubleWildcardPieceCount);
$pathPiece = implode('/', $pathPiecesForSegment);
$pathPiecesIndex += $doubleWildcardPieceCount;
$pathPieces[] = $pathPiece;
continue;
}
if ($segment->getSegmentType() == Segment::WILDCARD_SEGMENT) {
if ($pathPiecesIndex >= $slashPathPiecesCount) {
break;
}
}
if ($segment->getSeparator() === '/') {
if ($pathPiecesIndex >= $slashPathPiecesCount) {
throw $this->matchException($path, "segment and path length mismatch");
}
$pathPiece = substr($slashPathPieces[$pathPiecesIndex++], $startIndex);
$startIndex = 0;
} else {
$rawPiece = substr($slashPathPieces[$pathPiecesIndex], $startIndex);
$pathPieceLength = strpos($rawPiece, $segment->getSeparator());
$pathPiece = substr($rawPiece, 0, $pathPieceLength);
$startIndex += $pathPieceLength + 1;
}
$pathPieces[] = $pathPiece;
}
if ($flattenedKeySegmentTuples[$i - 1][1]->getSegmentType() !== Segment::DOUBLE_WILDCARD_SEGMENT) {
// Process any remaining pieces. The binding logic will throw exceptions for any invalid paths.
for (; $pathPiecesIndex < count($slashPathPieces); $pathPiecesIndex++) {
$pathPieces[] = $slashPathPieces[$pathPiecesIndex];
}
}
$pathPiecesCount = count($pathPieces);
// We would like to match pieces of our path 1:1 with the segments of our template. However,
// this is confounded by the presence of double wildcards ('**') in the template, which can
// match multiple segments in the path.
// Because there can only be one '**' present, we can determine how many segments it must
// match by examining the difference in count between the template segments and the
// path pieces.
if ($pathPiecesCount < $flattenedKeySegmentTuplesCount) {
// Each segment in $flattenedKeyedSegments must consume at least one
// segment in $pathSegments, so matching must fail.
throw $this->matchException($path, "path does not contain enough segments to be matched");
}
$doubleWildcardPieceCount = $pathPiecesCount - $flattenedKeySegmentTuplesCount + 1;
$bindings = [];
$pathPiecesIndex = 0;
/** @var Segment $segment */
foreach ($flattenedKeySegmentTuples as list($segmentKey, $segment)) {
$pathPiece = $pathPieces[$pathPiecesIndex++];
if (!$segment->matches($pathPiece)) {
throw $this->matchException($path, "expected path element matching '$segment', got '$pathPiece'");
}
// If we have a valid key, add our $pathPiece to the $bindings array. Note that there
// may be multiple copies of the same $segmentKey. This is because a flattened variable
// segment can match multiple pieces from the path. We can add these to an array and
// collapse them all once the bindings are complete.
if (isset($segmentKey)) {
$bindings += [$segmentKey => []];
$bindings[$segmentKey][] = $pathPiece;
}
}
// It is possible that we have left over path pieces, which can occur if our template does
// not have a double wildcard. In that case, the match should fail.
if ($pathPiecesIndex !== $pathPiecesCount) {
throw $this->matchException($path, "expected end of path, got '$pathPieces[$pathPiecesIndex]'");
}
// Collapse the bindings from lists into strings
$collapsedBindings = [];
foreach ($bindings as $key => $boundPieces) {
$collapsedBindings[$key] = implode('/', $boundPieces);
}
return $collapsedBindings;
}
private function matchException(string $path, string $reason)
{
return new ValidationException("Could not match path '$path' to template '$this': $reason");
}
private function renderingException(array $bindings, string $reason)
{
$bindingsString = print_r($bindings, true);
return new ValidationException(
"Error rendering '$this': $reason\n" .
"Provided bindings: $bindingsString"
);
}
/**
* @param Segment[] $segments
* @param string|null $separator An optional string separator
* @return array[] A list of [string, Segment] tuples
*/
private static function buildKeySegmentTuples(array $segments, string $separator = null)
{
$keySegmentTuples = [];
$positionalArgumentCounter = 0;
foreach ($segments as $segment) {
switch ($segment->getSegmentType()) {
case Segment::WILDCARD_SEGMENT:
case Segment::DOUBLE_WILDCARD_SEGMENT:
$positionalKey = "\$$positionalArgumentCounter";
$positionalArgumentCounter++;
$newSegment = $segment;
if ($separator !== null) {
$newSegment = new Segment(
$segment->getSegmentType(),
$segment->getValue(),
$segment->getKey(),
$segment->getTemplate(),
$separator
);
}
$keySegmentTuples[] = [$positionalKey, $newSegment];
break;
default:
$keySegmentTuples[] = [$segment->getKey(), $segment];
}
}
return $keySegmentTuples;
}
/**
* @param array[] $keySegmentTuples A list of [string, Segment] tuples
* @return array[] A list of [string, Segment] tuples
*/
private static function flattenKeySegmentTuples(array $keySegmentTuples)
{
$flattenedKeySegmentTuples = [];
foreach ($keySegmentTuples as list($key, $segment)) {
/** @var Segment $segment */
switch ($segment->getSegmentType()) {
case Segment::VARIABLE_SEGMENT:
// For segment variables, replace the segment with the segments of its children
$template = $segment->getTemplate();
$nestedKeySegmentTuples = self::buildKeySegmentTuples(
$template->segments,
$segment->getSeparator()
);
foreach ($nestedKeySegmentTuples as list($nestedKey, $nestedSegment)) {
/** @var Segment $nestedSegment */
// Nested variables are not allowed
assert($nestedSegment->getSegmentType() !== Segment::VARIABLE_SEGMENT);
// Insert the nested segment with key set to the outer key of the
// parent variable segment
$flattenedKeySegmentTuples[] = [$key, $nestedSegment];
}
break;
default:
// For all other segments, don't change the key or segment
$flattenedKeySegmentTuples[] = [$key, $segment];
}
}
return $flattenedKeySegmentTuples;
}
/**
* @param Segment[] $segments
* @return int
*/
private static function countDoubleWildcards(array $segments)
{
$doubleWildcardCount = 0;
foreach ($segments as $segment) {
switch ($segment->getSegmentType()) {
case Segment::DOUBLE_WILDCARD_SEGMENT:
$doubleWildcardCount++;
break;
case Segment::VARIABLE_SEGMENT:
$doubleWildcardCount += self::countDoubleWildcards($segment->getTemplate()->segments);
break;
}
}
return $doubleWildcardCount;
}
/**
* Joins segments using their separators.
* @param array $segmentsToRender
* @return string
*/
private static function renderSegments(array $segmentsToRender)
{
$renderResult = "";
for ($i = 0; $i < count($segmentsToRender); $i++) {
$segment = $segmentsToRender[$i];
$renderResult .= $segment;
if ($i < count($segmentsToRender) - 1) {
$renderResult .= $segment->getSeparator();
}
}
return $renderResult;
}
}
src/ResourceTemplate/ResourceTemplateInterface.php 0000644 00000007170 15174273622 0016451 0 ustar 00 "). (Note that a trailing verb without a
* leading slash is not permitted).
*
* Examples:
* projects
* /projects
* foo/{bar=**}/fizz/*
* /foo/{bar=**}/fizz/*:action
*
* Templates use the syntax of the API platform; see
* https://github.com/googleapis/api-common-protos/blob/master/google/api/http.proto
* for details. A template consists of a sequence of literals, wildcards, and variable bindings,
* where each binding can have a sub-path. A string representation can be parsed into an
* instance of AbsoluteResourceTemplate, which can then be used to perform matching and instantiation.
*
* @internal
*/
interface ResourceTemplateInterface
{
/**
* @return string A string representation of the resource template
*/
public function __toString();
/**
* Renders a resource template using the provided bindings.
*
* @param array $bindings An array matching var names to binding strings.
* @return string A rendered representation of this resource template.
* @throws ValidationException If $bindings does not contain all required keys
* or if a sub-template can't be parsed.
*/
public function render(array $bindings);
/**
* Check if $path matches a resource string.
*
* @param string $path A resource string.
* @return bool
*/
public function matches(string $path);
/**
* Matches a given $path to a resource template, and returns an array of bindings between
* wildcards / variables in the template and values in the path. If $path does not match the
* template, then a ValidationException is thrown.
*
* @param string $path A resource string.
* @throws ValidationException if path can't be matched to the template.
* @return array Array matching var names to binding values.
*/
public function match(string $path);
}
src/ResourceTemplate/Segment.php 0000644 00000013426 15174273622 0012750 0 ustar 00 segmentType = $segmentType;
$this->value = $value;
$this->key = $key;
$this->template = $template;
$this->separator = $separator;
switch ($this->segmentType) {
case Segment::LITERAL_SEGMENT:
$this->stringRepr = "{$this->value}";
break;
case Segment::WILDCARD_SEGMENT:
$this->stringRepr = "*";
break;
case Segment::DOUBLE_WILDCARD_SEGMENT:
$this->stringRepr = "**";
break;
case Segment::VARIABLE_SEGMENT:
$this->stringRepr = "{{$this->key}={$this->template}}";
break;
default:
throw new ValidationException(
"Unexpected Segment type: {$this->segmentType}"
);
}
}
/**
* @return string A string representation of the segment.
*/
public function __toString()
{
return $this->stringRepr;
}
/**
* Checks if $value matches this Segment.
*
* @param string $value
* @return bool
* @throws ValidationException
*/
public function matches(string $value)
{
switch ($this->segmentType) {
case Segment::LITERAL_SEGMENT:
return $this->value === $value;
case Segment::WILDCARD_SEGMENT:
return self::isValidBinding($value);
case Segment::DOUBLE_WILDCARD_SEGMENT:
return self::isValidDoubleWildcardBinding($value);
case Segment::VARIABLE_SEGMENT:
return $this->template->matches($value);
default:
throw new ValidationException(
"Unexpected Segment type: {$this->segmentType}"
);
}
}
/**
* @return int
*/
public function getSegmentType()
{
return $this->segmentType;
}
/**
* @return string|null
*/
public function getKey()
{
return $this->key;
}
/**
* @return string|null
*/
public function getValue()
{
return $this->value;
}
/**
* @return RelativeResourceTemplate|null
*/
public function getTemplate()
{
return $this->template;
}
/**
* @return string
*/
public function getSeparator()
{
return $this->separator;
}
/**
* Check if $binding is a valid segment binding. Segment bindings may contain any characters
* except a forward slash ('/'), and may not be empty.
*
* @param string $binding
* @return bool
*/
private static function isValidBinding(string $binding)
{
return preg_match("-^[^/]+$-", $binding) === 1;
}
/**
* Check if $binding is a valid double wildcard binding. Segment bindings may contain any
* characters, but may not be empty.
*
* @param string $binding
* @return bool
*/
private static function isValidDoubleWildcardBinding(string $binding)
{
return preg_match("-^.+$-", $binding) === 1;
}
}
src/RetrySettings.php 0000644 00000053067 15174273622 0010716 0 ustar 00 100,
* 'retryDelayMultiplier' => 1.3,
* 'maxRetryDelayMillis' => 60000,
* 'initialRpcTimeoutMillis' => 20000,
* 'rpcTimeoutMultiplier' => 1.0,
* 'maxRpcTimeoutMillis' => 20000,
* 'totalTimeoutMillis' => 600000,
* 'retryableCodes' => [ApiStatus::DEADLINE_EXCEEDED, ApiStatus::UNAVAILABLE],
* ]);
* ```
*
* It is also possible to create a new RetrySettings object from an existing
* object using the {@see \Google\ApiCore\RetrySettings::with()} method.
*
* Example modifying an existing RetrySettings object using `with()`:
* ```
* $newRetrySettings = $retrySettings->with([
* 'totalTimeoutMillis' => 700000,
* ]);
* ```
*
* Modifying the retry behavior of an RPC method
* ---------------------------------------------
*
* RetrySettings objects can be used to control retries for many RPC methods in
* [google-cloud-php](https://github.com/googleapis/google-cloud-php).
* The examples below make use of the
* [GroupServiceClient](https://googleapis.github.io/google-cloud-php/#/docs/google-cloud/monitoring/v3/groupserviceclient)
* from the [Monitoring V3 API](https://github.com/googleapis/google-cloud-php/tree/master/src/Monitoring/V3),
* but they can be applied to other APIs in the
* [google-cloud-php](https://github.com/googleapis/google-cloud-php) repository.
*
* It is possible to specify the retry behavior to be used by an RPC via the
* `retrySettings` field in the `optionalArgs` parameter. The `retrySettings`
* field can contain either a RetrySettings object, or a PHP array containing
* the particular retry parameters to be updated.
*
* Example of disabling retries for a single call to the
* [listGroups](https://googleapis.github.io/google-cloud-php/#/docs/google-cloud/monitoring/v3/groupserviceclient?method=listGroups)
* method, and setting a custom timeout:
* ```
* $result = $client->listGroups($name, [
* 'retrySettings' => [
* 'retriesEnabled' => false,
* 'noRetriesRpcTimeoutMillis' => 5000,
* ]
* ]);
* ```
*
* Example of creating a new RetrySettings object and using it to override
* the retry settings for a call to the
* [listGroups](https://googleapis.github.io/google-cloud-php/#/docs/google-cloud/monitoring/v3/groupserviceclient?method=listGroups)
* method:
* ```
* $customRetrySettings = new RetrySettings([
* 'initialRetryDelayMillis' => 100,
* 'retryDelayMultiplier' => 1.3,
* 'maxRetryDelayMillis' => 60000,
* 'initialRpcTimeoutMillis' => 20000,
* 'rpcTimeoutMultiplier' => 1.0,
* 'maxRpcTimeoutMillis' => 20000,
* 'totalTimeoutMillis' => 600000,
* 'retryableCodes' => [ApiStatus::DEADLINE_EXCEEDED, ApiStatus::UNAVAILABLE],
* ]);
*
* $result = $client->listGroups($name, [
* 'retrySettings' => $customRetrySettings
* ]);
* ```
*
* Modifying the default retry behavior for RPC methods on a Client object
* -----------------------------------------------------------------------
*
* It is also possible to specify the retry behavior for RPC methods when
* constructing a client object using the 'retrySettingsArray'. The examples
* below again make use of the
* [GroupServiceClient](https://googleapis.github.io/google-cloud-php/#/docs/google-cloud/monitoring/v3/groupserviceclient)
* from the [Monitoring V3 API](https://github.com/googleapis/google-cloud-php/tree/master/src/Monitoring/V3),
* but they can be applied to other APIs in the
* [google-cloud-php](https://github.com/googleapis/google-cloud-php) repository.
*
* The GroupServiceClient object accepts an optional `retrySettingsArray`
* parameter, which can be used to specify retry behavior for RPC methods
* on the client. The `retrySettingsArray` accepts a PHP array in which keys
* are the names of RPC methods on the client, and values are either a
* RetrySettings object or a PHP array containing the particular retry
* parameters to be updated.
*
* Example updating the retry settings for four methods of GroupServiceClient:
* ```
* use Google\Cloud\Monitoring\V3\GroupServiceClient;
*
* $customRetrySettings = new RetrySettings([
* 'initialRetryDelayMillis' => 100,
* 'retryDelayMultiplier' => 1.3,
* 'maxRetryDelayMillis' => 60000,
* 'initialRpcTimeoutMillis' => 20000,
* 'rpcTimeoutMultiplier' => 1.0,
* 'maxRpcTimeoutMillis' => 20000,
* 'totalTimeoutMillis' => 600000,
* 'retryableCodes' => [ApiStatus::DEADLINE_EXCEEDED, ApiStatus::UNAVAILABLE],
* ]);
*
* $updatedCustomRetrySettings = $customRetrySettings->with([
* 'totalTimeoutMillis' => 700000
* ]);
*
* $client = new GroupServiceClient([
* 'retrySettingsArray' => [
* 'listGroups' => ['retriesEnabled' => false],
* 'getGroup' => [
* 'initialRpcTimeoutMillis' => 10000,
* 'maxRpcTimeoutMillis' => 30000,
* 'totalTimeoutMillis' => 60000,
* ],
* 'deleteGroup' => $customRetrySettings,
* 'updateGroup' => $updatedCustomRetrySettings
* ],
* ]);
* ```
*
* Configure the use of logical timeout
* ------------------------------------
*
* To configure the use of a logical timeout, where a logical timeout is the
* duration a method is given to complete one or more RPC attempts, with each
* attempt using only the time remaining in the logical timeout, use
* {@see \Google\ApiCore\RetrySettings::logicalTimeout()} combined with
* {@see \Google\ApiCore\RetrySettings::with()}.
*
* ```
* $timeoutSettings = RetrySettings::logicalTimeout(30000);
*
* $customRetrySettings = $customRetrySettings->with($timeoutSettings);
*
* $result = $client->listGroups($name, [
* 'retrySettings' => $customRetrySettings
* ]);
* ```
*
* {@see \Google\ApiCore\RetrySettings::logicalTimeout()} can also be used on a
* method call independent of a RetrySettings instance.
*
* ```
* $timeoutSettings = RetrySettings::logicalTimeout(30000);
*
* $result = $client->listGroups($name, [
* 'retrySettings' => $timeoutSettings
* ]);
* ```
*/
class RetrySettings
{
use ValidationTrait;
const DEFAULT_MAX_RETRIES = 0;
private $retriesEnabled;
private $retryableCodes;
private $initialRetryDelayMillis;
private $retryDelayMultiplier;
private $maxRetryDelayMillis;
private $initialRpcTimeoutMillis;
private $rpcTimeoutMultiplier;
private $maxRpcTimeoutMillis;
private $totalTimeoutMillis;
private $noRetriesRpcTimeoutMillis;
/**
* The number of maximum retries an operation can do.
* This doesn't include the original API call.
* Setting this to 0 means no limit.
*/
private int $maxRetries;
/**
* When set, this function will be used to evaluate if the retry should
* take place or not. The callable will have the following signature:
* function (Exception $e, array $options): bool
*/
private ?Closure $retryFunction;
/**
* Constructs an instance.
*
* @param array $settings {
* Required. Settings for configuring the retry behavior. All parameters are required except
* $retriesEnabled and $noRetriesRpcTimeoutMillis, which are optional and have defaults
* determined based on the other settings provided.
*
* @type bool $retriesEnabled Optional. Enables retries. If not specified, the value is
* determined using the $retryableCodes setting. If $retryableCodes is empty,
* then $retriesEnabled is set to false; otherwise, it is set to true.
* @type int $noRetriesRpcTimeoutMillis Optional. The timeout of the rpc call to be used
* if $retriesEnabled is false, in milliseconds. It not specified, the value
* of $initialRpcTimeoutMillis is used.
* @type array $retryableCodes The Status codes that are retryable. Each status should be
* either one of the string constants defined on {@see \Google\ApiCore\ApiStatus}
* or an integer constant defined on {@see \Google\Rpc\Code}.
* @type int $initialRetryDelayMillis The initial delay of retry in milliseconds.
* @type int $retryDelayMultiplier The exponential multiplier of retry delay.
* @type int $maxRetryDelayMillis The max delay of retry in milliseconds.
* @type int $initialRpcTimeoutMillis The initial timeout of rpc call in milliseconds.
* @type int $rpcTimeoutMultiplier The exponential multiplier of rpc timeout.
* @type int $maxRpcTimeoutMillis The max timeout of rpc call in milliseconds.
* @type int $totalTimeoutMillis The max accumulative timeout in total.
* @type int $maxRetries The max retries allowed for an operation.
* Defaults to the value of the DEFAULT_MAX_RETRIES constant.
* This option is experimental.
* @type callable $retryFunction This function will be used to decide if we should retry or not.
* Callable signature: `function (Exception $e, array $options): bool`
* This option is experimental.
* }
*/
public function __construct(array $settings)
{
$this->validateNotNull($settings, [
'initialRetryDelayMillis',
'retryDelayMultiplier',
'maxRetryDelayMillis',
'initialRpcTimeoutMillis',
'rpcTimeoutMultiplier',
'maxRpcTimeoutMillis',
'totalTimeoutMillis',
'retryableCodes'
]);
$this->initialRetryDelayMillis = $settings['initialRetryDelayMillis'];
$this->retryDelayMultiplier = $settings['retryDelayMultiplier'];
$this->maxRetryDelayMillis = $settings['maxRetryDelayMillis'];
$this->initialRpcTimeoutMillis = $settings['initialRpcTimeoutMillis'];
$this->rpcTimeoutMultiplier = $settings['rpcTimeoutMultiplier'];
$this->maxRpcTimeoutMillis = $settings['maxRpcTimeoutMillis'];
$this->totalTimeoutMillis = $settings['totalTimeoutMillis'];
$this->retryableCodes = $settings['retryableCodes'];
$this->retriesEnabled = array_key_exists('retriesEnabled', $settings)
? $settings['retriesEnabled']
: (count($this->retryableCodes) > 0);
$this->noRetriesRpcTimeoutMillis = array_key_exists('noRetriesRpcTimeoutMillis', $settings)
? $settings['noRetriesRpcTimeoutMillis']
: $this->initialRpcTimeoutMillis;
$this->maxRetries = $settings['maxRetries'] ?? self::DEFAULT_MAX_RETRIES;
$this->retryFunction = $settings['retryFunction'] ?? null;
}
/**
* Constructs an array mapping method names to CallSettings.
*
* @param string $serviceName
* The fully-qualified name of this service, used as a key into
* the client config file.
* @param array $clientConfig
* An array parsed from the standard API client config file.
* @param bool $disableRetries
* Disable retries in all loaded RetrySettings objects. Defaults to false.
* @throws ValidationException
* @return RetrySettings[] $retrySettings
*/
public static function load(
string $serviceName,
array $clientConfig,
bool $disableRetries = false
) {
$serviceRetrySettings = [];
$serviceConfig = $clientConfig['interfaces'][$serviceName];
$retryCodes = $serviceConfig['retry_codes'];
$retryParams = $serviceConfig['retry_params'];
foreach ($serviceConfig['methods'] as $methodName => $methodConfig) {
$timeoutMillis = $methodConfig['timeout_millis'];
if (empty($methodConfig['retry_codes_name']) || empty($methodConfig['retry_params_name'])) {
// Construct a RetrySettings object with retries disabled
$retrySettings = self::constructDefault()->with([
'noRetriesRpcTimeoutMillis' => $timeoutMillis,
]);
} else {
$retryCodesName = $methodConfig['retry_codes_name'];
$retryParamsName = $methodConfig['retry_params_name'];
if (!array_key_exists($retryCodesName, $retryCodes)) {
throw new ValidationException("Invalid retry_codes_name setting: '$retryCodesName'");
}
if (!array_key_exists($retryParamsName, $retryParams)) {
throw new ValidationException("Invalid retry_params_name setting: '$retryParamsName'");
}
foreach ($retryCodes[$retryCodesName] as $status) {
if (!ApiStatus::isValidStatus($status)) {
throw new ValidationException("Invalid status code: '$status'");
}
}
$retryParameters = self::convertArrayFromSnakeCase($retryParams[$retryParamsName]) + [
'retryableCodes' => $retryCodes[$retryCodesName],
'noRetriesRpcTimeoutMillis' => $timeoutMillis,
];
if ($disableRetries) {
$retryParameters['retriesEnabled'] = false;
}
$retrySettings = new RetrySettings($retryParameters);
}
$serviceRetrySettings[$methodName] = $retrySettings;
}
return $serviceRetrySettings;
}
public static function constructDefault()
{
return new RetrySettings([
'retriesEnabled' => false,
'noRetriesRpcTimeoutMillis' => 30000,
'initialRetryDelayMillis' => 100,
'retryDelayMultiplier' => 1.3,
'maxRetryDelayMillis' => 60000,
'initialRpcTimeoutMillis' => 20000,
'rpcTimeoutMultiplier' => 1,
'maxRpcTimeoutMillis' => 20000,
'totalTimeoutMillis' => 600000,
'retryableCodes' => [],
'maxRetries' => self::DEFAULT_MAX_RETRIES,
'retryFunction' => null]);
}
/**
* Creates a new instance of RetrySettings that updates the settings in the existing instance
* with the settings specified in the $settings parameter.
*
* @param array $settings {
* Settings for configuring the retry behavior. Supports all of the options supported by
* the constructor; see {@see \Google\ApiCore\RetrySettings::__construct()}. All parameters
* are optional - all unset parameters will default to the value in the existing instance.
* }
* @return RetrySettings
*/
public function with(array $settings)
{
$existingSettings = [
'initialRetryDelayMillis' => $this->getInitialRetryDelayMillis(),
'retryDelayMultiplier' => $this->getRetryDelayMultiplier(),
'maxRetryDelayMillis' => $this->getMaxRetryDelayMillis(),
'initialRpcTimeoutMillis' => $this->getInitialRpcTimeoutMillis(),
'rpcTimeoutMultiplier' => $this->getRpcTimeoutMultiplier(),
'maxRpcTimeoutMillis' => $this->getMaxRpcTimeoutMillis(),
'totalTimeoutMillis' => $this->getTotalTimeoutMillis(),
'retryableCodes' => $this->getRetryableCodes(),
'retriesEnabled' => $this->retriesEnabled(),
'noRetriesRpcTimeoutMillis' => $this->getNoRetriesRpcTimeoutMillis(),
'maxRetries' => $this->getMaxRetries(),
'retryFunction' => $this->getRetryFunction(),
];
return new RetrySettings($settings + $existingSettings);
}
/**
* Creates an associative array of the {@see \Google\ApiCore\RetrySettings} timeout fields configured
* with the given timeout specified in the $timeout parameter interpreted as a logical timeout.
*
* @param int $timeout The timeout in milliseconds to be used as a logical call timeout.
* @return array
*/
public static function logicalTimeout(int $timeout)
{
return [
'initialRpcTimeoutMillis' => $timeout,
'maxRpcTimeoutMillis' => $timeout,
'totalTimeoutMillis' => $timeout,
'noRetriesRpcTimeoutMillis' => $timeout,
'rpcTimeoutMultiplier' => 1.0
];
}
/**
* @return bool Returns true if retries are enabled, otherwise returns false.
*/
public function retriesEnabled()
{
return $this->retriesEnabled;
}
/**
* @return int The timeout of the rpc call to be used if $retriesEnabled is false,
* in milliseconds.
*/
public function getNoRetriesRpcTimeoutMillis()
{
return $this->noRetriesRpcTimeoutMillis;
}
/**
* @return int[] Status codes to retry
*/
public function getRetryableCodes()
{
return $this->retryableCodes;
}
/**
* @return int The initial retry delay in milliseconds. If $this->retriesEnabled()
* is false, this setting is unused.
*/
public function getInitialRetryDelayMillis()
{
return $this->initialRetryDelayMillis;
}
/**
* @return float The retry delay multiplier. If $this->retriesEnabled()
* is false, this setting is unused.
*/
public function getRetryDelayMultiplier()
{
return $this->retryDelayMultiplier;
}
/**
* @return int The maximum retry delay in milliseconds. If $this->retriesEnabled()
* is false, this setting is unused.
*/
public function getMaxRetryDelayMillis()
{
return $this->maxRetryDelayMillis;
}
/**
* @return int The initial rpc timeout in milliseconds. If $this->retriesEnabled()
* is false, this setting is unused - use noRetriesRpcTimeoutMillis to
* set the timeout in that case.
*/
public function getInitialRpcTimeoutMillis()
{
return $this->initialRpcTimeoutMillis;
}
/**
* @return float The rpc timeout multiplier. If $this->retriesEnabled()
* is false, this setting is unused.
*/
public function getRpcTimeoutMultiplier()
{
return $this->rpcTimeoutMultiplier;
}
/**
* @return int The maximum rpc timeout in milliseconds. If $this->retriesEnabled()
* is false, this setting is unused - use noRetriesRpcTimeoutMillis to
* set the timeout in that case.
*/
public function getMaxRpcTimeoutMillis()
{
return $this->maxRpcTimeoutMillis;
}
/**
* @return int The total time in milliseconds to spend on the call, including all
* retry attempts and delays between attempts. If $this->retriesEnabled()
* is false, this setting is unused - use noRetriesRpcTimeoutMillis to
* set the timeout in that case.
*/
public function getTotalTimeoutMillis()
{
return $this->totalTimeoutMillis;
}
/**
* @experimental
*/
public function getMaxRetries()
{
return $this->maxRetries;
}
/**
* @experimental
*/
public function getRetryFunction()
{
return $this->retryFunction;
}
private static function convertArrayFromSnakeCase(array $settings)
{
$camelCaseSettings = [];
foreach ($settings as $key => $value) {
$camelCaseKey = str_replace(' ', '', ucwords(str_replace('_', ' ', $key)));
$camelCaseSettings[lcfirst($camelCaseKey)] = $value;
}
return $camelCaseSettings;
}
}
src/Serializer.php 0000644 00000042041 15174273622 0010167 0 ustar 00 \Google\Rpc\RetryInfo::class,
'google.rpc.debuginfo-bin' => \Google\Rpc\DebugInfo::class,
'google.rpc.quotafailure-bin' => \Google\Rpc\QuotaFailure::class,
'google.rpc.badrequest-bin' => \Google\Rpc\BadRequest::class,
'google.rpc.requestinfo-bin' => \Google\Rpc\RequestInfo::class,
'google.rpc.resourceinfo-bin' => \Google\Rpc\ResourceInfo::class,
'google.rpc.errorinfo-bin' => \Google\Rpc\ErrorInfo::class,
'google.rpc.help-bin' => \Google\Rpc\Help::class,
'google.rpc.localizedmessage-bin' => \Google\Rpc\LocalizedMessage::class,
];
private $fieldTransformers;
private $messageTypeTransformers;
private $decodeFieldTransformers;
private $decodeMessageTypeTransformers;
private $descriptorMaps = [];
/**
* Serializer constructor.
*
* @param array $fieldTransformers An array mapping field names to transformation functions
* @param array $messageTypeTransformers An array mapping message names to transformation functions
* @param array $decodeFieldTransformers An array mapping field names to transformation functions
* @param array $decodeMessageTypeTransformers An array mapping message names to transformation functions
*/
public function __construct(
$fieldTransformers = [],
$messageTypeTransformers = [],
$decodeFieldTransformers = [],
$decodeMessageTypeTransformers = []
) {
$this->fieldTransformers = $fieldTransformers;
$this->messageTypeTransformers = $messageTypeTransformers;
$this->decodeFieldTransformers = $decodeFieldTransformers;
$this->decodeMessageTypeTransformers = $decodeMessageTypeTransformers;
}
/**
* Encode protobuf message as a PHP array
*
* @param mixed $message
* @return array
* @throws ValidationException
*/
public function encodeMessage($message)
{
// Get message descriptor
$pool = DescriptorPool::getGeneratedPool();
$messageType = $pool->getDescriptorByClassName(get_class($message));
try {
return $this->encodeMessageImpl($message, $messageType);
} catch (\Exception $e) {
throw new ValidationException(
"Error encoding message: " . $e->getMessage(),
$e->getCode(),
$e
);
}
}
/**
* Decode PHP array into the specified protobuf message
*
* @param mixed $message
* @param array $data
* @return mixed
* @throws ValidationException
*/
public function decodeMessage($message, array $data)
{
// Get message descriptor
$pool = DescriptorPool::getGeneratedPool();
$messageType = $pool->getDescriptorByClassName(get_class($message));
try {
return $this->decodeMessageImpl($message, $messageType, $data);
} catch (\Exception $e) {
throw new ValidationException(
"Error decoding message: " . $e->getMessage(),
$e->getCode(),
$e
);
}
}
/**
* @param Message $message
* @return string Json representation of $message
* @throws ValidationException
*/
public static function serializeToJson(Message $message)
{
return json_encode(self::serializeToPhpArray($message), JSON_PRETTY_PRINT);
}
/**
* @param Message $message
* @return array PHP array representation of $message
* @throws ValidationException
*/
public static function serializeToPhpArray(Message $message)
{
return self::getPhpArraySerializer()->encodeMessage($message);
}
/**
* Decode metadata received from gRPC status object
*
* @param array $metadata
* @return array
*/
public static function decodeMetadata(array $metadata)
{
if (count($metadata) == 0) {
return [];
}
$result = [];
foreach ($metadata as $key => $values) {
foreach ($values as $value) {
$decodedValue = [
'@type' => $key,
];
if (self::hasBinaryHeaderSuffix($key)) {
if (isset(self::$metadataKnownTypes[$key])) {
$class = self::$metadataKnownTypes[$key];
/** @var Message $message */
$message = new $class();
try {
$message->mergeFromString($value);
$decodedValue += self::serializeToPhpArray($message);
} catch (\Exception $e) {
// We encountered an error trying to deserialize the data
$decodedValue += [
'data' => '',
];
}
} else {
// The metadata contains an unexpected binary type
$decodedValue += [
'data' => '',
];
}
} else {
$decodedValue += [
'data' => $value,
];
}
$result[] = $decodedValue;
}
}
return $result;
}
/**
* Decode an array of Any messages into a printable PHP array.
*
* @param iterable $anyArray
* @return array
*/
public static function decodeAnyMessages($anyArray)
{
$results = [];
foreach ($anyArray as $any) {
try {
/** @var Any $any */
/** @var Message $unpacked */
$unpacked = $any->unpack();
$results[] = self::serializeToPhpArray($unpacked);
} catch (\Exception $ex) {
echo "$ex\n";
// failed to unpack the $any object - show as unknown binary data
$results[] = [
'typeUrl' => $any->getTypeUrl(),
'value' => '',
];
}
}
return $results;
}
/**
* @param FieldDescriptor $field
* @param Message|array|string $data
* @return mixed
* @throws \Exception
*/
private function encodeElement(FieldDescriptor $field, $data)
{
switch ($field->getType()) {
case GPBType::MESSAGE:
if (is_array($data)) {
$result = $data;
} else {
$result = $this->encodeMessageImpl($data, $field->getMessageType());
}
$messageType = $field->getMessageType()->getFullName();
if (isset($this->messageTypeTransformers[$messageType])) {
$result = $this->messageTypeTransformers[$messageType]($result);
}
break;
default:
$result = $data;
break;
}
if (isset($this->fieldTransformers[$field->getName()])) {
$result = $this->fieldTransformers[$field->getName()]($result);
}
return $result;
}
private function getDescriptorMaps(Descriptor $descriptor)
{
if (!isset($this->descriptorMaps[$descriptor->getFullName()])) {
$fieldsByName = [];
$fieldCount = $descriptor->getFieldCount();
for ($i = 0; $i < $fieldCount; $i++) {
$field = $descriptor->getField($i);
$fieldsByName[$field->getName()] = $field;
}
$fieldToOneof = [];
$oneofCount = $descriptor->getOneofDeclCount();
for ($i = 0; $i < $oneofCount; $i++) {
$oneof = $descriptor->getOneofDecl($i);
$oneofFieldCount = $oneof->getFieldCount();
for ($j = 0; $j < $oneofFieldCount; $j++) {
$field = $oneof->getField($j);
$fieldToOneof[$field->getName()] = $oneof->getName();
}
}
$this->descriptorMaps[$descriptor->getFullName()] = [$fieldsByName, $fieldToOneof];
}
return $this->descriptorMaps[$descriptor->getFullName()];
}
/**
* @param Message $message
* @param Descriptor $messageType
* @return array
* @throws \Exception
*/
private function encodeMessageImpl(Message $message, Descriptor $messageType)
{
$data = [];
$fieldCount = $messageType->getFieldCount();
for ($i = 0; $i < $fieldCount; $i++) {
$field = $messageType->getField($i);
$key = $field->getName();
$getter = $this->getGetter($key);
$v = $message->$getter();
if (is_null($v)) {
continue;
}
// Check and skip unset fields inside oneofs
list($_, $fieldsToOneof) = $this->getDescriptorMaps($messageType);
if (isset($fieldsToOneof[$key])) {
$oneofName = $fieldsToOneof[$key];
$oneofGetter = $this->getGetter($oneofName);
if ($message->$oneofGetter() !== $key) {
continue;
}
}
if ($field->isMap()) {
list($mapFieldsByName, $_) = $this->getDescriptorMaps($field->getMessageType());
$keyField = $mapFieldsByName[self::MAP_KEY_FIELD_NAME];
$valueField = $mapFieldsByName[self::MAP_VALUE_FIELD_NAME];
$arr = [];
foreach ($v as $k => $vv) {
$arr[$this->encodeElement($keyField, $k)] = $this->encodeElement($valueField, $vv);
}
$v = $arr;
} elseif ($field->getLabel() === GPBLabel::REPEATED) {
$arr = [];
foreach ($v as $k => $vv) {
$arr[$k] = $this->encodeElement($field, $vv);
}
$v = $arr;
} else {
$v = $this->encodeElement($field, $v);
}
$key = self::toCamelCase($key);
$data[$key] = $v;
}
return $data;
}
/**
* @param FieldDescriptor $field
* @param mixed $data
* @return mixed
* @throws \Exception
*/
private function decodeElement(FieldDescriptor $field, $data)
{
if (isset($this->decodeFieldTransformers[$field->getName()])) {
$data = $this->decodeFieldTransformers[$field->getName()]($data);
}
switch ($field->getType()) {
case GPBType::MESSAGE:
if ($data instanceof Message) {
return $data;
}
$messageType = $field->getMessageType();
$messageTypeName = $messageType->getFullName();
$klass = $messageType->getClass();
$msg = new $klass();
if (isset($this->decodeMessageTypeTransformers[$messageTypeName])) {
$data = $this->decodeMessageTypeTransformers[$messageTypeName]($data);
}
return $this->decodeMessageImpl($msg, $messageType, $data);
default:
return $data;
}
}
/**
* @param Message $message
* @param Descriptor $messageType
* @param array $data
* @return mixed
* @throws \Exception
*/
private function decodeMessageImpl(Message $message, Descriptor $messageType, array $data)
{
list($fieldsByName, $_) = $this->getDescriptorMaps($messageType);
foreach ($data as $key => $v) {
// Get the field by tag number or name
$fieldName = self::toSnakeCase($key);
// Unknown field found
if (!isset($fieldsByName[$fieldName])) {
throw new RuntimeException(sprintf(
"cannot handle unknown field %s on message %s",
$fieldName,
$messageType->getFullName()
));
}
/** @var FieldDescriptor $field */
$field = $fieldsByName[$fieldName];
if ($field->isMap()) {
list($mapFieldsByName, $_) = $this->getDescriptorMaps($field->getMessageType());
$keyField = $mapFieldsByName[self::MAP_KEY_FIELD_NAME];
$valueField = $mapFieldsByName[self::MAP_VALUE_FIELD_NAME];
$arr = [];
foreach ($v as $k => $vv) {
$arr[$this->decodeElement($keyField, $k)] = $this->decodeElement($valueField, $vv);
}
$value = $arr;
} elseif ($field->getLabel() === GPBLabel::REPEATED) {
$arr = [];
foreach ($v as $k => $vv) {
$arr[$k] = $this->decodeElement($field, $vv);
}
$value = $arr;
} else {
$value = $this->decodeElement($field, $v);
}
$setter = $this->getSetter($field->getName());
$message->$setter($value);
// We must unset $value here, otherwise the protobuf c extension will mix up the references
// and setting one value will change all others
unset($value);
}
return $message;
}
/**
* @param string $name
* @return string Getter function
*/
public static function getGetter(string $name)
{
return 'get' . ucfirst(self::toCamelCase($name));
}
/**
* @param string $name
* @return string Setter function
*/
public static function getSetter(string $name)
{
return 'set' . ucfirst(self::toCamelCase($name));
}
/**
* Convert string from camelCase to snake_case
*
* @param string $key
* @return string
*/
public static function toSnakeCase(string $key)
{
return strtolower(preg_replace(['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'], '$1_$2', $key));
}
/**
* Convert string from snake_case to camelCase
*
* @param string $key
* @return string
*/
public static function toCamelCase(string $key)
{
return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key))));
}
private static function hasBinaryHeaderSuffix(string $key)
{
return substr_compare($key, "-bin", strlen($key) - 4) === 0;
}
private static function getPhpArraySerializer()
{
if (is_null(self::$phpArraySerializer)) {
self::$phpArraySerializer = new Serializer();
}
return self::$phpArraySerializer;
}
public static function loadKnownMetadataTypes()
{
foreach (self::$metadataKnownTypes as $key => $class) {
new $class;
}
}
}
// It is necessary to call this when this file is included. Otherwise we cannot be
// guaranteed that the relevant classes will be loaded into the protobuf descriptor
// pool when we try to unpack an Any object containing that class.
// phpcs:disable PSR1.Files.SideEffects
Serializer::loadKnownMetadataTypes();
// phpcs:enable
src/ServerStream.php 0000644 00000006673 15174273622 0010513 0 ustar 00 call = $serverStreamingCall;
if (array_key_exists('resourcesGetMethod', $streamingDescriptor)) {
$this->resourcesGetMethod = $streamingDescriptor['resourcesGetMethod'];
}
}
/**
* A generator which yields results from the server until the streaming call
* completes. Throws an ApiException if the streaming call failed.
*
* @throws ApiException
* @return \Generator|mixed
*/
public function readAll()
{
$resourcesGetMethod = $this->resourcesGetMethod;
if (!is_null($resourcesGetMethod)) {
foreach ($this->call->responses() as $response) {
foreach ($response->$resourcesGetMethod() as $resource) {
yield $resource;
}
}
} else {
foreach ($this->call->responses() as $response) {
yield $response;
}
}
// Errors in the REST transport will be thrown from there and not reach
// this handling. Successful REST server-streams will have an OK status.
$status = $this->call->getStatus();
if ($status->code !== Code::OK) {
throw ApiException::createFromStdClass($status);
}
}
/**
* Return the underlying call object.
*
* @return ServerStreamingCallInterface
*/
public function getServerStreamingCall()
{
return $this->call;
}
}
src/ServerStreamingCallInterface.php 0000644 00000005702 15174273622 0013616 0 ustar 00 $metadata Metadata to send with the call, if applicable
* (optional)
* @param array $options An array of options, possible keys:
* 'flags' => a number (optional)
* @return void
*/
public function start($data, array $metadata = [], array $options = []);
/**
* @return mixed An iterator of response values.
*/
public function responses();
/**
* Return the status of the server stream.
*
* @return \stdClass The API status.
*/
public function getStatus();
/**
* @return mixed The metadata sent by the server.
*/
public function getMetadata();
/**
* @return mixed The trailing metadata sent by the server.
*/
public function getTrailingMetadata();
/**
* @return string The URI of the endpoint.
*/
public function getPeer();
/**
* Cancels the call.
*
* @return void
*/
public function cancel();
/**
* Set the CallCredentials for the underlying Call.
*
* @param mixed $call_credentials The CallCredentials object
*
* @return void
*/
public function setCallCredentials($call_credentials);
}
src/ServiceAddressTrait.php 0000644 00000004753 15174273622 0012000 0 ustar 00 assertSame($expected, $actual);
return;
}
if (is_array($expected) || $expected instanceof RepeatedField) {
if (is_array($expected) === is_array($actual)) {
$this->assertEquals($expected, $actual);
}
$this->assertCount(count($expected), $actual);
$expectedValues = $this->getValues($expected);
$actualValues = $this->getValues($actual);
for ($i = 0; $i < count($expectedValues); $i++) {
$expectedElement = $expectedValues[$i];
$actualElement = $actualValues[$i];
$this->assertProtobufEquals($expectedElement, $actualElement);
}
} else {
$this->assertEquals($expected, $actual);
if ($expected instanceof Message) {
$pool = DescriptorPool::getGeneratedPool();
$descriptor = $pool->getDescriptorByClassName(get_class($expected));
$fieldCount = $descriptor->getFieldCount();
for ($i = 0; $i < $fieldCount; $i++) {
$field = $descriptor->getField($i);
$getter = Serializer::getGetter($field->getName());
$expectedFieldValue = $expected->$getter();
$actualFieldValue = $actual->$getter();
$this->assertProtobufEquals($expectedFieldValue, $actualFieldValue);
}
}
}
}
/**
* @param iterable $field
*/
private function getValues($field)
{
return array_values(
is_array($field)
? $field
: iterator_to_array($field)
);
}
}
src/Testing/MessageAwareArrayComparator.php 0000644 00000001606 15174273622 0015070 0 ustar 00 exporter = new MessageAwareExporter();
}
}
src/Testing/MessageAwareExporter.php 0000644 00000002432 15174273622 0013570 0 ustar 00 responses = $responses;
$this->deserialize = $deserialize;
if (is_null($status)) {
$status = new MockStatus(Code::OK);
}
$this->status = $status;
}
/**
* @return mixed|null
* @throws ApiException
*/
public function read()
{
if (count($this->responses) > 0) {
$resp = array_shift($this->responses);
if (is_null($resp)) {
// Null was added to the responses list to simulate a failed stream
// To ensure that getStatus can now be called, we clear the remaining
// responses and set writesDone to true
$this->responses = [];
$this->writesDone();
return null;
}
$obj = $this->deserializeMessage($resp, $this->deserialize);
return $obj;
} elseif ($this->writesDone) {
return null;
} else {
throw new ApiException(
"No more responses to read, but closeWrite() not called - "
. "this would be blocking",
Grpc\STATUS_INTERNAL,
null
);
}
}
/**
* @return stdClass|null
* @throws ApiException
*/
public function getStatus()
{
if (count($this->responses) > 0) {
throw new ApiException(
"Calls to getStatus() will block if all responses are not read",
Grpc\STATUS_INTERNAL,
null
);
}
if (!$this->writesDone) {
throw new ApiException(
"Calls to getStatus() will block if closeWrite() not called",
Grpc\STATUS_INTERNAL,
null
);
}
return $this->status;
}
/**
* Save the request object, to be retrieved via getReceivedCalls()
* @param Message|mixed $request The request object
* @param array $options An array of options.
* @throws ApiException
*/
public function write($request, array $options = [])
{
if ($this->writesDone) {
throw new ApiException(
"Cannot call write() after writesDone()",
Grpc\STATUS_INTERNAL,
null
);
}
if (is_a($request, '\Google\Protobuf\Internal\Message')) {
/** @var Message $newRequest */
$newRequest = new $request();
$newRequest->mergeFromString($request->serializeToString());
$request = $newRequest;
}
$this->receivedWrites[] = $request;
}
/**
* Set writesDone to true
*/
public function writesDone()
{
$this->writesDone = true;
}
/**
* Return a list of calls made to write(), and clear $receivedFuncCalls.
*
* @return mixed[] An array of received requests
*/
public function popReceivedCalls()
{
$receivedFuncCallsTemp = $this->receivedWrites;
$this->receivedWrites = [];
return $receivedFuncCallsTemp;
}
}
src/Testing/MockClientStreamingCall.php 0000644 00000010414 15174273622 0014170 0 ustar 00 mockUnaryCall = new MockUnaryCall($response, $deserialize, $status);
}
/**
* Immediately return the preset response object and status.
* @return array The response object and status.
*/
public function wait()
{
$this->waitCalled = true;
return $this->mockUnaryCall->wait();
}
/**
* Save the request object, to be retrieved via getReceivedCalls()
* @param Message|mixed $request The request object
* @param array $options An array of options
* @throws ApiException
*/
public function write($request, array $options = [])
{
if ($this->waitCalled) {
throw new ApiException("Cannot call write() after wait()", Code::INTERNAL, ApiStatus::INTERNAL);
}
if (is_a($request, '\Google\Protobuf\Internal\Message')) {
/** @var Message $newRequest */
$newRequest = new $request();
$newRequest->mergeFromString($request->serializeToString());
$request = $newRequest;
}
$this->receivedWrites[] = $request;
}
/**
* Return a list of calls made to write(), and clear $receivedFuncCalls.
*
* @return mixed[] An array of received requests
*/
public function popReceivedCalls()
{
$receivedFuncCallsTemp = $this->receivedWrites;
$this->receivedWrites = [];
return $receivedFuncCallsTemp;
}
}
src/Testing/MockGrpcTransport.php 0000644 00000010042 15174273622 0013111 0 ustar 00 mockCall = $mockCall;
$opts = ['credentials' => ChannelCredentials::createSsl()];
parent::__construct('', $opts);
}
/**
* @param string $method
* @param array $arguments
* @param callable $deserialize
*/
protected function _simpleRequest(
$method,
$arguments,
$deserialize,
array $metadata = [],
array $options = []
) {
$this->logCall($method, $deserialize, $metadata, $options, $arguments);
return $this->mockCall;
}
/**
* @param string $method
* @param callable $deserialize
*/
protected function _clientStreamRequest(
$method,
$deserialize,
array $metadata = [],
array $options = []
) {
$this->logCall($method, $deserialize, $metadata, $options);
return $this->mockCall;
}
/**
* @param string $method
* @param array $arguments
* @param callable $deserialize
*/
protected function _serverStreamRequest(
$method,
$arguments,
$deserialize,
array $metadata = [],
array $options = []
) {
$this->logCall($method, $deserialize, $metadata, $options, $arguments);
return $this->mockCall;
}
/**
* @param string $method
* @param callable $deserialize
*/
protected function _bidiRequest(
$method,
$deserialize,
array $metadata = [],
array $options = []
) {
$this->logCall($method, $deserialize, $metadata, $options);
return $this->mockCall;
}
/**
* @param string $method
* @param callable $deserialize
* @param array $arguments
*/
private function logCall(
$method,
$deserialize,
array $metadata = [],
array $options = [],
$arguments = null
) {
$this->requestArguments = [
'method' => $method,
'arguments' => $arguments,
'deserialize' => $deserialize,
'metadata' => $metadata,
'options' => $options,
];
}
public function getRequestArguments()
{
return $this->requestArguments;
}
}
src/Testing/MockRequest.php 0000644 00000003576 15174273622 0011747 0 ustar 00 google.apicore.testing.MockRequest
*
* @internal
*/
class MockRequest extends \Google\Protobuf\Internal\Message
{
/**
* Generated from protobuf field string page_token = 1;
*/
protected $page_token = '';
/**
* Generated from protobuf field uint64 page_size = 2;
*/
protected $page_size = 0;
/**
* Constructor.
*
* @param array $data {
* Optional. Data for populating the Message object.
*
* @type string $page_token
* @type int|string $page_size
* }
*/
public function __construct($data = NULL) {
Mocks::initOnce();
parent::__construct($data);
}
/**
* Generated from protobuf field string page_token = 1;
* @return string
*/
public function getPageToken()
{
return $this->page_token;
}
/**
* Generated from protobuf field string page_token = 1;
* @param string $var
* @return $this
*/
public function setPageToken($var)
{
GPBUtil::checkString($var, True);
$this->page_token = $var;
return $this;
}
/**
* Generated from protobuf field uint64 page_size = 2;
* @return int|string
*/
public function getPageSize()
{
return $this->page_size;
}
/**
* Generated from protobuf field uint64 page_size = 2;
* @param int|string $var
* @return $this
*/
public function setPageSize($var)
{
GPBUtil::checkUint64($var);
$this->page_size = $var;
return $this;
}
}
src/Testing/MockRequestBody.php 0000644 00000041305 15174273622 0012555 0 ustar 00 google.apicore.testing.MockRequestBody
*
* @internal
*/
class MockRequestBody extends \Google\Protobuf\Internal\Message
{
/**
* Generated from protobuf field string name = 1;
*/
protected $name = '';
/**
* Generated from protobuf field uint64 number = 2;
*/
protected $number = 0;
/**
* Generated from protobuf field repeated string repeated_field = 3;
*/
private $repeated_field;
/**
* Generated from protobuf field .google.apicore.testing.MockRequestBody nested_message = 4;
*/
protected $nested_message = null;
/**
* Generated from protobuf field .google.protobuf.BytesValue bytes_value = 5;
*/
protected $bytes_value = null;
/**
* Generated from protobuf field .google.protobuf.Duration duration_value = 6;
*/
protected $duration_value = null;
/**
* Generated from protobuf field .google.protobuf.FieldMask field_mask = 7;
*/
protected $field_mask = null;
/**
* Generated from protobuf field .google.protobuf.Int64Value int64_value = 8;
*/
protected $int64_value = null;
/**
* Generated from protobuf field .google.protobuf.ListValue list_value = 9;
*/
protected $list_value = null;
/**
* Generated from protobuf field .google.protobuf.StringValue string_value = 10;
*/
protected $string_value = null;
/**
* Generated from protobuf field .google.protobuf.Struct struct_value = 11;
*/
protected $struct_value = null;
/**
* Generated from protobuf field .google.protobuf.Timestamp timestamp_value = 12;
*/
protected $timestamp_value = null;
/**
* Generated from protobuf field .google.protobuf.Value value_value = 13;
*/
protected $value_value = null;
protected $oneof_field;
/**
* Constructor.
*
* @param array $data {
* Optional. Data for populating the Message object.
*
* @type string $name
* @type int|string $number
* @type string[]|\Google\Protobuf\Internal\RepeatedField $repeated_field
* @type \Google\ApiCore\Testing\MockRequestBody $nested_message
* @type \Google\Protobuf\BytesValue $bytes_value
* @type \Google\Protobuf\Duration $duration_value
* @type \Google\Protobuf\FieldMask $field_mask
* @type \Google\Protobuf\Int64Value $int64_value
* @type \Google\Protobuf\ListValue $list_value
* @type \Google\Protobuf\StringValue $string_value
* @type \Google\Protobuf\Struct $struct_value
* @type \Google\Protobuf\Timestamp $timestamp_value
* @type \Google\Protobuf\Value $value_value
* @type string $field_1
* @type string $field_2
* @type string $field_3
* }
*/
public function __construct($data = NULL) {
\GPBMetadata\ApiCore\Testing\Mocks::initOnce();
parent::__construct($data);
}
/**
* Generated from protobuf field string name = 1;
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Generated from protobuf field string name = 1;
* @param string $var
* @return $this
*/
public function setName($var)
{
GPBUtil::checkString($var, True);
$this->name = $var;
return $this;
}
/**
* Generated from protobuf field uint64 number = 2;
* @return int|string
*/
public function getNumber()
{
return $this->number;
}
/**
* Generated from protobuf field uint64 number = 2;
* @param int|string $var
* @return $this
*/
public function setNumber($var)
{
GPBUtil::checkUint64($var);
$this->number = $var;
return $this;
}
/**
* Generated from protobuf field repeated string repeated_field = 3;
* @return \Google\Protobuf\Internal\RepeatedField
*/
public function getRepeatedField()
{
return $this->repeated_field;
}
/**
* Generated from protobuf field repeated string repeated_field = 3;
* @param string[]|\Google\Protobuf\Internal\RepeatedField $var
* @return $this
*/
public function setRepeatedField($var)
{
$arr = GPBUtil::checkRepeatedField($var, \Google\Protobuf\Internal\GPBType::STRING);
$this->repeated_field = $arr;
return $this;
}
/**
* Generated from protobuf field .google.apicore.testing.MockRequestBody nested_message = 4;
* @return \Google\ApiCore\Testing\MockRequestBody
*/
public function getNestedMessage()
{
return isset($this->nested_message) ? $this->nested_message : null;
}
public function hasNestedMessage()
{
return isset($this->nested_message);
}
public function clearNestedMessage()
{
unset($this->nested_message);
}
/**
* Generated from protobuf field .google.apicore.testing.MockRequestBody nested_message = 4;
* @param \Google\ApiCore\Testing\MockRequestBody $var
* @return $this
*/
public function setNestedMessage($var)
{
GPBUtil::checkMessage($var, \Google\ApiCore\Testing\MockRequestBody::class);
$this->nested_message = $var;
return $this;
}
/**
* Generated from protobuf field .google.protobuf.BytesValue bytes_value = 5;
* @return \Google\Protobuf\BytesValue
*/
public function getBytesValue()
{
return isset($this->bytes_value) ? $this->bytes_value : null;
}
public function hasBytesValue()
{
return isset($this->bytes_value);
}
public function clearBytesValue()
{
unset($this->bytes_value);
}
/**
* Returns the unboxed value from getBytesValue()
* Generated from protobuf field .google.protobuf.BytesValue bytes_value = 5;
* @return string|null
*/
public function getBytesValueUnwrapped()
{
return $this->readWrapperValue("bytes_value");
}
/**
* Generated from protobuf field .google.protobuf.BytesValue bytes_value = 5;
* @param \Google\Protobuf\BytesValue $var
* @return $this
*/
public function setBytesValue($var)
{
GPBUtil::checkMessage($var, \Google\Protobuf\BytesValue::class);
$this->bytes_value = $var;
return $this;
}
/**
* Sets the field by wrapping a primitive type in a Google\Protobuf\BytesValue object.
* Generated from protobuf field .google.protobuf.BytesValue bytes_value = 5;
* @param string|null $var
* @return $this
*/
public function setBytesValueUnwrapped($var)
{
$this->writeWrapperValue("bytes_value", $var);
return $this;}
/**
* Generated from protobuf field .google.protobuf.Duration duration_value = 6;
* @return \Google\Protobuf\Duration
*/
public function getDurationValue()
{
return isset($this->duration_value) ? $this->duration_value : null;
}
public function hasDurationValue()
{
return isset($this->duration_value);
}
public function clearDurationValue()
{
unset($this->duration_value);
}
/**
* Generated from protobuf field .google.protobuf.Duration duration_value = 6;
* @param \Google\Protobuf\Duration $var
* @return $this
*/
public function setDurationValue($var)
{
GPBUtil::checkMessage($var, \Google\Protobuf\Duration::class);
$this->duration_value = $var;
return $this;
}
/**
* Generated from protobuf field .google.protobuf.FieldMask field_mask = 7;
* @return \Google\Protobuf\FieldMask
*/
public function getFieldMask()
{
return isset($this->field_mask) ? $this->field_mask : null;
}
public function hasFieldMask()
{
return isset($this->field_mask);
}
public function clearFieldMask()
{
unset($this->field_mask);
}
/**
* Generated from protobuf field .google.protobuf.FieldMask field_mask = 7;
* @param \Google\Protobuf\FieldMask $var
* @return $this
*/
public function setFieldMask($var)
{
GPBUtil::checkMessage($var, \Google\Protobuf\FieldMask::class);
$this->field_mask = $var;
return $this;
}
/**
* Generated from protobuf field .google.protobuf.Int64Value int64_value = 8;
* @return \Google\Protobuf\Int64Value
*/
public function getInt64Value()
{
return isset($this->int64_value) ? $this->int64_value : null;
}
public function hasInt64Value()
{
return isset($this->int64_value);
}
public function clearInt64Value()
{
unset($this->int64_value);
}
/**
* Returns the unboxed value from getInt64Value()
* Generated from protobuf field .google.protobuf.Int64Value int64_value = 8;
* @return int|string|null
*/
public function getInt64ValueUnwrapped()
{
return $this->readWrapperValue("int64_value");
}
/**
* Generated from protobuf field .google.protobuf.Int64Value int64_value = 8;
* @param \Google\Protobuf\Int64Value $var
* @return $this
*/
public function setInt64Value($var)
{
GPBUtil::checkMessage($var, \Google\Protobuf\Int64Value::class);
$this->int64_value = $var;
return $this;
}
/**
* Sets the field by wrapping a primitive type in a Google\Protobuf\Int64Value object.
* Generated from protobuf field .google.protobuf.Int64Value int64_value = 8;
* @param int|string|null $var
* @return $this
*/
public function setInt64ValueUnwrapped($var)
{
$this->writeWrapperValue("int64_value", $var);
return $this;}
/**
* Generated from protobuf field .google.protobuf.ListValue list_value = 9;
* @return \Google\Protobuf\ListValue
*/
public function getListValue()
{
return isset($this->list_value) ? $this->list_value : null;
}
public function hasListValue()
{
return isset($this->list_value);
}
public function clearListValue()
{
unset($this->list_value);
}
/**
* Generated from protobuf field .google.protobuf.ListValue list_value = 9;
* @param \Google\Protobuf\ListValue $var
* @return $this
*/
public function setListValue($var)
{
GPBUtil::checkMessage($var, \Google\Protobuf\ListValue::class);
$this->list_value = $var;
return $this;
}
/**
* Generated from protobuf field .google.protobuf.StringValue string_value = 10;
* @return \Google\Protobuf\StringValue
*/
public function getStringValue()
{
return isset($this->string_value) ? $this->string_value : null;
}
public function hasStringValue()
{
return isset($this->string_value);
}
public function clearStringValue()
{
unset($this->string_value);
}
/**
* Returns the unboxed value from getStringValue()
* Generated from protobuf field .google.protobuf.StringValue string_value = 10;
* @return string|null
*/
public function getStringValueUnwrapped()
{
return $this->readWrapperValue("string_value");
}
/**
* Generated from protobuf field .google.protobuf.StringValue string_value = 10;
* @param \Google\Protobuf\StringValue $var
* @return $this
*/
public function setStringValue($var)
{
GPBUtil::checkMessage($var, \Google\Protobuf\StringValue::class);
$this->string_value = $var;
return $this;
}
/**
* Sets the field by wrapping a primitive type in a Google\Protobuf\StringValue object.
* Generated from protobuf field .google.protobuf.StringValue string_value = 10;
* @param string|null $var
* @return $this
*/
public function setStringValueUnwrapped($var)
{
$this->writeWrapperValue("string_value", $var);
return $this;}
/**
* Generated from protobuf field .google.protobuf.Struct struct_value = 11;
* @return \Google\Protobuf\Struct
*/
public function getStructValue()
{
return isset($this->struct_value) ? $this->struct_value : null;
}
public function hasStructValue()
{
return isset($this->struct_value);
}
public function clearStructValue()
{
unset($this->struct_value);
}
/**
* Generated from protobuf field .google.protobuf.Struct struct_value = 11;
* @param \Google\Protobuf\Struct $var
* @return $this
*/
public function setStructValue($var)
{
GPBUtil::checkMessage($var, \Google\Protobuf\Struct::class);
$this->struct_value = $var;
return $this;
}
/**
* Generated from protobuf field .google.protobuf.Timestamp timestamp_value = 12;
* @return \Google\Protobuf\Timestamp
*/
public function getTimestampValue()
{
return isset($this->timestamp_value) ? $this->timestamp_value : null;
}
public function hasTimestampValue()
{
return isset($this->timestamp_value);
}
public function clearTimestampValue()
{
unset($this->timestamp_value);
}
/**
* Generated from protobuf field .google.protobuf.Timestamp timestamp_value = 12;
* @param \Google\Protobuf\Timestamp $var
* @return $this
*/
public function setTimestampValue($var)
{
GPBUtil::checkMessage($var, \Google\Protobuf\Timestamp::class);
$this->timestamp_value = $var;
return $this;
}
/**
* Generated from protobuf field .google.protobuf.Value value_value = 13;
* @return \Google\Protobuf\Value
*/
public function getValueValue()
{
return isset($this->value_value) ? $this->value_value : null;
}
public function hasValueValue()
{
return isset($this->value_value);
}
public function clearValueValue()
{
unset($this->value_value);
}
/**
* Generated from protobuf field .google.protobuf.Value value_value = 13;
* @param \Google\Protobuf\Value $var
* @return $this
*/
public function setValueValue($var)
{
GPBUtil::checkMessage($var, \Google\Protobuf\Value::class);
$this->value_value = $var;
return $this;
}
/**
* Generated from protobuf field string field_1 = 14;
* @return string
*/
public function getField1()
{
return $this->readOneof(14);
}
public function hasField1()
{
return $this->hasOneof(14);
}
/**
* Generated from protobuf field string field_1 = 14;
* @param string $var
* @return $this
*/
public function setField1($var)
{
GPBUtil::checkString($var, True);
$this->writeOneof(14, $var);
return $this;
}
/**
* Generated from protobuf field string field_2 = 15;
* @return string
*/
public function getField2()
{
return $this->readOneof(15);
}
public function hasField2()
{
return $this->hasOneof(15);
}
/**
* Generated from protobuf field string field_2 = 15;
* @param string $var
* @return $this
*/
public function setField2($var)
{
GPBUtil::checkString($var, True);
$this->writeOneof(15, $var);
return $this;
}
/**
* Generated from protobuf field string field_3 = 16;
* @return string
*/
public function getField3()
{
return $this->readOneof(16);
}
public function hasField3()
{
return $this->hasOneof(16);
}
/**
* Generated from protobuf field string field_3 = 16;
* @param string $var
* @return $this
*/
public function setField3($var)
{
GPBUtil::checkString($var, True);
$this->writeOneof(16, $var);
return $this;
}
/**
* @return string
*/
public function getOneofField()
{
return $this->whichOneof("oneof_field");
}
}
src/Testing/MockResponse.php 0000644 00000010237 15174273622 0012105 0 ustar 00 google.apicore.testing.MockResponse
*
* @internal
*/
class MockResponse extends \Google\Protobuf\Internal\Message
{
/**
* Generated from protobuf field string name = 1;
*/
protected $name = '';
/**
* Generated from protobuf field uint64 number = 2;
*/
protected $number = 0;
/**
* Generated from protobuf field repeated string resources_list = 3;
*/
private $resources_list;
/**
* Generated from protobuf field string next_page_token = 4;
*/
protected $next_page_token = '';
/**
* Generated from protobuf field map resources_map = 5;
*/
private $resources_map;
/**
* Constructor.
*
* @param array $data {
* Optional. Data for populating the Message object.
*
* @type string $name
* @type int|string $number
* @type string[]|\Google\Protobuf\Internal\RepeatedField $resources_list
* @type string $next_page_token
* @type array|\Google\Protobuf\Internal\MapField $resources_map
* }
*/
public function __construct($data = NULL) {
\GPBMetadata\ApiCore\Testing\Mocks::initOnce();
parent::__construct($data);
}
/**
* Generated from protobuf field string name = 1;
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Generated from protobuf field string name = 1;
* @param string $var
* @return $this
*/
public function setName($var)
{
GPBUtil::checkString($var, True);
$this->name = $var;
return $this;
}
/**
* Generated from protobuf field uint64 number = 2;
* @return int|string
*/
public function getNumber()
{
return $this->number;
}
/**
* Generated from protobuf field uint64 number = 2;
* @param int|string $var
* @return $this
*/
public function setNumber($var)
{
GPBUtil::checkUint64($var);
$this->number = $var;
return $this;
}
/**
* Generated from protobuf field repeated string resources_list = 3;
* @return \Google\Protobuf\Internal\RepeatedField
*/
public function getResourcesList()
{
return $this->resources_list;
}
/**
* Generated from protobuf field repeated string resources_list = 3;
* @param string[]|\Google\Protobuf\Internal\RepeatedField $var
* @return $this
*/
public function setResourcesList($var)
{
$arr = GPBUtil::checkRepeatedField($var, \Google\Protobuf\Internal\GPBType::STRING);
$this->resources_list = $arr;
return $this;
}
/**
* Generated from protobuf field string next_page_token = 4;
* @return string
*/
public function getNextPageToken()
{
return $this->next_page_token;
}
/**
* Generated from protobuf field string next_page_token = 4;
* @param string $var
* @return $this
*/
public function setNextPageToken($var)
{
GPBUtil::checkString($var, True);
$this->next_page_token = $var;
return $this;
}
/**
* Generated from protobuf field map resources_map = 5;
* @return \Google\Protobuf\Internal\MapField
*/
public function getResourcesMap()
{
return $this->resources_map;
}
/**
* Generated from protobuf field map resources_map = 5;
* @param array|\Google\Protobuf\Internal\MapField $var
* @return $this
*/
public function setResourcesMap($var)
{
$arr = GPBUtil::checkMapField($var, \Google\Protobuf\Internal\GPBType::STRING, \Google\Protobuf\Internal\GPBType::STRING);
$this->resources_map = $arr;
return $this;
}
}
src/Testing/MockServerStreamingCall.php 0000644 00000007016 15174273622 0014224 0 ustar 00 responses = $responses;
$this->deserialize = $deserialize;
if (is_null($status)) {
$status = new MockStatus(Code::OK, 'OK', []);
} elseif ($status instanceof stdClass) {
if (!property_exists($status, 'metadata')) {
$status->metadata = [];
}
}
$this->status = $status;
}
public function responses()
{
while (count($this->responses) > 0) {
$resp = array_shift($this->responses);
$obj = $this->deserializeMessage($resp, $this->deserialize);
yield $obj;
}
}
/**
* @return stdClass|null
* @throws ApiException
*/
public function getStatus()
{
if (count($this->responses) > 0) {
throw new ApiException(
"Calls to getStatus() will block if all responses are not read",
Code::INTERNAL,
ApiStatus::INTERNAL
);
}
return $this->status;
}
}
src/Testing/MockStatus.php 0000644 00000003704 15174273622 0011573 0 ustar 00 code = $code;
$this->details = $details;
$this->metadata = $metadata;
}
}
src/Testing/MockStubTrait.php 0000644 00000025533 15174273622 0012235 0 ustar 00 deserialize = $deserialize;
}
/**
* Overrides the _simpleRequest method in \Grpc\BaseStub
* (https://github.com/grpc/grpc/blob/master/src/php/lib/Grpc/BaseStub.php)
* Returns a MockUnaryCall object that will return the first item from $responses
* @param string $method The API method name to be called
* @param \Google\Protobuf\Internal\Message $argument The request object to the API method
* @param callable $deserialize A function to deserialize the response object
* @param array $metadata
* @param array $options
* @return MockUnaryCall
*/
public function _simpleRequest(
$method,
$argument,
$deserialize,
array $metadata = [],
array $options = []
) {
$this->receivedFuncCalls[] = new ReceivedRequest($method, $argument, $deserialize, $metadata, $options);
if (count($this->responses) < 1) {
throw new UnderflowException("ran out of responses");
}
list($response, $status) = array_shift($this->responses);
$call = new MockUnaryCall($response, $deserialize, $status);
$this->callObjects[] = $call;
return $call;
}
/**
* Overrides the _clientStreamRequest method in \Grpc\BaseStub
* (https://github.com/grpc/grpc/blob/master/src/php/lib/Grpc/BaseStub.php)
* Returns a MockClientStreamingCall object that will return the first item from $responses
*
* @param string $method The name of the method to call
* @param callable $deserialize A function that deserializes the responses
* @param array $metadata A metadata map to send to the server
* (optional)
* @param array $options An array of options (optional)
*
* @return MockClientStreamingCall The active call object
*/
public function _clientStreamRequest(
$method,
$deserialize,
array $metadata = [],
array $options = []
) {
$this->receivedFuncCalls[] = new ReceivedRequest($method, null, $deserialize, $metadata, $options);
if (count($this->responses) < 1) {
throw new UnderflowException("ran out of responses");
}
list($response, $status) = array_shift($this->responses);
$call = new MockClientStreamingCall($response, $deserialize, $status);
$this->callObjects[] = $call;
return $call;
}
/**
* Overrides the _serverStreamRequest method in \Grpc\BaseStub
* (https://github.com/grpc/grpc/blob/master/src/php/lib/Grpc/BaseStub.php)
* Returns a MockServerStreamingCall object that will stream items from $responses, and return
* a final status of $serverStreamingStatus.
*
* @param string $method The name of the method to call
* @param \Google\Protobuf\Internal\Message $argument The argument to the method
* @param callable $deserialize A function that deserializes the responses
* @param array $metadata A metadata map to send to the server
* (optional)
* @param array $options An array of options (optional)
*
* @return MockServerStreamingCall The active call object
*/
public function _serverStreamRequest(
$method,
$argument,
$deserialize,
array $metadata = [],
array $options = []
) {
if (is_a($argument, '\Google\Protobuf\Internal\Message')) {
/** @var Message $newArgument */
$newArgument = new $argument();
$newArgument->mergeFromString($argument->serializeToString());
$argument = $newArgument;
}
$this->receivedFuncCalls[] = new ReceivedRequest($method, $argument, $deserialize, $metadata, $options);
$responses = self::stripStatusFromResponses($this->responses);
$this->responses = [];
$call = new MockServerStreamingCall($responses, $deserialize, $this->serverStreamingStatus);
$this->callObjects[] = $call;
return $call;
}
/**
* Overrides the _bidiRequest method in \Grpc\BaseStub
* (https://github.com/grpc/grpc/blob/master/src/php/lib/Grpc/BaseStub.php)
* Returns a MockBidiStreamingCall object that will stream items from $responses, and return
* a final status of $serverStreamingStatus.
*
* @param string $method The name of the method to call
* @param callable $deserialize A function that deserializes the responses
* @param array $metadata A metadata map to send to the server
* (optional)
* @param array $options An array of options (optional)
*
* @return MockBidiStreamingCall The active call object
*/
public function _bidiRequest(
$method,
$deserialize,
array $metadata = [],
array $options = []
) {
$this->receivedFuncCalls[] = new ReceivedRequest($method, null, $deserialize, $metadata, $options);
$responses = self::stripStatusFromResponses($this->responses);
$this->responses = [];
$call = new MockBidiStreamingCall($responses, $deserialize, $this->serverStreamingStatus);
$this->callObjects[] = $call;
return $call;
}
public static function stripStatusFromResponses($responses)
{
$strippedResponses = [];
foreach ($responses as $response) {
list($resp, $_) = $response;
$strippedResponses[] = $resp;
}
return $strippedResponses;
}
/**
* Add a response object, and an optional status, to the list of responses to be returned via
* _simpleRequest.
* @param \Google\Protobuf\Internal\Message $response
* @param stdClass $status
*/
public function addResponse($response, stdClass $status = null)
{
if (!$this->deserialize && $response) {
$this->deserialize = [get_class($response), 'decode'];
}
if (is_a($response, '\Google\Protobuf\Internal\Message')) {
$response = $response->serializeToString();
}
$this->responses[] = [$response, $status];
}
/**
* Set the status object to be used when creating streaming calls.
*
* @param stdClass $status
*/
public function setStreamingStatus(stdClass $status)
{
$this->serverStreamingStatus = $status;
}
/**
* Return a list of calls made to _simpleRequest, and clear $receivedFuncCalls.
*
* @return ReceivedRequest[] An array of received requests
*/
public function popReceivedCalls()
{
$receivedFuncCallsTemp = $this->receivedFuncCalls;
$this->receivedFuncCalls = [];
return $receivedFuncCallsTemp;
}
/**
* @return int The number of calls received.
*/
public function getReceivedCallCount()
{
return count($this->receivedFuncCalls);
}
/**
* @return mixed[] The call objects created by calls to the stub
*/
public function popCallObjects()
{
$callObjectsTemp = $this->callObjects;
$this->callObjects = [];
return $callObjectsTemp;
}
/**
* @return bool True if $receivedFuncCalls and $response are empty.
*/
public function isExhausted()
{
return count($this->receivedFuncCalls) === 0
&& count($this->responses) === 0;
}
/**
* @param mixed $responseObject
* @param stdClass|null $status
* @param callable $deserialize
* @return static An instance of the current class type.
*/
public static function create($responseObject, stdClass $status = null, callable $deserialize = null)
{
$stub = new static($deserialize); // @phpstan-ignore-line
$stub->addResponse($responseObject, $status);
return $stub;
}
/**
* Creates a sequence such that the responses are returned in order.
* @param mixed[] $sequence
* @param callable $deserialize
* @param stdClass $finalStatus
* @return static An instance of the current class type.
*/
public static function createWithResponseSequence(array $sequence, callable $deserialize = null, stdClass $finalStatus = null)
{
$stub = new static($deserialize); // @phpstan-ignore-line
foreach ($sequence as $elem) {
if (count($elem) == 1) {
list($resp, $status) = [$elem, null];
} else {
list($resp, $status) = $elem;
}
$stub->addResponse($resp, $status);
}
if ($finalStatus) {
$stub->setStreamingStatus($finalStatus);
}
return $stub;
}
}
src/Testing/MockTransport.php 0000644 00000010110 15174273622 0012271 0 ustar 00 agentHeaderDescriptor = $agentHeaderDescriptor;
}
public function startUnaryCall(Call $call, array $options)
{
$call = call_user_func([$this, $call->getMethod()], $call, $options);
return $promise = new Promise(
function () use ($call, &$promise) {
list($response, $status) = $call->wait();
if ($status->code == Code::OK) {
$promise->resolve($response);
} else {
throw ApiException::createFromStdClass($status);
}
},
[$call, 'cancel']
);
}
public function startBidiStreamingCall(Call $call, array $options)
{
$newArgs = ['/' . $call->getMethod(), $this->deserialize, $options, $options];
$response = $this->_bidiRequest(...$newArgs);
return new BidiStream($response, $call->getDescriptor());
}
public function startClientStreamingCall(Call $call, array $options)
{
$newArgs = ['/' . $call->getMethod(), $this->deserialize, $options, $options];
$response = $this->_clientStreamRequest(...$newArgs);
return new ClientStream($response, $call->getDescriptor());
}
public function startServerStreamingCall(Call $call, array $options)
{
$newArgs = ['/' . $call->getMethod(), $call->getMessage(), $this->deserialize, $options, $options];
$response = $this->_serverStreamRequest(...$newArgs);
return new ServerStream($response, $call->getDescriptor());
}
public function __call(string $name, array $arguments)
{
$call = $arguments[0];
$options = $arguments[1];
$decode = $call->getDecodeType() ? [$call->getDecodeType(), 'decode'] : null;
return $this->_simpleRequest(
'/' . $call->getMethod(),
$call->getMessage(),
$decode,
isset($options['headers']) ? $options['headers'] : [],
$options
);
}
public function close()
{
// does nothing
}
}
src/Testing/MockUnaryCall.php 0000644 00000006062 15174273622 0012202 0 ustar 00 response = $response;
$this->deserialize = $deserialize;
if (is_null($status)) {
$status = new MockStatus(Code::OK);
}
$this->status = $status;
}
/**
* Immediately return the preset response object and status.
* @return array The response object and status.
*/
public function wait()
{
return [
$this->deserializeMessage($this->response, $this->deserialize),
$this->status,
];
}
}
src/Testing/ProtobufGPBEmptyComparator.php 0000644 00000004210 15174273622 0014667 0 ustar 00 exporter = new MessageAwareExporter();
}
/**
* Returns whether the comparator can compare two values.
*
* @param mixed $expected The first value to compare
* @param mixed $actual The second value to compare
* @return boolean
*/
public function accepts($expected, $actual)
{
return $expected instanceof Message && $actual instanceof Message;
}
/**
* Asserts that two values are equal.
*
* @param Message $expected The first value to compare
* @param Message $actual The second value to compare
* @param float|int $delta The allowed numerical distance between two values to
* consider them equal
* @param bool $canonicalize If set to TRUE, arrays are sorted before
* comparison
* @param bool $ignoreCase If set to TRUE, upper- and lowercasing is
* ignored when comparing string values
* @throws ComparisonFailure Thrown when the comparison
* fails. Contains information about the
* specific errors that lead to the failure.
*/
public function assertEquals($expected, $actual, $delta = 0, $canonicalize = FALSE, $ignoreCase = FALSE)
{
if ($expected->serializeToString() !== $actual->serializeToString()) {
throw new ComparisonFailure(
$expected,
$actual,
$this->exporter->shortenedExport($expected),
$this->exporter->shortenedExport($actual),
false,
'Given 2 Message objects are not the same'
);
}
}
}
src/Testing/ReceivedRequest.php 0000644 00000005051 15174273622 0012572 0 ustar 00 actualCall = [
'funcCall' => $funcCall,
'request' => $requestObject,
'deserialize' => $deserialize,
'metadata' => $metadata,
'options' => $options,
];
}
public function getArray()
{
return $this->actualCall;
}
public function getFuncCall()
{
return $this->actualCall['funcCall'];
}
public function getRequestObject()
{
return $this->actualCall['request'];
}
public function getMetadata()
{
return $this->actualCall['metadata'];
}
public function getOptions()
{
return $this->actualCall['options'];
}
}
src/Testing/SerializationTrait.php 0000644 00000004763 15174273622 0013325 0 ustar 00 $deserializeFunc($message);
} elseif (is_string($message)) {
$obj->mergeFromString($message);
}
return $obj;
}
// Protobuf-PHP implementation
return call_user_func($deserialize, $message);
}
}
src/Testing/mocks.proto 0000644 00000002403 15174273622 0011161 0 ustar 00 syntax = "proto3";
package google.apicore.testing;
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/wrappers.proto";
option php_namespace = "Google\\ApiCore\\Testing";
option php_metadata_namespace = "GPBMetadata\\ApiCore\\Testing";
message MockRequest {
string page_token = 1;
uint64 page_size = 2;
}
message MockResponse {
string name = 1;
uint64 number = 2;
repeated string resources_list = 3;
string next_page_token = 4;
map resources_map = 5;
}
message MockRequestBody {
string name = 1;
uint64 number = 2;
repeated string repeated_field = 3;
MockRequestBody nested_message = 4;
google.protobuf.BytesValue bytes_value = 5;
google.protobuf.Duration duration_value = 6;
google.protobuf.FieldMask field_mask = 7;
google.protobuf.Int64Value int64_value = 8;
google.protobuf.ListValue list_value = 9;
google.protobuf.StringValue string_value = 10;
google.protobuf.Struct struct_value = 11;
google.protobuf.Timestamp timestamp_value = 12;
google.protobuf.Value value_value = 13;
oneof oneof_field {
string field_1 = 14;
string field_2 = 15;
string field_3 = 16;
}
}
src/Transport/Grpc/ForwardingCall.php 0000644 00000005151 15174273622 0013644 0 ustar 00 innerCall = $innerCall;
}
/**
* @return mixed The metadata sent by the server
*/
public function getMetadata()
{
return $this->innerCall->getMetadata();
}
/**
* @return mixed The trailing metadata sent by the server
*/
public function getTrailingMetadata()
{
return $this->innerCall->getTrailingMetadata();
}
/**
* @return string The URI of the endpoint
*/
public function getPeer()
{
return $this->innerCall->getPeer();
}
/**
* Cancels the call.
*/
public function cancel()
{
$this->innerCall->cancel();
}
}
src/Transport/Grpc/ForwardingServerStreamingCall.php 0000644 00000004450 15174273622 0016706 0 ustar 00 innerCall->responses();
}
/**
* Wait for the server to send the status, and return it.
*
* @return \stdClass The status object, with integer $code, string
* $details, and array $metadata members
*/
public function getStatus()
{
return $this->innerCall->getStatus();
}
}
src/Transport/Grpc/ForwardingUnaryCall.php 0000644 00000003764 15174273622 0014673 0 ustar 00 innerCall->wait();
}
}
src/Transport/Grpc/ServerStreamingCallWrapper.php 0000644 00000006527 15174273622 0016233 0 ustar 00 stream = $stream;
}
/**
* {@inheritdoc}
*/
public function start($data, array $metadata = [], array $callOptions = [])
{
$this->stream->start($data, $metadata, $callOptions);
}
/**
* {@inheritdoc}
*/
public function responses()
{
foreach ($this->stream->responses() as $response) {
yield $response;
}
}
/**
* {@inheritdoc}
*/
public function getStatus()
{
return $this->stream->getStatus();
}
/**
* {@inheritdoc}
*/
public function getMetadata()
{
return $this->stream->getMetadata();
}
/**
* {@inheritdoc}
*/
public function getTrailingMetadata()
{
return $this->stream->getTrailingMetadata();
}
/**
* {@inheritdoc}
*/
public function getPeer()
{
return $this->stream->getPeer();
}
/**
* {@inheritdoc}
*/
public function cancel()
{
$this->stream->cancel();
}
/**
* {@inheritdoc}
*/
public function setCallCredentials($call_credentials)
{
$this->stream->setCallCredentials($call_credentials);
}
}
src/Transport/Grpc/UnaryInterceptorInterface.php 0000644 00000004416 15174273622 0016107 0 ustar 00 baseUri = $baseUri;
$this->httpHandler = $httpHandler;
$this->transportName = 'grpc-fallback';
}
/**
* Builds a GrpcFallbackTransport.
*
* @param string $apiEndpoint
* The address of the API remote host, for example "example.googleapis.com".
* @param array $config {
* Config options used to construct the grpc-fallback transport.
*
* @type callable $httpHandler A handler used to deliver PSR-7 requests.
* }
* @return GrpcFallbackTransport
* @throws ValidationException
*/
public static function build(string $apiEndpoint, array $config = [])
{
$config += [
'httpHandler' => null,
'clientCertSource' => null,
];
list($baseUri, $port) = self::normalizeServiceAddress($apiEndpoint);
$httpHandler = $config['httpHandler'] ?: self::buildHttpHandlerAsync();
$transport = new GrpcFallbackTransport("$baseUri:$port", $httpHandler);
if ($config['clientCertSource']) {
$transport->configureMtlsChannel($config['clientCertSource']);
}
return $transport;
}
/**
* {@inheritdoc}
*/
public function startUnaryCall(Call $call, array $options)
{
$httpHandler = $this->httpHandler;
return $httpHandler(
$this->buildRequest($call, $options),
$this->getCallOptions($options)
)->then(
function (ResponseInterface $response) use ($options) {
if (isset($options['metadataCallback'])) {
$metadataCallback = $options['metadataCallback'];
$metadataCallback($response->getHeaders());
}
return $response;
}
)->then(
function (ResponseInterface $response) use ($call) {
return $this->unpackResponse($call, $response);
},
function (\Exception $ex) {
throw $this->transformException($ex);
}
);
}
/**
* @param Call $call
* @param array $options
* @return RequestInterface
*/
private function buildRequest(Call $call, array $options)
{
// Build common headers and set the content type to 'application/x-protobuf'
$headers = ['Content-Type' => 'application/x-protobuf'] + self::buildCommonHeaders($options);
// It is necessary to supply 'grpc-web' in the 'x-goog-api-client' header
// when using the grpc-fallback protocol.
$headers += ['x-goog-api-client' => []];
$headers['x-goog-api-client'][] = 'grpc-web';
// Uri format: https:///$rpc/
$uri = "https://{$this->baseUri}/\$rpc/{$call->getMethod()}";
return new Request(
'POST',
$uri,
$headers,
$call->getMessage()->serializeToString()
);
}
/**
* @param Call $call
* @param ResponseInterface $response
* @return Message
*/
private function unpackResponse(Call $call, ResponseInterface $response)
{
$decodeType = $call->getDecodeType();
/** @var Message $responseMessage */
$responseMessage = new $decodeType;
$responseMessage->mergeFromString((string)$response->getBody());
return $responseMessage;
}
/**
* @param array $options
* @return array
*/
private function getCallOptions(array $options)
{
$callOptions = $options['transportOptions']['grpcFallbackOptions'] ?? [];
if (isset($options['timeoutMillis'])) {
$callOptions['timeout'] = $options['timeoutMillis'] / 1000;
}
if ($this->clientCertSource) {
list($cert, $key) = self::loadClientCertSource($this->clientCertSource);
$callOptions['cert'] = $cert;
$callOptions['key'] = $key;
}
return $callOptions;
}
/**
* @param \Exception $ex
* @return \Exception
*/
private function transformException(\Exception $ex)
{
if ($ex instanceof RequestException && $ex->hasResponse()) {
$res = $ex->getResponse();
$body = (string) $res->getBody();
$status = new Status();
try {
$status->mergeFromString($body);
return ApiException::createFromRpcStatus($status);
} catch (\Exception $parseException) {
// We were unable to parse the response body into a $status object. Instead,
// create an ApiException using the unparsed $body as message.
$code = ApiStatus::rpcCodeFromHttpStatusCode($res->getStatusCode());
return ApiException::createFromApiResponse($body, $code, null, $parseException);
}
} else {
return $ex;
}
}
}
src/Transport/GrpcTransport.php 0000644 00000024155 15174273622 0012670 0 ustar 00 [],
'channel' => null,
'interceptors' => [],
'clientCertSource' => null,
];
list($addr, $port) = self::normalizeServiceAddress($apiEndpoint);
$host = "$addr:$port";
$stubOpts = $config['stubOpts'];
// Set the required 'credentials' key in stubOpts if it is not already set. Use
// array_key_exists because null is a valid value.
if (!array_key_exists('credentials', $stubOpts)) {
if (isset($config['clientCertSource'])) {
list($cert, $key) = self::loadClientCertSource($config['clientCertSource']);
$stubOpts['credentials'] = ChannelCredentials::createSsl(null, $key, $cert);
} else {
$stubOpts['credentials'] = ChannelCredentials::createSsl();
}
}
$channel = $config['channel'];
if (!is_null($channel) && !($channel instanceof Channel)) {
throw new ValidationException(
"Channel argument to GrpcTransport must be of type \Grpc\Channel, " .
"instead got: " . print_r($channel, true)
);
}
try {
return new GrpcTransport($host, $stubOpts, $channel, $config['interceptors']);
} catch (Exception $ex) {
throw new ValidationException(
"Failed to build GrpcTransport: " . $ex->getMessage(),
$ex->getCode(),
$ex
);
}
}
/**
* {@inheritdoc}
*/
public function startBidiStreamingCall(Call $call, array $options)
{
return new BidiStream(
$this->_bidiRequest(
'/' . $call->getMethod(),
[$call->getDecodeType(), 'decode'],
isset($options['headers']) ? $options['headers'] : [],
$this->getCallOptions($options)
),
$call->getDescriptor()
);
}
/**
* {@inheritdoc}
*/
public function startClientStreamingCall(Call $call, array $options)
{
return new ClientStream(
$this->_clientStreamRequest(
'/' . $call->getMethod(),
[$call->getDecodeType(), 'decode'],
isset($options['headers']) ? $options['headers'] : [],
$this->getCallOptions($options)
),
$call->getDescriptor()
);
}
/**
* {@inheritdoc}
*/
public function startServerStreamingCall(Call $call, array $options)
{
$message = $call->getMessage();
if (!$message) {
throw new \InvalidArgumentException('A message is required for ServerStreaming calls.');
}
// This simultaenously creates and starts a \Grpc\ServerStreamingCall.
$stream = $this->_serverStreamRequest(
'/' . $call->getMethod(),
$message,
[$call->getDecodeType(), 'decode'],
isset($options['headers']) ? $options['headers'] : [],
$this->getCallOptions($options)
);
return new ServerStream(
new ServerStreamingCallWrapper($stream),
$call->getDescriptor()
);
}
/**
* {@inheritdoc}
*/
public function startUnaryCall(Call $call, array $options)
{
$unaryCall = $this->_simpleRequest(
'/' . $call->getMethod(),
$call->getMessage(),
[$call->getDecodeType(), 'decode'],
isset($options['headers']) ? $options['headers'] : [],
$this->getCallOptions($options)
);
/** @var Promise $promise */
$promise = new Promise(
function () use ($unaryCall, $options, &$promise) {
list($response, $status) = $unaryCall->wait();
if ($status->code == Code::OK) {
if (isset($options['metadataCallback'])) {
$metadataCallback = $options['metadataCallback'];
$metadataCallback($unaryCall->getMetadata());
}
$promise->resolve($response);
} else {
throw ApiException::createFromStdClass($status);
}
},
[$unaryCall, 'cancel']
);
return $promise;
}
private function getCallOptions(array $options)
{
$callOptions = $options['transportOptions']['grpcOptions'] ?? [];
if (isset($options['credentialsWrapper'])) {
$audience = $options['audience'] ?? null;
$credentialsWrapper = $options['credentialsWrapper'];
$callOptions['call_credentials_callback'] = $credentialsWrapper
->getAuthorizationHeaderCallback($audience);
}
if (isset($options['timeoutMillis'])) {
$callOptions['timeout'] = $options['timeoutMillis'] * 1000;
}
return $callOptions;
}
private static function loadClientCertSource(callable $clientCertSource)
{
return call_user_func($clientCertSource);
}
}
src/Transport/HttpUnaryTransportTrait.php 0000644 00000012350 15174273622 0014731 0 ustar 00 throwUnsupportedException();
}
/**
* {@inheritdoc}
* @return never
* @throws \BadMethodCallException
*/
public function startServerStreamingCall(Call $call, array $options)
{
$this->throwUnsupportedException();
}
/**
* {@inheritdoc}
* @return never
* @throws \BadMethodCallException
*/
public function startBidiStreamingCall(Call $call, array $options)
{
$this->throwUnsupportedException();
}
/**
* {@inheritdoc}
*/
public function close()
{
// Nothing to do.
}
/**
* @param array $options
* @return array
*/
private static function buildCommonHeaders(array $options)
{
$headers = $options['headers'] ?? [];
if (!is_array($headers)) {
throw new \InvalidArgumentException(
'The "headers" option must be an array'
);
}
// If not already set, add an auth header to the request
if (!isset($headers['Authorization']) && isset($options['credentialsWrapper'])) {
$credentialsWrapper = $options['credentialsWrapper'];
$audience = $options['audience'] ?? null;
$callback = $credentialsWrapper
->getAuthorizationHeaderCallback($audience);
// Prevent unexpected behavior, as the authorization header callback
// uses lowercase "authorization"
unset($headers['authorization']);
$authHeaders = $callback();
if (!is_array($authHeaders)) {
throw new \UnexpectedValueException(
'Expected array response from authorization header callback'
);
}
$headers += $authHeaders;
}
return $headers;
}
/**
* @return callable
* @throws ValidationException
*/
private static function buildHttpHandlerAsync()
{
try {
return [HttpHandlerFactory::build(), 'async'];
} catch (Exception $ex) {
throw new ValidationException("Failed to build HttpHandler", $ex->getCode(), $ex);
}
}
/**
* Set the path to a client certificate.
*
* @param callable $clientCertSource
*/
private function configureMtlsChannel(callable $clientCertSource)
{
$this->clientCertSource = $clientCertSource;
}
/**
* @return never
* @throws \BadMethodCallException
*/
private function throwUnsupportedException()
{
throw new \BadMethodCallException(
"Streaming calls are not supported while using the {$this->transportName} transport."
);
}
private static function loadClientCertSource(callable $clientCertSource)
{
$certFile = tempnam(sys_get_temp_dir(), 'cert');
$keyFile = tempnam(sys_get_temp_dir(), 'key');
list($cert, $key) = call_user_func($clientCertSource);
file_put_contents($certFile, $cert);
file_put_contents($keyFile, $key);
// the key and the cert are returned in one temporary file
return [$certFile, $keyFile];
}
}
src/Transport/Rest/JsonStreamDecoder.php 0000644 00000022167 15174273622 0014351 0 ustar 00 $options {
* An array of optional arguments.
*
* @type bool $ignoreUnknown
* Toggles whether or not to throw an exception when an unknown field
* is encountered in a response message. The default is true.
* @type int $readChunkSizeBytes
* The upper size limit in bytes that can be read at a time from the
* response stream. The default is 1 KB.
* }
*
* @experimental
*/
public function __construct(StreamInterface $stream, string $decodeType, array $options = [])
{
$this->stream = $stream;
$this->decodeType = $decodeType;
if (isset($options['ignoreUnknown'])) {
$this->ignoreUnknown = $options['ignoreUnknown'];
}
if (isset($options['readChunkSize'])) {
$this->readChunkSize = $options['readChunkSizeBytes'];
}
}
/**
* Begins decoding the configured response stream. It is a generator which
* yields messages of the given decode type from the stream until the stream
* completes. Throws an Exception if the stream is closed before the closing
* byte is read or if it encounters an error while decoding a message.
*
* @throws RuntimeException
* @return \Generator
*/
public function decode()
{
try {
foreach ($this->_decode() as $response) {
yield $response;
}
} catch (RuntimeException $re) {
$msg = $re->getMessage();
$streamClosedException =
strpos($msg, 'Stream is detached') !== false ||
strpos($msg, 'Unexpected stream close') !== false;
// Only throw the exception if close() was not called and it was not
// a closing-related exception.
if (!$this->closeCalled || !$streamClosedException) {
throw $re;
}
}
}
/**
* @return \Generator
*/
private function _decode()
{
$decodeType = $this->decodeType;
$str = false;
$prev = $chunk = '';
$start = $end = $cursor = $level = 0;
while ($chunk !== '' || !$this->stream->eof()) {
// Read up to $readChunkSize bytes from the stream.
$chunk .= $this->stream->read($this->readChunkSize);
// If the response stream has been closed and the only byte
// remaining is the closing array bracket, we are done.
if ($this->stream->eof() && $chunk === ']') {
$level--;
break;
}
// Parse the freshly read data available in $chunk.
$chunkLength = strlen($chunk);
while ($cursor < $chunkLength) {
// Access the next byte for processing.
$b = $chunk[$cursor];
// Track open/close double quotes of a key or value. Do not
// toggle flag with the pervious byte was an escape character.
if ($b === '"' && $prev !== self::ESCAPE_CHAR) {
$str = !$str;
}
// Skip over new lines that break up items.
if ($b === "\n" && $level === 1) {
$start++;
}
// Ignore commas separating messages in the stream array.
if ($b === ',' && $level === 1) {
$start++;
}
// Track the opening of a new array or object if not in a string
// value.
if (($b === '{' || $b === '[') && !$str) {
$level++;
// Opening of the array/root object.
// Do not include it in the messageBuffer.
if ($level === 1) {
$start++;
}
}
// Track the closing of an object if not in a string value.
if ($b === '}' && !$str) {
$level--;
if ($level === 1) {
$end = $cursor+1;
}
}
// Track the closing of an array if not in a string value.
if ($b === ']' && !$str) {
$level--;
// If we are seeing a closing square bracket at the
// response message level, e.g. {"foo], there is a problem.
if ($level === 1) {
throw new \RuntimeException('Received closing byte mid-message');
}
}
// A message-closing byte was just buffered. Decode the
// message with the decode type, clearing the messageBuffer,
// and yield it.
//
// TODO(noahdietz): Support google.protobuf.*Value messages that
// are encoded as primitives and separated by commas.
if ($end !== 0) {
$length = $end - $start;
/** @var \Google\Protobuf\Internal\Message $return */
$return = new $decodeType();
$return->mergeFromJsonString(
substr($chunk, $start, $length),
$this->ignoreUnknown
);
yield $return;
// Dump the part of the chunk used for parsing the message
// and use the remaining for the next message.
$remaining = $chunkLength-$length;
$chunk = substr($chunk, $end, $remaining);
// Reset all indices and exit chunk processing.
$start = 0;
$end = 0;
$cursor = 0;
break;
}
$cursor++;
// An escaped back slash should not escape the following character.
if ($b === self::ESCAPE_CHAR && $prev === self::ESCAPE_CHAR) {
$b = '';
}
$prev = $b;
}
// If after attempting to process the chunk, no progress was made and the
// stream is closed, we must break as the stream has closed prematurely.
if ($cursor === $chunkLength && $this->stream->eof()) {
break;
}
}
if ($level > 0) {
throw new \RuntimeException('Unexpected stream close before receiving the closing byte');
}
}
/**
* Closes the underlying stream. If the stream is actively being decoded, an
* exception will not be thrown due to the interruption.
*
* @return void
*/
public function close()
{
$this->closeCalled = true;
$this->stream->close();
}
}
src/Transport/Rest/RestServerStreamingCall.php 0000644 00000013371 15174273622 0015545 0 ustar 00 */
private array $decoderOptions;
private RequestInterface $originalRequest;
private ?JsonStreamDecoder $decoder;
private string $decodeType;
private ?ResponseInterface $response;
private stdClass $status;
/**
* @param callable $httpHandler
* @param string $decodeType
* @param array $decoderOptions
*/
public function __construct(callable $httpHandler, string $decodeType, array $decoderOptions)
{
$this->httpHandler = $httpHandler;
$this->decodeType = $decodeType;
$this->decoderOptions = $decoderOptions;
}
/**
* {@inheritdoc}
*/
public function start($request, array $headers = [], array $callOptions = [])
{
$this->originalRequest = $this->appendHeaders($request, $headers);
try {
$handler = $this->httpHandler;
$response = $handler(
$this->originalRequest,
$callOptions
)->wait();
} catch (\Exception $ex) {
if ($ex instanceof RequestException && $ex->hasResponse()) {
$ex = ApiException::createFromRequestException($ex, /* isStream */ true);
}
throw $ex;
}
// Create an OK Status for a successful request just so that it
// has a return value.
$this->status = new stdClass();
$this->status->code = Code::OK;
$this->status->message = ApiStatus::OK;
$this->status->details = [];
$this->response = $response;
}
/**
* @param RequestInterface $request
* @param array $headers
* @return RequestInterface
*/
private function appendHeaders(RequestInterface $request, array $headers)
{
foreach ($headers as $key => $value) {
$request = $request->hasHeader($key) ?
$request->withAddedHeader($key, $value) :
$request->withHeader($key, $value);
}
return $request;
}
/**
* {@inheritdoc}
*/
public function responses()
{
if (is_null($this->response)) {
throw new \Exception('Stream has not been started.');
}
// Decode the stream and yield responses as they are read.
$this->decoder = new JsonStreamDecoder(
$this->response->getBody(),
$this->decodeType,
$this->decoderOptions
);
foreach ($this->decoder->decode() as $message) {
yield $message;
}
}
/**
* Return the status of the server stream. If the call has not been started
* this will be null.
*
* @return stdClass The status, with integer $code, string
* $details, and array $metadata members
*/
public function getStatus()
{
return $this->status;
}
/**
* {@inheritdoc}
*/
public function getMetadata()
{
return is_null($this->response) ? null : $this->response->getHeaders();
}
/**
* The Rest transport does not support trailing metadata. This is a
* passthrough to getMetadata().
*/
public function getTrailingMetadata()
{
return $this->getMetadata();
}
/**
* {@inheritdoc}
*/
public function getPeer()
{
return $this->originalRequest->getUri();
}
/**
* {@inheritdoc}
*/
public function cancel()
{
if (!is_null($this->decoder)) {
$this->decoder->close();
}
}
/**
* For the REST transport this is a no-op.
* {@inheritdoc}
*/
public function setCallCredentials($call_credentials)
{
// Do nothing.
}
}
src/Transport/RestTransport.php 0000644 00000022616 15174273622 0012712 0 ustar 00 requestBuilder = $requestBuilder;
$this->httpHandler = $httpHandler;
$this->transportName = 'REST';
}
/**
* Builds a RestTransport.
*
* @param string $apiEndpoint
* The address of the API remote host, for example "example.googleapis.com".
* @param string $restConfigPath
* Path to rest config file.
* @param array $config {
* Config options used to construct the gRPC transport.
*
* @type callable $httpHandler A handler used to deliver PSR-7 requests.
* @type callable $clientCertSource A callable which returns the client cert as a string.
* }
* @return RestTransport
* @throws ValidationException
*/
public static function build(string $apiEndpoint, string $restConfigPath, array $config = [])
{
$config += [
'httpHandler' => null,
'clientCertSource' => null,
];
list($baseUri, $port) = self::normalizeServiceAddress($apiEndpoint);
$requestBuilder = new RequestBuilder("$baseUri:$port", $restConfigPath);
$httpHandler = $config['httpHandler'] ?: self::buildHttpHandlerAsync();
$transport = new RestTransport($requestBuilder, $httpHandler);
if ($config['clientCertSource']) {
$transport->configureMtlsChannel($config['clientCertSource']);
}
return $transport;
}
/**
* {@inheritdoc}
*/
public function startUnaryCall(Call $call, array $options)
{
$headers = self::buildCommonHeaders($options);
// call the HTTP handler
$httpHandler = $this->httpHandler;
return $httpHandler(
$this->requestBuilder->build(
$call->getMethod(),
$call->getMessage(),
$headers
),
$this->getCallOptions($options)
)->then(
function (ResponseInterface $response) use ($call, $options) {
$decodeType = $call->getDecodeType();
/** @var Message $return */
$return = new $decodeType;
$body = (string) $response->getBody();
// In some rare cases LRO response metadata may not be loaded
// in the descriptor pool, triggering an exception. The catch
// statement handles this case and attempts to add the LRO
// metadata type to the pool by directly instantiating the
// metadata class.
try {
$return->mergeFromJsonString(
$body,
true
);
} catch (\Exception $ex) {
if (!isset($options['metadataReturnType'])) {
throw $ex;
}
if (strpos($ex->getMessage(), 'Error occurred during parsing:') !== 0) {
throw $ex;
}
new $options['metadataReturnType']();
$return->mergeFromJsonString(
$body,
true
);
}
if (isset($options['metadataCallback'])) {
$metadataCallback = $options['metadataCallback'];
$metadataCallback($response->getHeaders());
}
return $return;
},
function (\Exception $ex) {
if ($ex instanceof RequestException && $ex->hasResponse()) {
throw ApiException::createFromRequestException($ex);
}
throw $ex;
}
);
}
/**
* {@inheritdoc}
* @throws \BadMethodCallException for forwards compatibility with older GAPIC clients
*/
public function startServerStreamingCall(Call $call, array $options)
{
$message = $call->getMessage();
if (!$message) {
throw new \InvalidArgumentException('A message is required for ServerStreaming calls.');
}
// Maintain forwards compatibility with older GAPIC clients not configured for REST server streaming
// @see https://github.com/googleapis/gax-php/issues/370
if (!$this->requestBuilder->pathExists($call->getMethod())) {
$this->unsupportedServerStreamingCall($call, $options);
}
$headers = self::buildCommonHeaders($options);
$callOptions = $this->getCallOptions($options);
$request = $this->requestBuilder->build(
$call->getMethod(),
$call->getMessage()
// Exclude headers here because they will be added in _serverStreamRequest().
);
$decoderOptions = [];
if (isset($options['decoderOptions'])) {
$decoderOptions = $options['decoderOptions'];
}
return new ServerStream(
$this->_serverStreamRequest(
$this->httpHandler,
$request,
$headers,
$call->getDecodeType(),
$callOptions,
$decoderOptions
),
$call->getDescriptor()
);
}
/**
* Creates and starts a RestServerStreamingCall.
*
* @param callable $httpHandler The HTTP Handler to invoke the request with.
* @param RequestInterface $request The request to invoke.
* @param array $headers The headers to include in the request.
* @param string $decodeType The response stream message type to decode.
* @param array $callOptions The call options to use when making the call.
* @param array $decoderOptions The options to use for the JsonStreamDecoder.
*
* @return RestServerStreamingCall
*/
private function _serverStreamRequest(
$httpHandler,
$request,
$headers,
$decodeType,
$callOptions,
$decoderOptions = []
) {
$call = new RestServerStreamingCall(
$httpHandler,
$decodeType,
$decoderOptions
);
$call->start($request, $headers, $callOptions);
return $call;
}
/**
* @param array $options
*
* @return array
*/
private function getCallOptions(array $options)
{
$callOptions = $options['transportOptions']['restOptions'] ?? [];
if (isset($options['timeoutMillis'])) {
$callOptions['timeout'] = $options['timeoutMillis'] / 1000;
}
if ($this->clientCertSource) {
list($cert, $key) = self::loadClientCertSource($this->clientCertSource);
$callOptions['cert'] = $cert;
$callOptions['key'] = $key;
}
return $callOptions;
}
}
src/Transport/TransportInterface.php 0000644 00000005566 15174273622 0013702 0 ustar 00 $options
*
* @return BidiStream
*/
public function startBidiStreamingCall(Call $call, array $options);
/**
* Starts a client streaming call.
*
* @param Call $call
* @param array $options
*
* @return ClientStream
*/
public function startClientStreamingCall(Call $call, array $options);
/**
* Starts a server streaming call.
*
* @param Call $call
* @param array $options
*
* @return ServerStream
*/
public function startServerStreamingCall(Call $call, array $options);
/**
* Returns a promise used to execute network requests.
*
* @param Call $call
* @param array $options
*
* @return PromiseInterface
* @throws ValidationException
*/
public function startUnaryCall(Call $call, array $options);
/**
* Closes the connection, if one exists.
*
* @return void
*/
public function close();
}
src/UriTrait.php 0000644 00000004516 15174273622 0007626 0 ustar 00 &$v) {
if (is_bool($v)) {
$v = $v ? 'true' : 'false';
}
}
return Utils::uriFor($uri)
->withQuery(
Query::build($query)
);
}
}
src/ValidationException.php 0000644 00000003321 15174273622 0012025 0 ustar 00