├── composer.json ├── LICENSE.md ├── .github └── workflows │ └── ci.yml ├── CHANGELOG.md ├── README.md └── src └── GuzzleHandler.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lukewaite/ringphp-guzzle-handler", 3 | "license": "MIT", 4 | "authors": [ 5 | { 6 | "name": "Luke Waite", 7 | "email": "lwaite@gmail.com" 8 | } 9 | ], 10 | "require": { 11 | "php": "^8.2|^8.3|^8.4", 12 | "guzzlehttp/ringphp": "^1.1", 13 | "guzzlehttp/guzzle": "^7.0", 14 | "guzzlehttp/psr7": "^2.0" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^9.0|^10.0|^11.0", 18 | "php-coveralls/php-coveralls": "^2.1" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "LukeWaite\\RingPhpGuzzleHandler\\": "src" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "LukeWaite\\RingPhpGuzzleHandler\\Tests\\": "tests" 28 | } 29 | }, 30 | "scripts": { 31 | "test": "phpunit" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Luke Waite 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | php: ['8.2', '8.3', '8.4'] 17 | dependencies: ['lowest', 'highest'] 18 | 19 | name: PHP ${{ matrix.php }} - ${{ matrix.dependencies }} 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.php }} 29 | extensions: mbstring 30 | 31 | - name: Cache Composer packages 32 | uses: actions/cache@v3 33 | with: 34 | path: vendor 35 | key: ${{ runner.os }}-php-${{ matrix.php }}-${{ matrix.dependencies }}-${{ hashFiles('**/composer.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-php-${{ matrix.php }}-${{ matrix.dependencies }}- 38 | 39 | - name: Install dependencies (highest) 40 | if: matrix.dependencies == 'highest' 41 | run: composer update --no-interaction --no-progress 42 | 43 | - name: Install dependencies (lowest) 44 | if: matrix.dependencies == 'lowest' 45 | run: composer update --prefer-lowest --no-interaction --no-progress 46 | 47 | - name: Run test suite 48 | run: vendor/bin/phpunit 49 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Notes for v1.x 2 | 3 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 4 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 5 | 6 | ## [Unreleased] 7 | 8 | ## [v2.2.0] - 2021-04-23 9 | 10 | * Add PHP 8 support ([#8](https://github.com/lukewaite/ringphp-guzzle-handler/pull/8/files)) 11 | 12 | ## [v2.1.0] - 2019-01-21 13 | 14 | * Add support for Async calls ([#2](https://github.com/lukewaite/ringphp-guzzle-handler/pull/2)) 15 | * Provide a default client ([#3](https://github.com/lukewaite/ringphp-guzzle-handler/pull/3)) 16 | * Use Guzzle to convert stream into stream ([#4](https://github.com/lukewaite/ringphp-guzzle-handler/pull/4)) 17 | 18 | ## [v2.0.0] - 2018-08-23 19 | 20 | * Breaking change: minimum PHP version now 7.0 21 | * add support for handling authentication / credentials ([#1](https://github.com/lukewaite/ringphp-guzzle-handler/pull/1)) 22 | 23 | ## [v1.0.1] - 2017-07-08 24 | 25 | * Breaking change: must now pass in a GuzzleHttp `Client`, not just config for one 26 | * Test cases added with travis 27 | 28 | ## v1.0.0 - 2017-07-08 [YANKED] 29 | 30 | * Initial Release 31 | 32 | [Unreleased]: https://github.com/lukewaite/ringphp-guzzle-handler/compare/v2.2.0...HEAD 33 | [v2.2.0]: https://github.com/lukewaite/ringphp-guzzle-handler/compare/v2.1.0...v2.2.0 34 | [v2.1.0]: https://github.com/lukewaite/ringphp-guzzle-handler/compare/v2.0.0...v2.1.0 35 | [v2.0.0]: https://github.com/lukewaite/ringphp-guzzle-handler/compare/v1.0.1...v2.0.0 36 | [v1.0.1]: https://github.com/lukewaite/ringphp-guzzle-handler/compare/v1.0.0...v1.0.1 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RingPHP Guzzle Handler 2 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/lukewaite/ringphp-guzzle-handler.svg?style=flat-square)](https://packagist.org/packages/lukewaite/ringphp-guzzle-handler) 3 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/lukewaite/ringphp-guzzle-handler.svg?style=flat-square)](https://packagist.org/packages/lukewaite/ringphp-guzzle-handler) 5 | 6 | ## Usage 7 | 8 | ### Installing 9 | 10 | This package can be installed with composer. 11 | 12 | $ composer require lukewaite/ringphp-guzzle-handler 13 | 14 | ### Elasticsearch 15 | 16 | ```php 17 | $guzzleHandler = new LukeWaite\RingPhpGuzzleHandler\GuzzleHandler(); 18 | 19 | $client = Elasticsearch\ClientBuilder::create() 20 | ->setHandler($guzzleHandler) 21 | ->build(); 22 | ``` 23 | 24 | Optionally, you may create a Guzzle client manually, and pass it through to the constructor: 25 | ```php 26 | $guzzle = new GuzzleHttp\Client(); 27 | $guzzleHandler = new LukeWaite\RingPhpGuzzleHandler\GuzzleHandler($guzzle); 28 | 29 | $client = Elasticsearch\ClientBuilder::create() 30 | ->setHandler($guzzleHandler) 31 | ->build(); 32 | ``` 33 | 34 | ## What have you done? 35 | I've built a [RingPHP][ringphp] Handler that uses Guzzle as the transport. 36 | 37 | ### You've done wot mate? 38 | Yes - I built a handler for RingPHP (an older GuzzleHttp project) that 39 | uses Guzzle 6 as the transport. 40 | 41 | ### Reasoning 42 | The ElasticSearch PHP SDK uses the RingPHP client library under the 43 | covers. You can provide a `Handler` when creating the client, but it has 44 | to be a RingPHP handler. 45 | 46 | NewRelic supports tracking external requests for Guzzle, but not for 47 | RingPHP. Using this handler means we can get more accurate instrumentation 48 | on our transactions. 49 | 50 | #### Example NewRelic APM Chart, Before and After Deployment 51 | ![newrelic before and after](https://lukewaite.ca/images/2017-07-15-newrelic-elasticsearch/newrelic-instrumentation.png) 52 | 53 | ## How true to RingPHP is this? 54 | The spec for [implementing handlers][implementing-handlers] has been 55 | followed, but in some cases I've had to go out of my way to tune this 56 | for the ElasticSearch PHP SDK. 57 | 58 | #### $response `body` 59 | You're supposed to be able to return a [lot of different types][response] 60 | here, but ElasticSearch expects it to be only a stream, so that's what we 61 | return. 62 | 63 | #### $response `transfer_stats` 64 | Transfer stats is supposed to be an arbitrary array of stats provided by 65 | the handler, but it turns out ElasticSearch expects some pretty specific 66 | stuff from the default CURL handler to be in there. 67 | 68 | [implementing-handlers]: http://ringphp.readthedocs.io/en/latest/client_handlers.html#implementing-handlers 69 | [response]: http://ringphp.readthedocs.io/en/latest/spec.html#responses 70 | [ringphp]: https://github.com/guzzle/RingPHP 71 | -------------------------------------------------------------------------------- /src/GuzzleHandler.php: -------------------------------------------------------------------------------- 1 | client = $client ?? new Client(); 24 | } 25 | 26 | public function __invoke(array $request): FutureArrayInterface 27 | { 28 | $defered = new Deferred(); 29 | $promise = $this->invokeGuzzle($request)->then([$defered, 'resolve'], [$defered, 'resolve']); 30 | 31 | return new FutureArray( 32 | $defered->promise(), 33 | [$promise, 'wait'], 34 | [$promise, 'cancel'] 35 | ); 36 | } 37 | 38 | private function invokeGuzzle(array $request): PromiseInterface 39 | { 40 | $url = Core::url($request); 41 | Core::doSleep($request); 42 | 43 | $stats = null; 44 | $options = [ 45 | RequestOptions::BODY => Core::body($request), 46 | RequestOptions::HEADERS => $request['headers'], 47 | RequestOptions::HTTP_ERRORS => false, 48 | ]; 49 | 50 | if (isset($request['client']['curl'][CURLOPT_USERPWD])) { 51 | $options['auth'] = explode(':', $request['client']['curl'][CURLOPT_USERPWD]); 52 | } 53 | 54 | $start = \microtime(true); 55 | return $this->client->requestAsync($request['http_method'], $url, $options) 56 | ->then( 57 | function ($response) use ($url, $start) { 58 | return $this->processResponse($url, (\microtime(true) - $start), $response); 59 | }, 60 | function (GuzzleException $exception) { 61 | return ['error' => $exception] + $this->emptyResponse(); 62 | } 63 | ); 64 | } 65 | 66 | private function processResponse(string $url, float $time, ResponseInterface $response): array 67 | { 68 | return [ 69 | 'version' => $response->getProtocolVersion(), 70 | 'status' => $response->getStatusCode(), 71 | 'reason' => $response->getReasonPhrase(), 72 | 'headers' => $response->getHeaders(), 73 | 'effective_url' => $url, 74 | 'body' => StreamWrapper::getResource($response->getBody()), 75 | 'transfer_stats' => $this->createTransferStats($url, $time, $response), 76 | ]; 77 | } 78 | 79 | private function createTransferStats(string $url, float $time, ResponseInterface $response): array 80 | { 81 | return [ 82 | 'url' => $url, 83 | 'total_time' => $time, 84 | 'content_type' => $response->getHeaderLine('Content-Type'), 85 | 'http_code' => $response->getStatusCode(), 86 | ]; 87 | } 88 | 89 | private function emptyResponse(): array 90 | { 91 | return [ 92 | 'version' => null, 93 | 'status' => null, 94 | 'reason' => null, 95 | 'headers' => [], 96 | 'effective_url' => '', 97 | 'body' => null, 98 | 'transfer_stats' => [ 99 | 'url' => null, 100 | 'total_time' => null, 101 | 'content_type' => null, 102 | 'http_code' => null, 103 | ], 104 | ]; 105 | } 106 | } 107 | --------------------------------------------------------------------------------