├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src └── ElasticsearchPhpHandler.php └── tests └── ElasticsearchPhpHandlerTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | vendor/ 3 | composer.lock 4 | 5 | /phpunit.xml 6 | /.phpunit.result.cache 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | - 7.0 6 | - 7.1 7 | - 7.2 8 | - 7.3 9 | - 8.1 10 | 11 | sudo: false 12 | 13 | env: 14 | - COMPOSER_OPTS="" 15 | - COMPOSER_OPTS="--prefer-lowest" 16 | 17 | matrix: 18 | allow_failures: 19 | - php: 7.3 20 | env: COMPOSER_OPTS="--prefer-lowest" # Elasticsearch-PHP 2.1 (a very old version) is not compatible with PHP 7.3 21 | - php: 8.1 22 | env: COMPOSER_OPTS="--prefer-lowest" # Elasticsearch-PHP 2.1 (a very old version) is not compatible with PHP 8.1 23 | fast_finish: true 24 | 25 | install: 26 | - travis_retry composer update $COMPOSER_OPTS --no-interaction 27 | - travis_retry composer require elasticsearch/elasticsearch:">=2.1 <8.0" $COMPOSER_OPTS --no-interaction 28 | 29 | script: vendor/bin/phpunit 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Apache License 2 | Version 2.0, January 2004 3 | 4 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 5 | 6 | ## 1. Definitions. 7 | 8 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 9 | through 9 of this document. 10 | 11 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the 12 | License. 13 | 14 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled 15 | by, or are under common control with that entity. For the purposes of this definition, "control" means 16 | (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract 17 | or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial 18 | ownership of such entity. 19 | 20 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 21 | 22 | "Source" form shall mean the preferred form for making modifications, including but not limited to software 23 | source code, documentation source, and configuration files. 24 | 25 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, 26 | including but not limited to compiled object code, generated documentation, and conversions to other media 27 | types. 28 | 29 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, 30 | as indicated by a copyright notice that is included in or attached to the work (an example is provided in the 31 | Appendix below). 32 | 33 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) 34 | the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, 35 | as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not 36 | include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work 37 | and Derivative Works thereof. 38 | 39 | "Contribution" shall mean any work of authorship, including the original version of the Work and any 40 | modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to 41 | Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to 42 | submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of 43 | electronic, verbal, or written communication sent to the Licensor or its representatives, including but not 44 | limited to communication on electronic mailing lists, source code control systems, and issue tracking systems 45 | that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but 46 | excluding communication that is conspicuously marked or otherwise designated in writing by the copyright 47 | owner as "Not a Contribution." 48 | 49 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been 50 | received by Licensor and subsequently incorporated within the Work. 51 | 52 | ## 2. Grant of Copyright License. 53 | 54 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, 55 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare 56 | Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such 57 | Derivative Works in Source or Object form. 58 | 59 | ## 3. Grant of Patent License. 60 | 61 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, 62 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent 63 | license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such 64 | license applies only to those patent claims licensable by such Contributor that are necessarily infringed by 65 | their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such 66 | Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim 67 | or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work 68 | constitutes direct or contributory patent infringement, then any patent licenses granted to You under this 69 | License for that Work shall terminate as of the date such litigation is filed. 70 | 71 | ## 4. Redistribution. 72 | 73 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without 74 | modifications, and in Source or Object form, provided that You meet the following conditions: 75 | 76 | 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and 77 | 78 | 2. You must cause any modified files to carry prominent notices stating that You changed the files; and 79 | 80 | 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, 81 | trademark, and attribution notices from the Source form of the Work, excluding those notices that do 82 | not pertain to any part of the Derivative Works; and 83 | 84 | 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that 85 | You distribute must include a readable copy of the attribution notices contained within such NOTICE 86 | file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one 87 | of the following places: within a NOTICE text file distributed as part of the Derivative Works; within 88 | the Source form or documentation, if provided along with the Derivative Works; or, within a display 89 | generated by the Derivative Works, if and wherever such third-party notices normally appear. The 90 | contents of the NOTICE file are for informational purposes only and do not modify the License. You may 91 | add Your own attribution notices within Derivative Works that You distribute, alongside or as an 92 | addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be 93 | construed as modifying the License. 94 | 95 | You may add Your own copyright statement to Your modifications and may provide additional or different license 96 | terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative 97 | Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the 98 | conditions stated in this License. 99 | 100 | ## 5. Submission of Contributions. 101 | 102 | Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by 103 | You to the Licensor shall be under the terms and conditions of this License, without any additional terms or 104 | conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate 105 | license agreement you may have executed with Licensor regarding such Contributions. 106 | 107 | ## 6. Trademarks. 108 | 109 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of 110 | the Licensor, except as required for reasonable and customary use in describing the origin of the Work and 111 | reproducing the content of the NOTICE file. 112 | 113 | ## 7. Disclaimer of Warranty. 114 | 115 | Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor 116 | provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 117 | or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, 118 | MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the 119 | appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of 120 | permissions under this License. 121 | 122 | ## 8. Limitation of Liability. 123 | 124 | In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless 125 | required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any 126 | Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential 127 | damages of any character arising as a result of this License or out of the use or inability to use the Work 128 | (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or 129 | any and all other commercial damages or losses), even if such Contributor has been advised of the possibility 130 | of such damages. 131 | 132 | ## 9. Accepting Warranty or Additional Liability. 133 | 134 | While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, 135 | acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this 136 | License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole 137 | responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold 138 | each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason 139 | of your accepting any such warranty or additional liability. 140 | 141 | END OF TERMS AND CONDITIONS 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Auth Elasticsearch-PHP 2 | 3 | [![Apache 2 License](https://img.shields.io/packagist/l/jsq/amazon-es-php.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0.html) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/jsq/amazon-es-php.svg?style=flat)](https://packagist.org/packages/jsq/amazon-es-php) 5 | [![Author](http://img.shields.io/badge/author-@jreskew-blue.svg?style=flat-square)](https://twitter.com/jreskew) 6 | [![Build Status](https://travis-ci.org/jeskew/amazon-es-php.svg?branch=master)](https://travis-ci.org/jeskew/amazon-es-php) 7 | 8 | **NB**: SignatureV4 support is built into the Opensearch-PHP client 9 | (`opensearch-project/opensearch-php`) as of [version 2.0.1](https://github.com/opensearch-project/opensearch-php/releases/tag/2.0.1). 10 | If you are using the Opensearch-PHP client, **you do not need to use this library**. 11 | 12 | This package provides a signing handler for use with the official 13 | Elasticsearch-PHP client (`elasticsearch/elasticsearch`). By default, the handler 14 | will load AWS credentials from the environment and send requestsusing a RingPHP cURL 15 | handler. 16 | 17 | The search library package must be installed separately. The documentation below will 18 | use Elasticsearch-PHP in the examples, but both libraries should be pretty identical. 19 | 20 | ## Basic Usage 21 | 22 | Instances of `Aws\ElasticsearchService\ElasticsearchPhpHandler` are callables 23 | that fulfill Elasticsearch-PHP's handler contract. They can be passed to 24 | `Elasticsearch\ClientBuilder`'s `setHandler` method: 25 | ```php 26 | use Aws\ElasticsearchService\ElasticsearchPhpHandler; 27 | use Elasticsearch\ClientBuilder; 28 | 29 | // Create a handler (with the region of your Amazon Elasticsearch Service domain) 30 | $handler = new ElasticsearchPhpHandler('us-west-2'); 31 | 32 | // Use this handler to create an Elasticsearch-PHP client 33 | $client = ClientBuilder::create() 34 | ->setHandler($handler) 35 | ->setHosts(['https://search-foo-3gn4utxfus5cqpn89go4z5lbsm.us-west-2.es.amazonaws.com:443']) 36 | ->build(); 37 | 38 | // Use the client as you normally would 39 | $client->index([ 40 | 'index' => $index, 41 | 'type' => $type, 42 | 'id' => $id, 43 | 'body' => [$key => $value] 44 | ]); 45 | ``` 46 | 47 | ## Using custom credentials 48 | 49 | By default, the handler will attempt to source credentials from the environment 50 | as described [in the AWS SDK for PHP documentation](http://docs.aws.amazon.com/aws-sdk-php/v3/guide/guide/credentials.html). 51 | To use custom credentials, pass in a [credential provider](http://docs.aws.amazon.com/aws-sdk-php/v3/guide/guide/credentials.html#credential-provider): 52 | ```php 53 | use Aws\Credentials\CredentialProvider; 54 | use Aws\Credentials\Credentials; 55 | use Aws\ElasticsearchService\ElasticsearchPhpHandler; 56 | 57 | $provider = CredentialProvider::fromCredentials( 58 | new Credentials('foo', 'bar', 'baz') 59 | ); 60 | 61 | $handler = new ElasticsearchPhpHandler('us-west-2', $provider); 62 | ``` 63 | 64 | ## Using a custom HTTP handler 65 | 66 | By default, the handler will use `Elasticsearch\ClientBuilder::defaultHandler()` 67 | to dispatch HTTP requests, but this is customizable via an optional constructor 68 | parameter. For example, this repository's tests use a custom handler to mock 69 | network traffic: 70 | ```php 71 | class ElasticsearchPhpHandlerTest extends \PHPUnit_Framework_TestCase 72 | { 73 | public function testSignsRequestsPassedToHandler() 74 | { 75 | $toWrap = function (array $ringRequest) { 76 | $this->assertArrayHasKey('X-Amz-Date', $ringRequest['headers']); 77 | $this->assertArrayHasKey('Authorization', $ringRequest['headers']); 78 | $this->assertStringStartsWith( 79 | 'AWS4-HMAC-SHA256 Credential=', 80 | $ringRequest['headers']['Authorization'][0] 81 | ); 82 | 83 | return $this->getGenericResponse(); 84 | }; 85 | $handler = new ElasticsearchPhpHandler('us-west-2', null, $toWrap); 86 | 87 | $client = \Elasticsearch\ClientBuilder::create() 88 | ->setHandler($handler) 89 | ->build(); 90 | 91 | $client->get([ 92 | 'index' => 'index', 93 | 'type' => 'type', 94 | 'id' => 'id', 95 | ]); 96 | } 97 | ... 98 | } 99 | ``` 100 | 101 | ## Installation 102 | 103 | ### Composer 104 | 105 | ``` 106 | composer require jsq/amazon-es-php elasticsearch/elasticsearch:"<8.0" 107 | ``` 108 | or 109 | ``` 110 | composer require jsq/amazon-es-php opensearch-project/opensearch-php 111 | ``` 112 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsq/amazon-es-php", 3 | "description": "Support for using IAM authentication with the official Elasticsearch PHP client", 4 | "keywords": ["elasticsearch", "opensearch", "aws", "search", "client", "iam"], 5 | "minimum-stability": "stable", 6 | "license": "Apache-2.0", 7 | "authors": [ 8 | { 9 | "name": "Jonathan Eskew", 10 | "email": "jonathan@jeskew.net" 11 | } 12 | ], 13 | "require": { 14 | "aws/aws-sdk-php": "^3.0" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^4.8.35|^5.4.0|^6.0.0|^7.0.0|^8.0.0", 18 | "ezimuel/ringphp": "^1.1.2" 19 | }, 20 | "suggest": { 21 | "elasticsearch/elasticsearch": "Require `elasticsearch/elasticsearch` when using Elasticsearch on AWS", 22 | "opensearch-project/opensearch-php": "Require `opensearch-project/opensearch-php` when using Opensearch on AWS" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Aws\\ElasticsearchService\\": "src" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Aws\\ElasticsearchService\\": "tests" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | tests/ 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/ElasticsearchPhpHandler.php: -------------------------------------------------------------------------------- 1 | signer = new SignatureV4('es', $region); 35 | $this->wrappedHandler = $wrappedHandler 36 | ?: static::defaultHandler(); 37 | $this->credentialProvider = $credentialProvider 38 | ?: CredentialProvider::defaultProvider(); 39 | } 40 | 41 | public function __invoke(array $request) 42 | { 43 | $creds = call_user_func($this->credentialProvider)->wait(); 44 | $psr7Request = $this->createPsr7Request($request); 45 | $signedRequest = $this->signer 46 | ->signRequest($psr7Request, $creds); 47 | 48 | return call_user_func($this->wrappedHandler, $this->createRingRequest($signedRequest)); 49 | } 50 | 51 | private function createPsr7Request(array $ringPhpRequest) 52 | { 53 | // fix for uppercase 'Host' array key in elasticsearch-php 5.3.1 and backward compatible 54 | // https://github.com/aws/aws-sdk-php/issues/1225 55 | $hostKey = isset($ringPhpRequest['headers']['Host'])? 'Host' : 'host'; 56 | 57 | // Amazon ES/OS listens on standard ports (443 for HTTPS, 80 for HTTP). 58 | // Consequently, the port should be stripped from the host header. 59 | $parsedUrl = parse_url($ringPhpRequest['headers'][$hostKey][0]); 60 | if (isset($parsedUrl['host'])) { 61 | $ringPhpRequest['headers'][$hostKey][0] = $parsedUrl['host']; 62 | } 63 | 64 | // Create a PSR-7 URI from the array passed to the handler 65 | $uri = (new Uri($ringPhpRequest['uri'])) 66 | ->withScheme($ringPhpRequest['scheme']) 67 | ->withHost($ringPhpRequest['headers'][$hostKey][0]); 68 | if (isset($ringPhpRequest['query_string'])) { 69 | $uri = $uri->withQuery($ringPhpRequest['query_string']); 70 | } 71 | 72 | // Create a PSR-7 request from the array passed to the handler 73 | return new Request( 74 | $ringPhpRequest['http_method'], 75 | $uri, 76 | $ringPhpRequest['headers'], 77 | $ringPhpRequest['body'] 78 | ); 79 | } 80 | 81 | private function createRingRequest(RequestInterface $request) 82 | { 83 | $uri = $request->getUri(); 84 | $body = (string) $request->getBody(); 85 | 86 | // RingPHP currently expects empty message bodies to be null: 87 | // https://github.com/guzzle/RingPHP/blob/4c8fe4c48a0fb7cc5e41ef529e43fecd6da4d539/src/Client/CurlFactory.php#L202 88 | if (empty($body)) { 89 | $body = null; 90 | } 91 | 92 | $ringRequest = [ 93 | 'http_method' => $request->getMethod(), 94 | 'scheme' => $uri->getScheme(), 95 | 'uri' => $uri->getPath(), 96 | 'body' => $body, 97 | 'headers' => $request->getHeaders(), 98 | ]; 99 | if ($uri->getQuery()) { 100 | $ringRequest['query_string'] = $uri->getQuery(); 101 | } 102 | 103 | return $ringRequest; 104 | } 105 | 106 | /** 107 | * Get the default handler closure from either the opensearch-php or the elasticsearch-php library. 108 | * 109 | * @throws RuntimeException 110 | */ 111 | public static function defaultHandler(array $multiParams = [], array $singleParams = []) 112 | { 113 | $builderClass = static::clientBuilderClass(); 114 | 115 | return $builderClass::defaultHandler($multiParams, $singleParams); 116 | } 117 | 118 | /** 119 | * Determine the client builder class on the installed search library 120 | */ 121 | protected static function clientBuilderClass() 122 | { 123 | // Opensearch-PHP 124 | if (class_exists('\OpenSearch\ClientBuilder')) { 125 | return '\OpenSearch\ClientBuilder'; 126 | } 127 | 128 | // Elasticsearch-PHP < 8.0 129 | if (class_exists('\Elasticsearch\ClientBuilder')) { 130 | return '\Elasticsearch\ClientBuilder'; 131 | } 132 | 133 | // Elasticsearch-PHP >= 8.0 134 | if (class_exists('\Elastic\Elasticsearch\ClientBuilder')) { 135 | throw new RuntimeException('Elasticsearch-PHP is incompatible from version 8.'); 136 | } 137 | 138 | throw new RuntimeException('ElasticsearchPhpHandler requires either Elasticsearch-PHP or Opensearch-PHP.'); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tests/ElasticsearchPhpHandlerTest.php: -------------------------------------------------------------------------------- 1 | assertArrayHasKey('X-Amz-Date', $ringRequest['headers']); 17 | $this->assertArrayHasKey('Authorization', $ringRequest['headers']); 18 | $this->assertRegExp( 19 | "~^AWS4-HMAC-SHA256 Credential=$key/\\d{8}/us-west-2/es/aws4_request~", 20 | $ringRequest['headers']['Authorization'][0] 21 | ); 22 | 23 | return $this->getGenericResponse(); 24 | }; 25 | putenv(CredentialProvider::ENV_KEY . "=$key"); 26 | putenv(CredentialProvider::ENV_SECRET . '=bar'); 27 | $client = $this->getElasticsearchClient( 28 | new ElasticsearchPhpHandler('us-west-2', null, $toWrap) 29 | ); 30 | 31 | $client->get([ 32 | 'index' => 'index', 33 | 'type' => 'type', 34 | 'id' => 'id', 35 | ]); 36 | } 37 | 38 | public function testSignsWithProvidedCredentials() 39 | { 40 | $provider = CredentialProvider::fromCredentials( 41 | new Credentials('foo', 'bar', 'baz') 42 | ); 43 | $toWrap = function (array $ringRequest) { 44 | $this->assertArrayHasKey('X-Amz-Security-Token', $ringRequest['headers']); 45 | $this->assertSame('baz', $ringRequest['headers']['X-Amz-Security-Token'][0]); 46 | $this->assertRegExp( 47 | '~^AWS4-HMAC-SHA256 Credential=foo/\d{8}/us-west-2/es/aws4_request~', 48 | $ringRequest['headers']['Authorization'][0] 49 | ); 50 | 51 | return $this->getGenericResponse(); 52 | }; 53 | 54 | $client = $this->getElasticsearchClient( 55 | new ElasticsearchPhpHandler('us-west-2', $provider, $toWrap) 56 | ); 57 | 58 | $client->get([ 59 | 'index' => 'index', 60 | 'type' => 'type', 61 | 'id' => 'id', 62 | ]); 63 | } 64 | 65 | public function testEmptyRequestBodiesShouldBeNull() 66 | { 67 | $toWrap = function (array $ringRequest) { 68 | $this->assertNull($ringRequest['body']); 69 | 70 | return $this->getGenericResponse(); 71 | }; 72 | 73 | $client = $this->getElasticsearchClient( 74 | new ElasticsearchPhpHandler('us-west-2', null, $toWrap) 75 | ); 76 | 77 | $client->indices()->exists(['index' => 'index']); 78 | } 79 | 80 | public function testNonEmptyRequestBodiesShouldNotBeNull() 81 | { 82 | $toWrap = function (array $ringRequest) { 83 | $this->assertNotNull($ringRequest['body']); 84 | 85 | return $this->getGenericResponse(); 86 | }; 87 | 88 | $client = $this->getElasticsearchClient( 89 | new ElasticsearchPhpHandler('us-west-2', null, $toWrap) 90 | ); 91 | 92 | $client->search([ 93 | 'index' => 'index', 94 | 'body' => [ 95 | 'query' => [ 'match_all' => (object)[] ], 96 | ], 97 | ]); 98 | } 99 | 100 | private function getElasticsearchClient(ElasticsearchPhpHandler $handler) 101 | { 102 | $builder = ClientBuilder::create() 103 | ->setHandler($handler); 104 | 105 | if (method_exists($builder, 'allowBadJSONSerialization')) { 106 | $builder = $builder->allowBadJSONSerialization(); 107 | } 108 | 109 | return $builder->build(); 110 | } 111 | 112 | private function getGenericResponse() 113 | { 114 | return new CompletedFutureArray([ 115 | 'status' => 200, 116 | 'body' => fopen('php://memory', 'r'), 117 | 'transfer_stats' => ['total_time' => 0], 118 | 'effective_url' => 'https://www.example.com', 119 | ]); 120 | } 121 | } 122 | --------------------------------------------------------------------------------