.repo-metadata.json000064400000000322151742737350010253 0ustar00{ "language": "php", "distribution_name": "google/gax", "release_level": "stable", "client_documentation": "https://cloud.google.com/php/docs/reference/gax/latest", "library_type": "CORE" } CHANGELOG.md000064400000040034151742737350006374 0ustar00# Changelog ## [1.35.1](https://github.com/googleapis/gax-php/compare/v1.35.0...v1.35.1) (2024-12-04) ### Bug Fixes * Ensure hasEmulator client option is passed to createTransport ([#594](https://github.com/googleapis/gax-php/issues/594)) ([13bbe8a](https://github.com/googleapis/gax-php/commit/13bbe8a2e6df2bfd6c5febba735113f1abcba201)) * Remove implicit null, add php 8.4 ([#599](https://github.com/googleapis/gax-php/issues/599)) ([af0a0e7](https://github.com/googleapis/gax-php/commit/af0a0e708dfbea46de627965db0f63114fcfb67f)) ## [1.35.0](https://github.com/googleapis/gax-php/compare/v1.34.1...v1.35.0) (2024-11-06) ### Features * Add `InsecureRequestBuilder` for emulator ([#582](https://github.com/googleapis/gax-php/issues/582)) ([cc1d047](https://github.com/googleapis/gax-php/commit/cc1d0472a1caf31bb3ecd98da1d6b8588f95b63a)) * **docs:** Use doctum shared workflow for reference docs ([#578](https://github.com/googleapis/gax-php/issues/578)) ([021763f](https://github.com/googleapis/gax-php/commit/021763f255acaffda6ebe34a9d1a01c2bd187326)) * Support for API Key client option ([#351](https://github.com/googleapis/gax-php/issues/351)) ([ab7f04f](https://github.com/googleapis/gax-php/commit/ab7f04fd8c9f7ed33a58155ae6b9e740f365ac2a)) ### Bug Fixes * **tests:** Skip docs tests from forks ([#591](https://github.com/googleapis/gax-php/issues/591)) ([35ae9f7](https://github.com/googleapis/gax-php/commit/35ae9f708d3ef937308d266e3a012296ce8606fc)) ## [1.34.1](https://github.com/googleapis/gax-php/compare/v1.34.0...v1.34.1) (2024-08-13) ### Bug Fixes * Disable universe domain check for MDS ([#575](https://github.com/googleapis/gax-php/issues/575)) ([a47a469](https://github.com/googleapis/gax-php/commit/a47a469d9ef76613c5d320539646323a5e7b978d)) ## [1.34.0](https://github.com/googleapis/gax-php/compare/v1.33.0...v1.34.0) (2024-05-29) ### Features * Support new surface operations clients ([#569](https://github.com/googleapis/gax-php/issues/569)) ([fa06e73](https://github.com/googleapis/gax-php/commit/fa06e738fc63a3b9f70a26e6d20f30c582ef1870)) ## [1.33.0](https://github.com/googleapis/gax-php/compare/v1.32.0...v1.33.0) (2024-05-10) ### Features * Support V2 OperationsClient in OperationResponse ([#564](https://github.com/googleapis/gax-php/issues/564)) ([7f8bb13](https://github.com/googleapis/gax-php/commit/7f8bb13f78463b1e7f2289ce5514763992806e5e)) ## [1.32.0](https://github.com/googleapis/gax-php/compare/v1.31.0...v1.32.0) (2024-04-24) ### Features * Add a custom encoder in Serializer ([#554](https://github.com/googleapis/gax-php/issues/554)) ([be28b5a](https://github.com/googleapis/gax-php/commit/be28b5a859b674a3d398bdaab7ed86b93dd7a593)) ## [1.31.0](https://github.com/googleapis/gax-php/compare/v1.30.1...v1.31.0) (2024-04-22) ### Features * Add the api header to the GapicClientTrait ([#553](https://github.com/googleapis/gax-php/issues/553)) ([cc102eb](https://github.com/googleapis/gax-php/commit/cc102ebdfd63019b1e6bcd51515be2a2cb13534d)) ### Bug Fixes * Add caching and micro optimizations in Serializer ([5a5d8a7](https://github.com/googleapis/gax-php/commit/5a5d8a763d8e2d470a6d960b788e7d2a938cd64f)) * Pass auth http handler to update metadata call ([#557](https://github.com/googleapis/gax-php/issues/557)) ([6e04a50](https://github.com/googleapis/gax-php/commit/6e04a50d013f5686ec5e66c457b9b440b9bcde9e)) ## [1.30.0](https://github.com/googleapis/gax-php/compare/v1.29.1...v1.30.0) (2024-02-28) ### Features * Auto Populate fields configured for auto population in Rpc Request Message ([#543](https://github.com/googleapis/gax-php/issues/543)) ([99d6b89](https://github.com/googleapis/gax-php/commit/99d6b899ddf55d51fab976844c1e0f8fe9918a52)) * Make the default authHttpHandler in CredentialsWrapper null ([#544](https://github.com/googleapis/gax-php/issues/544)) ([2a25eea](https://github.com/googleapis/gax-php/commit/2a25eeacadf2f783f64b4eca4f94e067ddef3eaa)) ## [1.29.1](https://github.com/googleapis/gax-php/compare/v1.29.0...v1.29.1) (2024-02-26) ### Bug Fixes * Allow InsecureCredentialsWrapper::getAuthorizationHeaderCallback to return null ([#541](https://github.com/googleapis/gax-php/issues/541)) ([676f4f7](https://github.com/googleapis/gax-php/commit/676f4f7e3d8925d8aba00285616fdf89440b45f9)) ## [1.29.0](https://github.com/googleapis/gax-php/compare/v1.28.1...v1.29.0) (2024-02-26) ### Features * Add InsecureCredentialsWrapper for Emulator connection ([#538](https://github.com/googleapis/gax-php/issues/538)) ([b5dbeaf](https://github.com/googleapis/gax-php/commit/b5dbeaf33594b300a0c678ffc6a6946b09fce7dd)) ## [1.28.1](https://github.com/googleapis/gax-php/compare/v1.28.0...v1.28.1) (2024-02-20) ### Bug Fixes * Universe domain check for grpc transport ([#534](https://github.com/googleapis/gax-php/issues/534)) ([1026d8a](https://github.com/googleapis/gax-php/commit/1026d8aec73e0aad8949a86ee7575e3edb3d56be)) ## [1.28.0](https://github.com/googleapis/gax-php/compare/v1.27.2...v1.28.0) (2024-02-15) ### Features * Allow setting of universe domain in environment variable ([#520](https://github.com/googleapis/gax-php/issues/520)) ([6e6603b](https://github.com/googleapis/gax-php/commit/6e6603b03285f3f8d1072776cd206720e3990f50)) ## [1.27.2](https://github.com/googleapis/gax-php/compare/v1.27.1...v1.27.2) (2024-02-14) ### Bug Fixes * Typo in TransportOptions option name ([#530](https://github.com/googleapis/gax-php/issues/530)) ([6914fe0](https://github.com/googleapis/gax-php/commit/6914fe04554867bd827be6596fafc751a3d7621a)) ## [1.27.1](https://github.com/googleapis/gax-php/compare/v1.27.0...v1.27.1) (2024-02-14) ### Bug Fixes * Issues in Options classes ([#528](https://github.com/googleapis/gax-php/issues/528)) ([aa9ba3a](https://github.com/googleapis/gax-php/commit/aa9ba3a6bac9324ad894d9677da0e897497ebab2)) ## [1.27.0](https://github.com/googleapis/gax-php/compare/v1.26.3...v1.27.0) (2024-02-07) ### Features * Create ClientOptionsTrait ([#527](https://github.com/googleapis/gax-php/issues/527)) ([cfe2c60](https://github.com/googleapis/gax-php/commit/cfe2c60a36233f74259c96a6799d8492ed7c45d0)) * Implement ProjectIdProviderInterface in CredentialsWrapper ([#523](https://github.com/googleapis/gax-php/issues/523)) ([b56a463](https://github.com/googleapis/gax-php/commit/b56a4635abfeeec08895202da8218e9ba915413e)) * Update ArrayTrait to be consistent with Core ([#526](https://github.com/googleapis/gax-php/issues/526)) ([8e44185](https://github.com/googleapis/gax-php/commit/8e44185dd6f8f8f9ef5b136776cba61ec7a8b8f6)) ### Bug Fixes * Correct exception type for Guzzle promise ([#521](https://github.com/googleapis/gax-php/issues/521)) ([7129373](https://github.com/googleapis/gax-php/commit/712937339c134e1d92cab5fa736cfe1bbcd7f343)) ## [1.26.3](https://github.com/googleapis/gax-php/compare/v1.26.2...v1.26.3) (2024-01-18) ### Bug Fixes * CallOptions should use transportOptions ([#513](https://github.com/googleapis/gax-php/issues/513)) ([2d45ee1](https://github.com/googleapis/gax-php/commit/2d45ee187cdc3619b30c51b653b508718baf3af4)) ## [1.26.2](https://github.com/googleapis/gax-php/compare/v1.26.1...v1.26.2) (2024-01-09) ### Bug Fixes * Ensure modifyClientOptions is called for new surface clients ([#515](https://github.com/googleapis/gax-php/issues/515)) ([68231b8](https://github.com/googleapis/gax-php/commit/68231b896dec8efb86f8986aefba3d247d2a2d1c)) ## [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.md000064400000003675151742737350007374 0ustar00# 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/) LICENSE000064400000002700151742737350005566 0ustar00Copyright 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.md000064400000005055151742737350006046 0ustar00# Google API Core for PHP ![Build Status](https://github.com/googleapis/gax-php/actions/workflows/tests.yml/badge.svg) - [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 8.0 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 > composer install ``` 3. Run tests. ```sh > composer test ``` 4. Updating dependencies after changing `composer.json`: ```sh > composer update ` ``` 5. Formatting source: ```sh > composer cs-lint > composer cs-fix ``` ## License BSD - See [LICENSE][] for more information. [LICENSE]: https://github.com/googleapis/gax-php/blob/main/LICENSE SECURITY.md000064400000000511151742737350006350 0ustar00# 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. VERSION000064400000000007151742737350005627 0ustar001.35.1 composer.json000064400000002652151742737350007311 0ustar00{ "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": "^8.0", "google/auth": "^1.34.0", "google/grpc-gcp": "^0.4", "grpc/grpc": "^1.13", "google/protobuf": "^v3.25.3||^4.26.1", "guzzlehttp/promises": "^2.0", "guzzlehttp/psr7": "^2.0", "google/common-protos": "^4.4", "google/longrunning": "~0.4", "ramsey/uuid": "^4.0" }, "require-dev": { "phpunit/phpunit": "^9.6", "squizlabs/php_codesniffer": "3.*", "phpspec/prophecy-phpunit": "^2.1", "phpstan/phpstan": "^1.10" }, "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", "test": "./vendor/bin/phpunit", "cs-lint": "vendor/bin/phpcs --standard=./ruleset.xml", "cs-fix": "vendor/bin/phpcbf --standard=./ruleset.xml" } } metadata/ApiCore/Testing/Mocks.php000064400000003704151742737350013072 0ustar00internalAddGeneratedFile( ' – 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 ( HB oneof_fieldB7ÊGoogle\\ApiCore\\TestingâGPBMetadata\\ApiCore\\Testingbproto3' , true); static::$is_initialized = true; } } metadata/Google/ApiCore/Tests/Unit/Example.php000064400000001534151742737350015230 0ustar00internalAddGeneratedFile(hex2bin( "0a85010a0d6578616d706c652e70726f746f1219676f6f676c652e617069" . "636f72652e74657374732e756e6974220b0a094d794d6573736167654244" . "ca0219476f6f676c655c417069436f72655c54657374735c556e6974e202" . "254750424d657461646174615c476f6f676c655c417069436f72655c5465" . "7374735c556e6974620670726f746f33" )); static::$is_initialized = true; } } metadata/README.md000064400000000243151742737350007620 0ustar00# 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.dist000064400000000123151742737350010056 0ustar00parameters: treatPhpDocTypesAsCertain: false level: 5 paths: - src phpunit.xml.dist000064400000001112151742737350007730 0ustar00 src/ApiCore tests/Tests/Unit renovate.json000064400000000151151742737350007275 0ustar00{ "extends": [ "config:base", ":preserveSemverRanges", ":disableDependencyDashboard" ] } src/AgentHeader.php000064400000013532151742737350010235 0ustar00 $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.php000064400000025552151742737350010463 0ustar00 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/ApiKeyHeaderCredentials.php000064400000007331151742737350012537 0ustar00apiKey = $apiKey; $this->quotaProject = $quotaProject; } /** * @return string|null The quota project associated with the credentials. */ public function getQuotaProject(): ?string { return $this->quotaProject; } /** * @param string|null $unusedAudience audiences are not supported for API keys. * * @return callable|null Callable function that returns the API key header. */ public function getAuthorizationHeaderCallback(?string $unusedAudience = null): ?callable { $apiKey = $this->apiKey; // 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 ($apiKey) { return ['x-goog-api-key' => [$apiKey]]; }; } /** * Verify that the expected universe domain matches the universe domain from the credentials. * * @throws ValidationException if the universe domain does not match. */ public function checkUniverseDomain(): void { // This is a no-op, as API keys are not tied to a universe domain. As a result, the // potential for leaking API keys to the GDU is higher, and it's recommended to specify // the "universeDomain" option with the GAPIC client. } } src/ApiStatus.php000064400000015006151742737350010001 0ustar00 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.php000064400000011104151742737350010141 0ustar00pluck($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) ); } /** * A method, similar to PHP's `array_merge_recursive`, with two differences. * * 1. Keys in $array2 take precedence over keys in $array1. * 2. Non-array keys found in both inputs are not transformed into an array * and appended. Rather, the value in $array2 is used. * * @param array $array1 * @param array $array2 * @return array */ private function arrayMergeRecursive(array $array1, array $array2) { foreach ($array2 as $key => $value) { if (array_key_exists($key, $array1) && is_array($array1[$key]) && is_array($value)) { $array1[$key] = ($this->isAssoc($array1[$key]) && $this->isAssoc($value)) ? $this->arrayMergeRecursive($array1[$key], $value) : array_merge($array1[$key], $value); } else { $array1[$key] = $value; } } return $array1; } } src/BidiStream.php000064400000013524151742737350010112 0ustar00call = $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.php000064400000006722151742737350006744 0ustar00method = $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/ClientOptionsTrait.php000064400000030526151742737350011666 0ustar00mergeFromJsonString(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, 'universeDomain' => null, ]; $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->isBackwardsCompatibilityMode()) { $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); } } else { // Ads is using this method in their new surface clients, so we need to call it. // However, this method is not used anywhere else for the new surface clients // @TODO: Remove this in GAX V2 $this->modifyClientOptions($options); } // 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']); } // If the user has not supplied a universe domain, use the environment variable if set. // Otherwise, use the default ("googleapis.com"). $options['universeDomain'] ??= getenv('GOOGLE_CLOUD_UNIVERSE_DOMAIN') ?: GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN; // 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)); } /** * @param mixed $credentials * @param array $credentialsConfig * @return CredentialsWrapper * @throws ValidationException */ private function createCredentialsWrapper($credentials, array $credentialsConfig, string $universeDomain) { if (is_null($credentials)) { // If the user has explicitly set the apiKey option, use Api Key credentials return CredentialsWrapper::build($credentialsConfig, $universeDomain); } if (is_string($credentials) || is_array($credentials)) { return CredentialsWrapper::build(['keyFile' => $credentials] + $credentialsConfig, $universeDomain); } if ($credentials instanceof FetchAuthTokenInterface) { $authHttpHandler = $credentialsConfig['authHttpHandler'] ?? null; return new CredentialsWrapper($credentials, $authHttpHandler, $universeDomain); } if ($credentials instanceof CredentialsWrapper) { return $credentials; } throw new ValidationException(sprintf( 'Unexpected value in $auth option, got: %s', print_r($credentials, true) )); } /** * 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. } /** * @internal */ private function isBackwardsCompatibilityMode(): bool { return false; } } src/ClientStream.php000064400000006642151742737350010464 0ustar00call = $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.php000064400000033142151742737350011663 0ustar00credentialsFetcher = $credentialsFetcher; $this->authHttpHandler = $authHttpHandler; 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']; if (is_null($keyFile)) { $loader = self::buildApplicationDefaultCredentials( $args['scopes'], $args['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, $args['authHttpHandler'], $universeDomain); } /** * @return string|null The quota project associated with the credentials. */ public function getQuotaProject(): ?string { if ($this->credentialsFetcher instanceof GetQuotaProjectInterface) { return $this->credentialsFetcher->getQuotaProject(); } return null; } public function getProjectId(?callable $httpHandler = null): ?string { // Ensure that FetchAuthTokenCache does not throw an exception if ($this->credentialsFetcher instanceof FetchAuthTokenCache && !$this->credentialsFetcher->getFetcher() instanceof ProjectIdProviderInterface) { return null; } if ($this->credentialsFetcher instanceof ProjectIdProviderInterface) { return $this->credentialsFetcher->getProjectId($httpHandler); } 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): ?callable { // 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, $this->authHttpHandler); } // 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. * * @throws ValidationException if the universe domain does not match. */ public function checkUniverseDomain(): void { if (false === $this->hasCheckedUniverse && $this->shouldCheckUniverseDomain()) { $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; } } /** * Skip universe domain check for Metadata server (e.g. GCE) credentials. * * @return bool */ private function shouldCheckUniverseDomain(): bool { $fetcher = $this->credentialsFetcher instanceof FetchAuthTokenCache ? $this->credentialsFetcher->getFetcher() : $this->credentialsFetcher; if ($fetcher instanceof GCECredentials) { return false; } return true; } /** * @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.php000064400000014415151742737350011775 0ustar00 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.php000064400000003507151742737350007437 0ustar00 $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 $backwardsCompatibilityMode; /** * 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; } /** * 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', ]); // "hasEmulator" is not a supported Client Option, but is used // internally to determine if the client is running in emulator mode. // Therefore, we need to remove it from the $options array before // creating the ClientOptions. $hasEmulator = $this->pluck('hasEmulator', $options, false) ?? false; if ($this->isBackwardsCompatibilityMode()) { if (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']); } } else { // cast to ClientOptions for new surfaces only $options = new ClientOptions($options); } $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->isBackwardsCompatibilityMode() ? 'legacy' : 'new', $options['gapicVersion'] ); $this->agentHeader['User-Agent'] = [$userAgentHeader]; self::validateFileExists($options['descriptorsConfigPath']); $descriptors = require($options['descriptorsConfigPath']); $this->descriptors = $descriptors['interfaces'][$this->serviceName]; if (isset($options['apiKey'], $options['credentials'])) { throw new ValidationException( 'API Keys and Credentials are mutually exclusive authentication methods and cannot be used together.' ); } // Set the credentialsWrapper if (isset($options['apiKey'])) { $this->credentialsWrapper = new ApiKeyHeaderCredentials( $options['apiKey'], $options['credentialsConfig']['quotaProject'] ?? null ); } else { $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'], $hasEmulator ); } /** * @param string $apiEndpoint * @param string $transport * @param TransportOptions|array $transportConfig * @param callable $clientCertSource * @param bool $hasEmulator * @return TransportInterface * @throws ValidationException */ private function createTransport( string $apiEndpoint, $transport, $transportConfig, ?callable $clientCertSource = null, bool $hasEmulator = false ) { 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']; $configForSpecifiedTransport['hasEmulator'] = $hasEmulator; 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. * @type array $autoPopulationSettings Settings for * auto population of particular request fields if unset. * } * * @return callable */ private function createCallStack(array $callConstructionOptions) { $fixedHeaders = $this->agentHeader; if ($quotaProject = $this->credentialsWrapper->getQuotaProject()) { $fixedHeaders += [ 'X-Goog-User-Project' => [$quotaProject] ]; } if (isset($this->apiVersion)) { $fixedHeaders += [ 'X-Goog-Api-Version' => [$this->apiVersion] ]; } $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 RequestAutoPopulationMiddleware( $callStack, $callConstructionOptions['autoPopulationSettings'], ); $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]; $autoPopulatedFields = $this->descriptors[$methodName]['autoPopulatedFields'] ?? []; // 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, 'autoPopulationSettings' => $autoPopulatedFields, ]; } /** * @return array */ private function configureCallOptions(array $optionalArgs): array { if ($this->isBackwardsCompatibilityMode()) { return $optionalArgs; } // cast to CallOptions for new surfaces only return (new CallOptions($optionalArgs))->toArray(); } /** * @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 } /** * 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 isBackwardsCompatibilityMode(): bool { return $this->backwardsCompatibilityMode ?? $this->backwardsCompatibilityMode = substr(__CLASS__, -11) === 'GapicClient'; } } src/GrpcSupportTrait.php000064400000004372151742737350011364 0ustar00baseUri, $path )); if ($queryParams) { $uri = $this->buildUriWithQuery( $uri, $queryParams ); } return $uri; } } src/Middleware/CredentialsWrapperMiddleware.php000064400000004672151742737360015745 0ustar00nextHandler = $nextHandler; $this->credentialsWrapper = $credentialsWrapper; } public function __invoke(Call $call, array $options) { $next = $this->nextHandler; return $next( $call, $options + ['credentialsWrapper' => $this->credentialsWrapper] ); } } src/Middleware/FixedHeaderMiddleware.php000064400000005027151742737360014312 0ustar00nextHandler = $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.php000064400000006650151742737360014045 0ustar00handler = $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.php000064400000005413151742737360014264 0ustar00nextHandler = $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.php000064400000004475151742737360014751 0ustar00nextHandler = $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.php000064400000005473151742737360013167 0ustar00nextHandler = $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/RequestAutoPopulationMiddleware.php000064400000007536151742737360016505 0ustar00 */ private $autoPopulationSettings; public function __construct( callable $nextHandler, array $autoPopulationSettings ) { $this->nextHandler = $nextHandler; $this->autoPopulationSettings = $autoPopulationSettings; } /** * @param Call $call * @param array $options * * @return PromiseInterface */ public function __invoke(Call $call, array $options) { $next = $this->nextHandler; if (empty($this->autoPopulationSettings)) { return $next($call, $options); } $request = $call->getMessage(); foreach ($this->autoPopulationSettings as $fieldName => $valueType) { $getFieldName = 'get' . ucwords($fieldName); // We use a getter instead of a hazzer here because there's no need to // differentiate between isset and an empty default value. Even if a // field is explicitly set to an empty string, we want to autopopulate it. if (empty($request->$getFieldName())) { $setFieldName = 'set' . ucwords($fieldName); switch ($valueType) { case Format::UUID4: $request->$setFieldName(Uuid::uuid4()->toString()); break; default: throw new \UnexpectedValueException(sprintf( 'Value type %s::%s not supported for auto population of the field %s', Format::class, Format::name($valueType), $fieldName )); } } } $call = $call->withMessage($request); return $next( $call, $options ); } } src/Middleware/ResponseMetadataMiddleware.php000064400000005250151742737360015377 0ustar00nextHandler = $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.php000064400000015645151742737360013256 0ustar00nextHandler = $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.php000064400000050643151742737360011552 0ustar00 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 $getOperationRequest; private ?string $cancelOperationRequest; private ?string $deleteOperationRequest; 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, 'getOperationRequest' => GetOperationRequest::class, 'cancelOperationRequest' => CancelOperationRequest::class, 'deleteOperationRequest' => DeleteOperationRequest::class, ]; $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']; $this->getOperationRequest = $options['getOperationRequest']; $this->cancelOperationRequest = $options['cancelOperationRequest']; $this->deleteOperationRequest = $options['deleteOperationRequest']; 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'); } $requestClass = $this->isNewSurfaceOperationsClient() ? $this->getOperationRequest : null; $this->lastProtoResponse = $this->operationsCall($this->getOperationMethod, $requestClass); } /** * 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'); } $requestClass = $this->isNewSurfaceOperationsClient() ? $this->cancelOperationRequest : null; $this->operationsCall($this->cancelOperationMethod, $requestClass); } /** * 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'); } $requestClass = $this->isNewSurfaceOperationsClient() ? $this->deleteOperationRequest : null; $this->operationsCall($this->deleteOperationMethod, $requestClass); $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; } /** * Call the operations client to perform an operation. * * @param string $method The method to call on the operations client. * @param string|null $requestClass The request class to use for the call. * Will be null for legacy operations clients. */ private function operationsCall(string $method, ?string $requestClass) { $args = array_merge([$this->getName()], array_values($this->additionalArgs)); if ($requestClass) { if (!method_exists($requestClass, 'build')) { throw new LogicException('Request class must support the static build method'); } $request = call_user_func_array($requestClass . '::build', $args); $args = [$request]; } 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); } private function isNewSurfaceOperationsClient(): bool { return !$this->operationsClient instanceof LegacyOperationsClient && false !== strpos(get_class($this->operationsClient), self::NEW_CLIENT_NAMESPACE); } } src/Options/CallOptions.php000064400000013405151742737360011750 0ustar00> $headers * Key-value array containing headers. * @type int $timeoutMillis * The timeout in milliseconds for the call. * @type array $transportOptions * Transport-specific call options. See {@see CallOptions::setTransportOptions}. * @type RetrySettings|array $retrySettings * A retry settings override for the call. If $retrySettings is an * array, the settings will be merged with the method's default * retry settings. If $retrySettings is a RetrySettings object, * that object will be used instead of the method defaults. * } */ 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->setHeaders($arr['headers'] ?? []); $this->setTimeoutMillis($arr['timeoutMillis'] ?? null); $this->setTransportOptions($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 $transportOptions { * 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 setTransportOptions(array $transportOptions) { $this->transportOptions = $transportOptions; } /** * @deprecated use CallOptions::setTransportOptions */ public function setTransportSpecificOptions(array $transportSpecificOptions) { $this->setTransportOptions($transportSpecificOptions); } /** * @param RetrySettings|array|null $retrySettings */ public function setRetrySettings($retrySettings) { $this->retrySettings = $retrySettings; } } src/Options/ClientOptions.php000064400000027011151742737360012311 0ustar00 '/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; private ?string $apiKey; /** * @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. * @type string $apiKey * The API key to be used for the client. * } */ 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'] ?? null); $this->setCredentialsConfig($arr['credentialsConfig'] ?? []); $this->setTransport($arr['transport'] ?? null); $this->setTransportConfig(new TransportOptions($arr['transportConfig'] ?? [])); $this->setVersionFile($arr['versionFile'] ?? null); $this->setDescriptorsConfigPath($arr['descriptorsConfigPath'] ?? null); $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); $this->setApiKey($arr['apiKey'] ?? 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)) { $clientCertSource = Closure::fromCallable($clientCertSource); } $this->clientCertSource = $clientCertSource; } /** * @param string $universeDomain */ public function setUniverseDomain(?string $universeDomain) { $this->universeDomain = $universeDomain; } /** * @param string $apiKey */ public function setApiKey(?string $apiKey) { $this->apiKey = $apiKey; } } src/Options/OptionsTrait.php000064400000005720151742737360012161 0ustar00$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.php000064400000006122151742737360013067 0ustar00fromArray($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-fallback'] ?? [])); $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.php000064400000006451151742737360020700 0ustar00fromArray($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)) { $httpHandler = Closure::fromCallable($httpHandler); } $this->httpHandler = $httpHandler; } /** * @param ?callable $clientCertSource */ public function setClientCertSource(?callable $clientCertSource) { if (!is_null($clientCertSource)) { $clientCertSource = Closure::fromCallable($clientCertSource); } $this->clientCertSource = $clientCertSource; } } src/Options/TransportOptions/GrpcTransportOptions.php000064400000011041151742737360017247 0ustar00fromArray($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)) { $clientCertSource = Closure::fromCallable($clientCertSource); } $this->clientCertSource = $clientCertSource; } } src/Options/TransportOptions/RestTransportOptions.php000064400000007356151742737360017307 0ustar00fromArray($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)) { $httpHandler = Closure::fromCallable($httpHandler); } $this->httpHandler = $httpHandler; } /** * @param ?callable $clientCertSource */ public function setClientCertSource(?callable $clientCertSource) { if (!is_null($clientCertSource)) { $clientCertSource = Closure::fromCallable($clientCertSource); } $this->clientCertSource = $clientCertSource; } /** * @param ?string $restClientConfigPath */ public function setRestClientConfigPath(?string $restClientConfigPath) { $this->restClientConfigPath = $restClientConfigPath; } } src/Page.php000064400000022346151742737360006746 0ustar00call = $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.php000064400000014264151742737360012657 0ustar00descriptor = $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.php000064400000015434151742737360011465 0ustar00getList(...); * 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.php000064400000010074151742737360010455 0ustar00resourceTemplate = 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.php000064400000007230151742737360010475 0ustar00 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.php000064400000023426151742737360011031 0ustar00baseUri = $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 = null) { if ($result && $getter) { 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 */ protected 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.php000064400000005114151742737360014030 0ustar00 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.php000064400000007665151742737360012034 0ustar00 $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.php000064400000011737151742737360016341 0ustar00"). * * 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.php000064400000021332151742737360012603 0ustar00= 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.php000064400000037273151742737360016341 0ustar00"). * * 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.php000064400000007170151742737360016457 0ustar00"). (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.php000064400000013431151742737360012752 0ustar00segmentType = $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.php000064400000053067151742737360010724 0ustar00 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.php000064400000045010151742737360010174 0ustar00 \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; // Array of key-value pairs which specify a custom encoding function. // The key is the proto class and the value is the function // which will be used to convert the proto instead of the // encodeMessage method from the Serializer class. private $customEncoders; 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 = [], $customEncoders = [], ) { $this->fieldTransformers = $fieldTransformers; $this->messageTypeTransformers = $messageTypeTransformers; $this->decodeFieldTransformers = $decodeFieldTransformers; $this->decodeMessageTypeTransformers = $decodeMessageTypeTransformers; $this->customEncoders = $customEncoders; } /** * Encode protobuf message as a PHP array * * @param mixed $message * @return array * @throws ValidationException */ public function encodeMessage($message) { $cls = get_class($message); // If we have supplied a customEncoder for this class type, // then we use that instead of the general encodeMessage definition. if (array_key_exists($cls, $this->customEncoders)) { $func = $this->customEncoders[$cls]; return call_user_func($func, $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 = []; // Call the getDescriptorMaps outside of the loop to save processing. // Use the same set of fields to loop over, instead of using field count. list($fields, $fieldsToOneof) = $this->getDescriptorMaps($messageType); foreach ($fields as $field) { $key = $field->getName(); $getter = $this->getGetter($key); $v = $message->$getter(); if (is_null($v)) { continue; } // Check and skip unset fields inside oneofs 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) { if (!isset(self::$getterMap[$name])) { self::$getterMap[$name] = 'get' . ucfirst(self::toCamelCase($name)); } return self::$getterMap[$name]; } /** * @param string $name * @return string Setter function */ public static function getSetter(string $name) { if (!isset(self::$setterMap[$name])) { self::$setterMap[$name] = 'set' . ucfirst(self::toCamelCase($name)); } return self::$setterMap[$name]; } /** * Convert string from camelCase to snake_case * * @param string $key * @return string */ public static function toSnakeCase(string $key) { if (!isset(self::$snakeCaseMap[$key])) { self::$snakeCaseMap[$key] = strtolower( preg_replace(['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'], '$1_$2', $key) ); } return self::$snakeCaseMap[$key]; } /** * Convert string from snake_case to camelCase * * @param string $key * @return string */ public static function toCamelCase(string $key) { if (!isset(self::$camelCaseMap[$key])) { self::$camelCaseMap[$key] = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))); } return self::$camelCaseMap[$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.php000064400000006673151742737360010521 0ustar00call = $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.php000064400000005702151742737360013624 0ustar00 $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.php000064400000004753151742737360012006 0ustar00assertSame($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.php000064400000001606151742737360015076 0ustar00exporter = new MessageAwareExporter(); } } src/Testing/MessageAwareExporter.php000064400000002432151742737360013576 0ustar00responses = $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.php000064400000010414151742737360014176 0ustar00mockUnaryCall = 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.php000064400000010042151742737360013117 0ustar00mockCall = $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.php000064400000003601151742737360011742 0ustar00google.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.php000064400000041327151742737360012567 0ustar00google.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.php000064400000010242151742737360012107 0ustar00google.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.php000064400000007017151742737360014233 0ustar00responses = $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.php000064400000003705151742737360011602 0ustar00code = $code; $this->details = $details; $this->metadata = $metadata; } } src/Testing/MockStubTrait.php000064400000025541151742737360012242 0ustar00deserialize = $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.php000064400000010110151742737360012277 0ustar00agentHeaderDescriptor = $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.php000064400000006063151742737360012211 0ustar00response = $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.php000064400000004211151742737360014676 0ustar00exporter = 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.php000064400000005051151742737360012600 0ustar00actualCall = [ '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.php000064400000004763151742737360013333 0ustar00$deserializeFunc($message); } elseif (is_string($message)) { $obj->mergeFromString($message); } return $obj; } // Protobuf-PHP implementation return call_user_func($deserialize, $message); } } src/Testing/mocks.proto000064400000002403151742737360011167 0ustar00syntax = "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.php000064400000005151151742737360013652 0ustar00innerCall = $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.php000064400000004450151742737360016714 0ustar00innerCall->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.php000064400000003764151742737360014701 0ustar00innerCall->wait(); } } src/Transport/Grpc/ServerStreamingCallWrapper.php000064400000006527151742737360016241 0ustar00stream = $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.php000064400000004416151742737360016115 0ustar00baseUri = $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.php000064400000024770151742737360012701 0ustar00 [], '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) { $this->verifyUniverseDomain($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) { $this->verifyUniverseDomain($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) { $this->verifyUniverseDomain($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) { $this->verifyUniverseDomain($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 verifyUniverseDomain(array $options) { if (isset($options['credentialsWrapper'])) { $options['credentialsWrapper']->checkUniverseDomain(); } } 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.php000064400000012520151742737360014736 0ustar00throwUnsupportedException(); } /** * {@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']); // Mitigate scenario where InsecureCredentialsWrapper returns null. $authHeaders = empty($callback) ? [] : $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.php000064400000022173151742737360014354 0ustar00 $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.php000064400000013371151742737360015553 0ustar00 */ 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.php000064400000023167151742737360012722 0ustar00requestBuilder = $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. * @type bool $hasEmulator True if the emulator is enabled. * } * @return RestTransport * @throws ValidationException */ public static function build(string $apiEndpoint, string $restConfigPath, array $config = []) { $config += [ 'httpHandler' => null, 'clientCertSource' => null, 'hasEmulator' => false, ]; list($baseUri, $port) = self::normalizeServiceAddress($apiEndpoint); $requestBuilder = $config['hasEmulator'] ? new InsecureRequestBuilder("$baseUri:$port", $restConfigPath) : 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 (\Throwable $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.php000064400000005566151742737360013710 0ustar00 $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.php000064400000004516151742737360007634 0ustar00 &$v) { if (is_bool($v)) { $v = $v ? 'true' : 'false'; } } return Utils::uriFor($uri) ->withQuery( Query::build($query) ); } } src/ValidationException.php000064400000003321151742737360012033 0ustar00