├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── UPGRADE.md ├── composer.json └── src ├── DataCollector ├── DataCollectorSymfonyCompatibilityTrait.php └── HttpDataCollector.php ├── DependencyInjection ├── Configuration.php └── EightPointsGuzzleExtension.php ├── EightPointsGuzzleBundle.php ├── Events ├── Event.php ├── GuzzleEvents.php ├── PostTransactionEvent.php └── PreTransactionEvent.php ├── Log ├── DevNullLogger.php ├── LogGroup.php ├── LogMessage.php ├── LogRequest.php ├── LogResponse.php ├── Logger.php └── LoggerInterface.php ├── Middleware ├── EventDispatchMiddleware.php ├── LogMiddleware.php ├── ProfileMiddleware.php ├── RequestTimeMiddleware.php └── SymfonyLogMiddleware.php ├── PluginInterface.php ├── Resources ├── config │ └── services.xml ├── doc │ ├── autowiring-clients.md │ ├── configuration-reference.md │ ├── disable-exception-on-http-error.md │ ├── environment-variables-integration.md │ ├── how-to-create-a-single-file-plugin.md │ ├── img │ │ ├── debug_logs.png │ │ ├── icon_slack.png │ │ └── magic_header_middleware.png │ ├── intercept-request-and-response.md │ └── redefine-client-class.md └── views │ ├── Icons │ └── logo.svg.twig │ ├── debug.html.twig │ ├── main.css.twig │ └── profiler.html.twig └── Twig └── Extension └── DebugExtension.php /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Symfony GuzzleBundle 2 | 3 | 👍 First off, thanks for taking the time to contribute! 🎉 4 | 5 | ## Reporting a Bug 6 | Whenever you find a bug in this bundle, we kindly ask you to report it. It helps us make a better EightPointsGuzzleBundle. 7 | Report it using the official bug [tracker][1] and follow some basic rules: 8 | 9 | - Use the title field to clearly describe the issue 10 | - Describe the steps needed to reproduce the bug with short code examples (providing a unit test that illustrates the bug is best) 11 | - Give as much detail as possible about your environment (PHP version, GuzzleBundle version, Symfony version, ...) 12 | 13 | 14 | ## Adding Feature 15 | You can request a new feature by submitting an issue to our [GitHub Repository][1]. 16 | If you would like to implement a new feature, please submit an issue with a proposal for your work first, to be sure that we can use it. Please consider what kind of change it is: 17 | 18 | - For a Major Feature, first open an issue and outline your proposal so that it can be discussed. This will also allow us to better coordinate our efforts, prevent duplication of work, and help you to craft the change so that it is successfully accepted into the project. 19 | - Small Features can be crafted and directly submitted as a Pull Request. 20 | 21 | 22 | ## Coding Standards 23 | 24 | ### General rule 25 | When contributing code to this bundle, you must follow the [Symfony coding standards][2]. To make a long story short, here is the golden rule: Imitate the existing Symfony code. 26 | Most open-source Bundles and libraries used by Symfony also follow the same guidelines, and you should too. 27 | Remember that the main advantage of standards is that every piece of code looks and feels familiar, it's not about this or that being more readable. 28 | 29 | ### Annotations 30 | Rules: 31 | - write full path of classes 32 | - write `@param` if method has parameters 33 | - write `@return` 34 | - don't align parameters 35 | 36 | Example: 37 | ```php 38 | /** 39 | * @param array $config 40 | * @param \EightPoints\Bundle\GuzzleBundle\Log\LoggerInterface $logger 41 | * 42 | * @return bool 43 | */ 44 | public function randomMethod(array $config, LoggerInterface $logger) : bool 45 | { 46 | // method logic 47 | 48 | return true; 49 | } 50 | ``` 51 | 52 | ## Testing 53 | 54 | ### PHPUnit 55 | Though we have GitHub Actions running tests on every push you do, it would be nice to make sure you run PHPUnit locally before push to upstream. 56 | The easiest way to run tests is to execute this in project root directory - `vendor/bin/simple-phpunit` 57 | 58 | ### Infection PHP 59 | For now we can not include Infection PHP in the repo, because some Symfony 2.7 dependencies are not compatible with Infection. 60 | But you can always install Infection globally and run it for project with: 61 | - `composer global require infection/infection` 62 | - update your $PATH variable in `~/.bash_profile` to contain path to composer binary files `~/.composer/vendor/bin`. Beware for some systems path might be slightly different! 63 | - `infection --threads=4` in the project root 64 | 65 | [1]: https://github.com/8p/EightPointsGuzzleBundle/issues 66 | [2]: https://symfony.com/doc/current/contributing/code/standards.html 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 8points IT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **[Prerequisites](#prerequisites)** | 2 | **[Installation](#installation)** | 3 | **[Configuration](#configuration)** | 4 | **[Usage](#usage)** | 5 | **[Plugins](#plugins)** | 6 | **[Events](#events)** | 7 | **[Features](#features)** | 8 | **[Suggestions](#suggestions)** | 9 | **[Contributing](#contributing)** | 10 | **[Learn more](#learn-more)** | 11 | **[License](#license)** 12 | 13 | # EightPoints GuzzleBundle for Symfony 14 | 15 | [![Total Downloads](https://poser.pugx.org/eightpoints/guzzle-bundle/downloads.png)](https://packagist.org/packages/eightpoints/guzzle-bundle) 16 | [![Monthly Downloads](https://poser.pugx.org/eightpoints/guzzle-bundle/d/monthly.png)](https://packagist.org/packages/eightpoints/guzzle-bundle) 17 | [![Latest Stable Version](https://poser.pugx.org/eightpoints/guzzle-bundle/v/stable.png)](https://packagist.org/packages/eightpoints/guzzle-bundle) 18 | [![Build Status](https://github.com/8p/EightPointsGuzzleBundle/workflows/PHPUnit/badge.svg)](https://github.com/8p/EightPointsGuzzleBundle/actions?query=branch%3Amaster+workflow%3APHPUnit) 19 | [![Scrutinizer Score](https://scrutinizer-ci.com/g/8p/EightPointsGuzzleBundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/8p/EightPointsGuzzleBundle/) 20 | [![License](https://poser.pugx.org/eightpoints/guzzle-bundle/license)](https://packagist.org/packages/eightpoints/guzzle-bundle) 21 | 22 | This bundle integrates [Guzzle 6.x|7.x][1] into [Symfony][16]. Guzzle is a PHP library for building RESTful web service clients. 23 | 24 | GuzzleBundle follows semantic versioning. Read more on [semver.org][2]. 25 | 26 | ---- 27 | 28 | ## Prerequisites 29 | - PHP 7.2 or higher 30 | - Symfony 5.x or 6.x or 7.x 31 | 32 | ---- 33 | 34 | ## Installation 35 | 36 | ### Installing the bundle 37 | 38 | To install this bundle, run the command below on the command line and you will get the latest stable version from [Packagist][3]. 39 | 40 | ``` bash 41 | composer require eightpoints/guzzle-bundle 42 | ``` 43 | 44 | _Note: this bundle has a [Symfony Flex Recipe][14] to automatically register and configure this bundle into your symfony application._ 45 | 46 | If your project does *not* use Symfony Flex the following needs to be added to `config/bundles.php` manually: 47 | 48 | ```php 49 | ['all' => true], 54 | ]; 55 | ``` 56 | 57 | ---- 58 | 59 | ## Configuration 60 | 61 | Guzzle clients can be configured in `config/packages/eight_points_guzzle.yaml`. For projects that use Symfony Flex this file is created 62 | automatically upon installation of this bundle. For projects that don't use Symfony Flex this file should be created manually. 63 | 64 | ```yaml 65 | eight_points_guzzle: 66 | # (de)activate logging/profiler; default: %kernel.debug% 67 | logging: true 68 | 69 | # configure when a response is considered to be slow (in ms); default 0 (disabled) 70 | slow_response_time: 1000 71 | 72 | clients: 73 | payment: 74 | base_url: 'http://api.payment.example' 75 | 76 | # NOTE: This option marks this Guzzle Client as lazy (https://symfony.com/doc/master/service_container/lazy_services.html) 77 | lazy: true # Default `false` 78 | 79 | # guzzle client options (full description here: https://guzzle.readthedocs.org/en/latest/request-options.html) 80 | options: 81 | auth: 82 | - acme # login 83 | - pa55w0rd # password 84 | 85 | headers: 86 | Accept: 'application/json' 87 | 88 | # Find proper php const, for example CURLOPT_SSLVERSION, remove CURLOPT_ and transform to lower case. 89 | # List of curl options: http://php.net/manual/en/function.curl-setopt.php 90 | curl: 91 | sslversion: 1 92 | 93 | timeout: 30 94 | 95 | # plugin settings 96 | plugin: ~ 97 | 98 | crm: 99 | base_url: 'http://api.crm.tld' 100 | options: 101 | headers: 102 | Accept: 'application/json' 103 | 104 | # More clients here 105 | ``` 106 | 107 | Please refer to the [Configuration Reference](src/Resources/doc/configuration-reference.md) for a complete list of all options. 108 | 109 | ## Usage 110 | 111 | Guzzle clients configured through this bundle are available in the Symfony Dependency Injection container under the name 112 | `eight_points_guzzle.client.`. So for example a client configured in the configuration with name `payment` is available 113 | as `eight_points_guzzle.client.payment`. 114 | 115 | Suppose you have the following controller that requires a Guzzle Client: 116 | 117 | ```php 118 | client = $client; 129 | } 130 | } 131 | ``` 132 | 133 | Using manual wiring this controller can be wired as follows: 134 | 135 | ```yaml 136 | services: 137 | my.example.controller: 138 | class: App\Controller\ExampleController 139 | arguments: ['@eight_points_guzzle.client.payment'] 140 | ``` 141 | 142 | For projects that use [autowiring][18], please refer to [our documentation on autowiring](src/Resources/doc/autowiring-clients.md). 143 | 144 | ---- 145 | 146 | ## Plugins 147 | 148 | This bundle allows to register and integrate plugins to extend functionality of guzzle and this bundle. 149 | 150 | ### Installation 151 | 152 | In order to install a plugin, find the following lines in `src/Kernel.php`: 153 | 154 | ```php 155 | foreach ($contents as $class => $envs) { 156 | if ($envs[$this->environment] ?? $envs['all'] ?? false) { 157 | yield new $class(); 158 | } 159 | } 160 | ``` 161 | 162 | and replace them with the following: 163 | 164 | ```php 165 | foreach ($contents as $class => $envs) { 166 | if ($envs[$this->environment] ?? $envs['all'] ?? false) { 167 | if ($class === \EightPoints\Bundle\GuzzleBundle\EightPointsGuzzleBundle::class) { 168 | yield new $class([ 169 | new \Gregurco\Bundle\GuzzleBundleOAuth2Plugin\GuzzleBundleOAuth2Plugin(), 170 | ]); 171 | } else { 172 | yield new $class(); 173 | } 174 | } 175 | } 176 | ``` 177 | 178 | ### Known and Supported Plugins 179 | - [gregurco/GuzzleBundleWssePlugin][5] 180 | - [gregurco/GuzzleBundleCachePlugin][6] 181 | - [gregurco/GuzzleBundleOAuth2Plugin][7] 182 | - [neirda24/GuzzleBundleHeaderForwardPlugin][12] 183 | - [neirda24/GuzzleBundleHeaderDisableCachePlugin][13] 184 | - [EugenGanshorn/GuzzleBundleRetryPlugin][15] 185 | 186 | ---- 187 | 188 | ## Events 189 | 190 | This bundle dispatches Symfony events right before a client makes a call and right after a client has made a call. 191 | There are two types of events dispatched every time; a _generic_ event, that is dispatched regardless of which client is doing the request, 192 | and a _client specific_ event, that is dispatched only to listeners specifically subscribed to events from a specific client. 193 | These events can be used to intercept requests to a remote system as well as responses from a remote system. 194 | In case a generic event listener and a client specific event listener both change a request/response, the changes from the client 195 | specific listener override those of the generic listener in case of a collision (both setting the same header for example). 196 | 197 | ### Listening To Events 198 | 199 | In order to listen to these events you should create a listener and register that listener in the Symfony services configuration as usual: 200 | 201 | ```yaml 202 | services: 203 | my_guzzle_listener: 204 | class: App\Service\MyGuzzleBundleListener 205 | tags: 206 | # Listen for generic pre transaction event (will receive events for all clients) 207 | - { name: 'kernel.event_listener', event: 'eight_points_guzzle.pre_transaction', method: 'onPreTransaction' } 208 | # Listen for client specific pre transaction events (will only receive events for the "payment" client) 209 | - { name: 'kernel.event_listener', event: 'eight_points_guzzle.pre_transaction.payment', method: 'onPrePaymentTransaction' } 210 | 211 | - # Listen for generic post transaction event (will receive events for all clients) 212 | - { name: 'kernel.event_listener', event: 'eight_points_guzzle.post_transaction', method: 'onPostTransaction' } 213 | # Listen for client specific post transaction events (will only receive events for the "payment" client) 214 | - { name: 'kernel.event_listener', event: 'eight_points_guzzle.post_transaction.payment', method: 'onPostPaymentTransaction' } 215 | ``` 216 | 217 | For more information, read the docs on [intercepting requests and responses](src/Resources/doc/intercept-request-and-response.md). 218 | 219 | ---- 220 | 221 | ## Features 222 | 223 | ### Symfony Debug Toolbar / Profiler 224 | Debug Logs 225 | 226 | ### Logging 227 | 228 | All requests are logged to Symfony's default logger (`@logger` service) with the following (default) format: 229 | ``` 230 | [{datetime}] eight_points_guzzle.{log_level}: {method} {uri} {code} 231 | ``` 232 | 233 | Example: 234 | ``` 235 | [2017-12-01 00:00:00] eight_points_guzzle.INFO: GET http://api.domain.tld 200 236 | ``` 237 | 238 | You can change the message format by overriding the `eight_points_guzzle.symfony_log_formatter.pattern` parameter. 239 | For all options please refer to [Guzzle's MessageFormatter][8]. 240 | 241 | ---- 242 | 243 | ## Suggestions 244 | 245 | ### Create aliases for clients 246 | 247 | In case your project uses manual wiring it is recommended to create aliases for the clients created with this bundle to 248 | get easier service names and also to make it easier to switch to other implementations in the future, might the need arise. 249 | 250 | ``` yaml 251 | services: 252 | crm.client: '@eight_points_guzzle.client.crm' 253 | ``` 254 | 255 | In case your project uses autowiring this suggestion does not apply. 256 | 257 | ---- 258 | 259 | ## Contributing 260 | 👍 If you would like to contribute to this bundle, please read [CONTRIBUTING.md](CONTRIBUTING.md). 261 | 262 | Slack Join our Slack channel on [Symfony Devs][9] for discussions, questions and more: [#8p-guzzlebundle][10]. 263 | 264 | 🎉 Thanks to all [contributors][11] who participated in this project. 265 | 266 | ---- 267 | 268 | ## Learn more 269 | - [Autowiring Clients](src/Resources/doc/autowiring-clients.md) 270 | - [Configuration Reference](src/Resources/doc/configuration-reference.md) 271 | - [Disable throwing exceptions on HTTP errors (4xx and 5xx responses)](src/Resources/doc/disable-exception-on-http-error.md) 272 | - [Environment variables integration](src/Resources/doc/environment-variables-integration.md) 273 | - [How to create a single-file plugin](src/Resources/doc/how-to-create-a-single-file-plugin.md) 274 | - [How to redefine class used for clients](src/Resources/doc/redefine-client-class.md) 275 | - [Intercept request and response](src/Resources/doc/intercept-request-and-response.md) 276 | 277 | ---- 278 | 279 | ## License 280 | 281 | This bundle is released under the [MIT license](LICENSE). 282 | 283 | [1]: http://guzzlephp.org/ 284 | [2]: http://semver.org/ 285 | [3]: https://packagist.org/packages/eightpoints/guzzle-bundle 286 | [4]: https://github.com/symfony/flex 287 | [5]: https://github.com/gregurco/GuzzleBundleWssePlugin 288 | [6]: https://github.com/gregurco/GuzzleBundleCachePlugin 289 | [7]: https://github.com/gregurco/GuzzleBundleOAuth2Plugin 290 | [8]: https://github.com/guzzle/guzzle/blob/6.3.0/src/MessageFormatter.php#L14 291 | [9]: https://symfony.com/slack-invite 292 | [10]: https://symfony-devs.slack.com/messages/C8LUKU6JD 293 | [11]: https://github.com/8p/EightPointsGuzzleBundle/graphs/contributors 294 | [12]: https://github.com/Neirda24/GuzzleBundleHeaderForwardPlugin 295 | [13]: https://github.com/Neirda24/GuzzleBundleHeaderDisableCachePlugin 296 | [14]: https://github.com/symfony/recipes-contrib/tree/master/eightpoints/guzzle-bundle 297 | [15]: https://github.com/EugenGanshorn/GuzzleBundleRetryPlugin 298 | [16]: https://symfony.com/ 299 | [17]: https://github.com/symfony/skeleton 300 | [18]: https://symfony.com/doc/current/service_container/autowiring.html 301 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrade instruction 2 | 3 | This document describes the changes needed when upgrading from one version to another. 4 | 5 | ## Upgrading From 7.x to 8.0 6 | 7 | ### Step 1: upgrade PHP and Symfony 8 | 9 | Minimum required PHP version was raised to 7.1 and Symfony to 4.0. 10 | [Rector](https://github.com/rectorphp/rector) can help you with migration. 11 | 12 | ### Step 2: remove usage of GuzzleEventListenerInterface 13 | 14 | The interface `GuzzleEventListenerInterface` was removed. 15 | Please read the [documentation](https://github.com/8p/EightPointsGuzzleBundle#listening-to-events) in case you want to listen pre/post translation events for specific client. 16 | 17 | ### Step 3: follow strict typing rules 18 | 19 | If your classes are overriding some classes from bundle, then check that all methods are following arguments and return types. 20 | 21 | ### Step 4: replace usage of EightPointsGuzzleBundlePlugin interface 22 | 23 | before: 24 | ```php 25 | use EightPoints\Bundle\GuzzleBundle\EightPointsGuzzleBundlePlugin; 26 | 27 | class Foo implements EightPointsGuzzleBundlePlugin 28 | { 29 | 30 | } 31 | ``` 32 | 33 | after: 34 | ```php 35 | use EightPoints\Bundle\GuzzleBundle\PluginInterface; 36 | 37 | class Foo implements PluginInterface 38 | { 39 | 40 | } 41 | ``` 42 | 43 | ## Upgrading From 6.x to 7.0 44 | 45 | ### Step 1: change namespace in AppKernel 46 | 47 | before: 48 | ```php 49 | public function registerBundles() 50 | { 51 | $bundles = [ 52 | ... 53 | new EightPoints\Bundle\GuzzleBundle\GuzzleBundle(), 54 | ... 55 | ]; 56 | } 57 | ``` 58 | 59 | after: 60 | ```php 61 | public function registerBundles() 62 | { 63 | $bundles = [ 64 | ... 65 | new EightPoints\Bundle\GuzzleBundle\EightPointsGuzzleBundle(), 66 | ... 67 | ]; 68 | } 69 | ``` 70 | 71 | ### Step 2: change config key in app/config/config.yml 72 | 73 | before: 74 | ```yaml 75 | guzzle: 76 | clients: 77 | api_payment: 78 | base_url: "http://api.domain.tld" 79 | ``` 80 | 81 | after: 82 | ```yaml 83 | eight_points_guzzle: 84 | clients: 85 | api_payment: 86 | base_url: "http://api.domain.tld" 87 | ``` 88 | 89 | ### Step 3: move headers key under options config 90 | 91 | before: 92 | ```yaml 93 | guzzle: 94 | clients: 95 | api_payment: 96 | base_url: "http://api.domain.tld" 97 | headers: 98 | Accept: "application/json" 99 | ``` 100 | 101 | after: 102 | ```yaml 103 | eight_points_guzzle: 104 | clients: 105 | api_payment: 106 | base_url: "http://api.domain.tld" 107 | options: 108 | headers: 109 | Accept: "application/json" 110 | ``` 111 | 112 | ### Step 4: client call 113 | 114 | before: 115 | ```php 116 | $this->get('guzzle.client.api_crm'); 117 | ``` 118 | 119 | after: 120 | ```php 121 | $this->get('eight_points_guzzle.client.api_crm'); 122 | ``` 123 | 124 | ### Step 5: event listeners definition 125 | 126 | before: 127 | ```xml 128 | 129 | 130 | 131 | ``` 132 | 133 | after: 134 | ```xml 135 | 136 | 137 | 138 | ``` 139 | 140 | ### Step 6: if you have created any services, you should change name 141 | 142 | before: 143 | ```xml 144 | 145 | ``` 146 | 147 | after: 148 | ```xml 149 | 150 | ``` 151 | 152 | ### Step 7: WSSE plugin 153 | 154 | WSSE plugin was moved to separate repository. 155 | If you are using WSSE then follow install guide from [gregurco/guzzle-bundle-wsse-plugin](https://github.com/gregurco/GuzzleBundleWssePlugin). 156 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eightpoints/guzzle-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Integrates Guzzle 6.x, a PHP HTTP Client, into Symfony. Comes with easy and powerful configuration options and optional plugins.", 5 | "keywords": [ 6 | "bundle", 7 | "guzzle", 8 | "symfony", 9 | "rest", 10 | "web service", 11 | "curl", 12 | "client", 13 | "HTTP client" 14 | ], 15 | "homepage": "https://github.com/8p/GuzzleBundle", 16 | "license": "MIT", 17 | "authors": [ 18 | { 19 | "name": "Florian Preusner", 20 | "email": "florian.preusner@8points.de", 21 | "homepage": "https://github.com/florianpreusner" 22 | }, 23 | { 24 | "name": "Community", 25 | "homepage": "https://github.com/8p/GuzzleBundle/contributors" 26 | } 27 | ], 28 | "require": { 29 | "php": ">=7.2", 30 | "guzzlehttp/guzzle": "^6.5.8|^7.4.5", 31 | "guzzlehttp/promises": "^1.5.3|^2.0", 32 | "guzzlehttp/psr7": "^1.9.1|^2.5", 33 | "symfony/framework-bundle": "~5.0|~6.0|~7.0", 34 | "symfony/expression-language": "~5.0|~6.0|~7.0", 35 | "symfony/stopwatch": "~5.0|~6.0|~7.0", 36 | "psr/log": "~1.0|~2.0|~3.0" 37 | }, 38 | "require-dev": { 39 | "symfony/phpunit-bridge": "~5.0|~6.0|~7.0", 40 | "symfony/twig-bundle": "~5.0|~6.0|~7.0", 41 | "symfony/var-dumper": "~5.0|~6.0|~7.0", 42 | "symfony/yaml": "~5.0|~6.0|~7.0" 43 | }, 44 | "suggest": { 45 | "namshi/cuzzle": "Outputs Curl command on profiler's page for debugging purposes" 46 | }, 47 | "autoload": { 48 | "psr-4": { 49 | "EightPoints\\Bundle\\GuzzleBundle\\": "src" 50 | } 51 | }, 52 | "autoload-dev": { 53 | "psr-4": { 54 | "EightPoints\\Bundle\\GuzzleBundle\\Tests\\": "tests" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/DataCollector/DataCollectorSymfonyCompatibilityTrait.php: -------------------------------------------------------------------------------- 1 | doCollect($request, $response, $exception); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/DataCollector/HttpDataCollector.php: -------------------------------------------------------------------------------- 1 | loggers = $loggers; 32 | $this->slowResponseTime = $slowResponseTime; 33 | 34 | $this->reset(); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | protected function doCollect(Request $request, Response $response, ?\Throwable $exception = null) 41 | { 42 | $messages = []; 43 | foreach ($this->loggers as $logger) { 44 | $messages = array_merge($messages, $logger->getMessages()); 45 | } 46 | 47 | if ($this->slowResponseTime > 0) { 48 | foreach ($messages as $message) { 49 | if (!$message instanceof LogMessage) { 50 | continue; 51 | } 52 | 53 | if ($message->getTransferTime() >= $this->slowResponseTime) { 54 | $this->data['hasSlowResponse'] = true; 55 | break; 56 | } 57 | } 58 | } 59 | 60 | $requestId = $request->getUri(); 61 | 62 | // clear log to have only messages related to Symfony request context 63 | foreach ($this->loggers as $logger) { 64 | $logger->clear(); 65 | } 66 | 67 | $logGroup = $this->getLogGroup($requestId); 68 | $logGroup->setRequestName($request->getPathInfo()); 69 | $logGroup->addMessages($messages); 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function getName() : string 76 | { 77 | return 'eight_points_guzzle'; 78 | } 79 | 80 | /** 81 | * Resets this data collector to its initial state. 82 | * 83 | * @return void 84 | */ 85 | public function reset() : void 86 | { 87 | $this->data = [ 88 | 'logs' => [], 89 | 'callCount' => 0, 90 | 'totalTime' => 0, 91 | 'hasSlowResponse' => false, 92 | ]; 93 | } 94 | 95 | /** 96 | * Returning log entries 97 | * 98 | * @return array 99 | */ 100 | public function getLogs() : array 101 | { 102 | return array_key_exists('logs', $this->data) ? $this->data['logs'] : []; 103 | } 104 | 105 | /** 106 | * Get all messages 107 | * 108 | * @return array 109 | */ 110 | public function getMessages() : array 111 | { 112 | $messages = []; 113 | 114 | foreach ($this->getLogs() as $log) { 115 | foreach ($log->getMessages() as $message) { 116 | $messages[] = $message; 117 | } 118 | } 119 | 120 | return $messages; 121 | } 122 | 123 | /** 124 | * Return amount of http calls 125 | * 126 | * @return integer 127 | */ 128 | public function getCallCount() : int 129 | { 130 | return count($this->getMessages()); 131 | } 132 | 133 | /** 134 | * Get Error Count 135 | * 136 | * @return integer 137 | */ 138 | public function getErrorCount() : int 139 | { 140 | return count($this->getErrorsByType(LogLevel::ERROR)); 141 | } 142 | 143 | /** 144 | * @param string $type 145 | * 146 | * @return array 147 | */ 148 | public function getErrorsByType(string $type) : array 149 | { 150 | return array_filter( 151 | $this->getMessages(), 152 | function (LogMessage $message) use ($type) { 153 | return $message->getLevel() === $type; 154 | } 155 | ); 156 | } 157 | 158 | /** 159 | * Get total time of all requests 160 | * 161 | * @return float 162 | */ 163 | public function getTotalTime() : float 164 | { 165 | return $this->data['totalTime']; 166 | } 167 | 168 | /** 169 | * Check if there were any slow responses 170 | * 171 | * @return bool 172 | */ 173 | public function hasSlowResponses() : bool 174 | { 175 | return $this->data['hasSlowResponse']; 176 | } 177 | 178 | /** 179 | * @param float $time 180 | * 181 | * @return void 182 | */ 183 | public function addTotalTime(float $time) : void 184 | { 185 | $this->data['totalTime'] += $time; 186 | } 187 | 188 | /** 189 | * Returns (new) LogGroup based on given id 190 | * 191 | * @param string $id 192 | * 193 | * @return \EightPoints\Bundle\GuzzleBundle\Log\LogGroup 194 | */ 195 | protected function getLogGroup(string $id) : LogGroup 196 | { 197 | if (!isset($this->data['logs'][$id])) { 198 | $this->data['logs'][$id] = new LogGroup(); 199 | } 200 | 201 | return $this->data['logs'][$id]; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | alias = $alias; 37 | $this->debug = $debug; 38 | $this->plugins = $plugins; 39 | } 40 | 41 | /** 42 | * Generates the configuration tree builder 43 | * 44 | * @throws \RuntimeException 45 | * 46 | * @return \Symfony\Component\Config\Definition\Builder\TreeBuilder 47 | */ 48 | public function getConfigTreeBuilder() : TreeBuilder 49 | { 50 | $builder = new TreeBuilder($this->alias); 51 | 52 | if (method_exists($builder, 'getRootNode')) { 53 | $root = $builder->getRootNode(); 54 | } else { 55 | // BC layer for symfony/config 4.1 and older 56 | $root = $builder->root($this->alias); 57 | } 58 | 59 | $root 60 | ->children() 61 | ->append($this->createClientsNode()) 62 | ->booleanNode('logging')->defaultValue($this->debug)->end() 63 | ->booleanNode('profiling')->defaultValue($this->debug)->end() 64 | ->integerNode('slow_response_time')->defaultValue(0)->end() 65 | ->end() 66 | ->end(); 67 | 68 | return $builder; 69 | } 70 | 71 | /** 72 | * Create Clients Configuration 73 | * 74 | * @throws \RuntimeException 75 | * 76 | * @return \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition 77 | */ 78 | private function createClientsNode() : ArrayNodeDefinition 79 | { 80 | $builder = new TreeBuilder('clients'); 81 | 82 | /** @var \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $node */ 83 | if (method_exists($builder, 'getRootNode')) { 84 | $node = $builder->getRootNode(); 85 | } else { 86 | // BC layer for symfony/config 4.1 and older 87 | $node = $builder->root('clients'); 88 | } 89 | 90 | /** @var \Symfony\Component\Config\Definition\Builder\NodeBuilder $nodeChildren */ 91 | $nodeChildren = $node->useAttributeAsKey('name') 92 | ->prototype('array') 93 | ->children(); 94 | 95 | $nodeChildren->scalarNode('class')->defaultValue('%eight_points_guzzle.http_client.class%')->end() 96 | ->scalarNode('base_url') 97 | ->defaultValue(null) 98 | ->validate() 99 | ->ifTrue(function ($v) { 100 | return !is_string($v); 101 | }) 102 | ->thenInvalid('base_url can be: string') 103 | ->end() 104 | ->end() 105 | ->booleanNode('lazy')->defaultValue(false)->end() 106 | ->integerNode('logging') 107 | ->defaultValue(null) 108 | ->beforeNormalization() 109 | ->always(function ($value): int { 110 | if ($value === 1 || $value === true) { 111 | return Logger::LOG_MODE_REQUEST_AND_RESPONSE; 112 | } elseif ($value === 0 || $value === false) { 113 | return Logger::LOG_MODE_NONE; 114 | } else { 115 | return constant(Logger::class .'::LOG_MODE_' . strtoupper($value)); 116 | } 117 | }) 118 | ->end() 119 | ->end() 120 | ->scalarNode('handler') 121 | ->defaultValue(null) 122 | ->validate() 123 | ->ifTrue(function ($v) { 124 | return $v !== null && (!is_string($v) || !class_exists($v)); 125 | }) 126 | ->thenInvalid('handler must be a valid FQCN for a loaded class') 127 | ->end() 128 | ->end() 129 | ->arrayNode('options') 130 | ->validate() 131 | ->ifTrue(function ($options) { 132 | return count($options['form_params']) && count($options['multipart']); 133 | }) 134 | ->thenInvalid('You cannot use form_params and multipart at the same time.') 135 | ->end() 136 | ->children() 137 | ->arrayNode('headers') 138 | ->useAttributeAsKey('name') 139 | ->normalizeKeys(false) 140 | ->prototype('scalar')->end() 141 | ->end() 142 | ->variableNode('allow_redirects') 143 | ->validate() 144 | ->ifTrue(function ($v) { 145 | return !is_array($v) && !is_bool($v); 146 | }) 147 | ->thenInvalid('allow_redirects can be: bool or array') 148 | ->end() 149 | ->end() 150 | ->variableNode('auth') 151 | ->validate() 152 | ->ifTrue(function ($v) { 153 | return !is_array($v) && !is_string($v); 154 | }) 155 | ->thenInvalid('auth can be: string or array') 156 | ->end() 157 | ->end() 158 | ->variableNode('query') 159 | ->validate() 160 | ->ifTrue(function ($v) { 161 | return !is_string($v) && !is_array($v); 162 | }) 163 | ->thenInvalid('query can be: string or array') 164 | ->end() 165 | ->end() 166 | ->arrayNode('curl') 167 | ->beforeNormalization() 168 | ->ifArray() 169 | ->then(function (array $curlOptions) { 170 | $result = []; 171 | 172 | foreach ($curlOptions as $key => $value) { 173 | $optionName = 'CURLOPT_' . strtoupper($key); 174 | 175 | if (!defined($optionName)) { 176 | throw new InvalidConfigurationException(sprintf( 177 | 'Invalid curl option in eight_points_guzzle: %s. ' . 178 | 'Ex: use sslversion for CURLOPT_SSLVERSION option. ' . PHP_EOL . 179 | 'See all available options: http://php.net/manual/en/function.curl-setopt.php', 180 | $key 181 | )); 182 | } 183 | 184 | $result[constant($optionName)] = $value; 185 | } 186 | 187 | return $result; 188 | }) 189 | ->end() 190 | ->prototype('scalar') 191 | ->end() 192 | ->end() 193 | ->variableNode('cert') 194 | ->validate() 195 | ->ifTrue(function ($v) { 196 | return !is_string($v) && (!is_array($v) || count($v) !== 2); 197 | }) 198 | ->thenInvalid('cert can be: string or array with two entries (path and password)') 199 | ->end() 200 | ->end() 201 | ->scalarNode('connect_timeout') 202 | ->beforeNormalization() 203 | ->always(function ($v) { 204 | return is_numeric($v) ? (float) $v : $v; 205 | }) 206 | ->end() 207 | ->validate() 208 | ->ifTrue(function ($v) { 209 | return !is_float($v) && !(is_string($v) && strpos($v, 'env_') === 0); 210 | }) 211 | ->thenInvalid('connect_timeout can be: float') 212 | ->end() 213 | ->end() 214 | ->booleanNode('debug')->end() 215 | ->variableNode('decode_content') 216 | ->validate() 217 | ->ifTrue(function ($v) { 218 | return !is_string($v) && !is_bool($v); 219 | }) 220 | ->thenInvalid('decode_content can be: bool or string (gzip, compress, deflate, etc...)') 221 | ->end() 222 | ->end() 223 | ->floatNode('delay')->end() 224 | ->arrayNode('form_params') 225 | ->useAttributeAsKey('name') 226 | ->prototype('variable')->end() 227 | ->end() 228 | ->arrayNode('multipart') 229 | ->prototype('variable')->end() 230 | ->end() 231 | ->scalarNode('sink') 232 | ->validate() 233 | ->ifTrue(function ($v) { 234 | return !is_string($v); 235 | }) 236 | ->thenInvalid('sink can be: string') 237 | ->end() 238 | ->end() 239 | ->booleanNode('http_errors')->end() 240 | ->variableNode('expect') 241 | ->validate() 242 | ->ifTrue(function ($v) { 243 | return !is_bool($v) && !is_int($v); 244 | }) 245 | ->thenInvalid('expect can be: bool or int') 246 | ->end() 247 | ->end() 248 | ->variableNode('ssl_key') 249 | ->validate() 250 | ->ifTrue(function ($v) { 251 | return !is_string($v) && (!is_array($v) || count($v) !== 2); 252 | }) 253 | ->thenInvalid('ssl_key can be: string or array with two entries (path and password)') 254 | ->end() 255 | ->end() 256 | ->booleanNode('stream')->end() 257 | ->booleanNode('synchronous')->end() 258 | ->scalarNode('read_timeout') 259 | ->beforeNormalization() 260 | ->always(function ($v) { 261 | return is_numeric($v) ? (float) $v : $v; 262 | }) 263 | ->end() 264 | ->validate() 265 | ->ifTrue(function ($v) { 266 | return !is_float($v) && !(is_string($v) && strpos($v, 'env_') === 0); 267 | }) 268 | ->thenInvalid('read_timeout can be: float') 269 | ->end() 270 | ->end() 271 | ->scalarNode('timeout') 272 | ->beforeNormalization() 273 | ->always(function ($v) { 274 | return is_numeric($v) ? (float) $v : $v; 275 | }) 276 | ->end() 277 | ->validate() 278 | ->ifTrue(function ($v) { 279 | return !is_float($v) && !(is_string($v) && strpos($v, 'env_') === 0); 280 | }) 281 | ->thenInvalid('timeout can be: float') 282 | ->end() 283 | ->end() 284 | ->variableNode('verify') 285 | ->validate() 286 | ->ifTrue(function ($v) { 287 | return !is_bool($v) && !is_string($v); 288 | }) 289 | ->thenInvalid('verify can be: bool or string') 290 | ->end() 291 | ->end() 292 | ->booleanNode('cookies')->end() 293 | ->arrayNode('proxy') 294 | ->beforeNormalization() 295 | ->ifString() 296 | ->then(function($v) { return ['http'=> $v]; }) 297 | ->end() 298 | ->validate() 299 | ->always(function($v) { 300 | if (empty($v['no'])) { 301 | unset($v['no']); 302 | } 303 | return $v; 304 | }) 305 | ->end() 306 | ->children() 307 | ->scalarNode('http')->end() 308 | ->scalarNode('https')->end() 309 | ->arrayNode('no') 310 | ->prototype('scalar')->end() 311 | ->end() 312 | ->end() 313 | ->end() 314 | ->scalarNode('version') 315 | ->validate() 316 | ->ifTrue(function ($v) { 317 | return !is_string($v) && !is_float($v); 318 | }) 319 | ->thenInvalid('version can be: string or float') 320 | ->end() 321 | ->end() 322 | ->end() 323 | ->end(); 324 | 325 | $pluginsNode = $nodeChildren->arrayNode('plugin')->addDefaultsIfNotSet(); 326 | 327 | foreach ($this->plugins as $plugin) { 328 | $pluginNode = new ArrayNodeDefinition($plugin->getPluginName()); 329 | 330 | $plugin->addConfiguration($pluginNode); 331 | 332 | $pluginsNode->children()->append($pluginNode); 333 | } 334 | 335 | return $node; 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/DependencyInjection/EightPointsGuzzleExtension.php: -------------------------------------------------------------------------------- 1 | plugins = $plugins; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function getConfiguration(array $config, ContainerBuilder $container) : Configuration 34 | { 35 | return new Configuration($this->getAlias(), $container->getParameter('kernel.debug'), $this->plugins); 36 | } 37 | 38 | /** 39 | * Loads the Guzzle configuration. 40 | * 41 | * @param array $configs an array of configuration settings 42 | * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container a ContainerBuilder instance 43 | * 44 | * @throws \InvalidArgumentException 45 | * @throws \Symfony\Component\DependencyInjection\Exception\BadMethodCallException 46 | * @throws \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException 47 | * @throws \Exception 48 | * 49 | * @return void 50 | */ 51 | public function load(array $configs, ContainerBuilder $container) 52 | { 53 | $configPath = implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'Resources', 'config']); 54 | $loader = new XmlFileLoader($container, new FileLocator($configPath)); 55 | 56 | $loader->load('services.xml'); 57 | 58 | $configuration = new Configuration($this->getAlias(), $container->getParameter('kernel.debug'), $this->plugins); 59 | $config = $this->processConfiguration($configuration, $configs); 60 | $logging = $config['logging'] === true; 61 | $profiling = $config['profiling'] === true; 62 | 63 | foreach ($this->plugins as $plugin) { 64 | $container->addObjectResource(new \ReflectionClass(get_class($plugin))); 65 | $plugin->load($config, $container); 66 | } 67 | 68 | foreach ($config['clients'] as $name => $options) { 69 | $options['logging'] = $logging ? ($options['logging'] ?? true) : false; 70 | 71 | $argument = [ 72 | 'base_uri' => $options['base_url'], 73 | 'handler' => $this->createHandler($container, $name, $options, $profiling) 74 | ]; 75 | 76 | // if present, add default options to the constructor argument for the Guzzle client 77 | if (isset($options['options']) && is_array($options['options'])) { 78 | foreach ($options['options'] as $key => $value) { 79 | if ($value === null || (is_array($value) && count($value) === 0)) { 80 | continue; 81 | } 82 | 83 | $argument[$key] = $value; 84 | } 85 | } 86 | 87 | $client = new Definition($options['class']); 88 | $client->addArgument($argument); 89 | $client->setPublic(true); 90 | $client->setLazy($options['lazy']); 91 | 92 | // set service name based on client name 93 | $serviceName = sprintf('%s.client.%s', $this->getAlias(), $name); 94 | $container->setDefinition($serviceName, $client); 95 | 96 | // Allowed only for Symfony 4.2+ 97 | if (method_exists($container, 'registerAliasForArgument')) { 98 | if ('%eight_points_guzzle.http_client.class%' !== $options['class']) { 99 | $container->registerAliasForArgument($serviceName, $options['class'], $name . 'Client'); 100 | } 101 | $container->registerAliasForArgument($serviceName, ClientInterface::class, $name . 'Client'); 102 | } 103 | } 104 | 105 | $clientsWithLogging = array_filter($config['clients'], function($options) use ($logging) { 106 | return $options['logging'] !== false && $logging !== false; 107 | }); 108 | 109 | if (count($clientsWithLogging) > 0) { 110 | $this->defineTwigDebugExtension($container); 111 | $this->defineDataCollector($container, $config['slow_response_time'] / 1000); 112 | $this->defineFormatter($container); 113 | $this->defineSymfonyLogFormatter($container); 114 | $this->defineSymfonyLogMiddleware($container); 115 | } 116 | } 117 | 118 | /** 119 | * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container 120 | * @param string $clientName 121 | * @param array $options 122 | * @param bool $profiling 123 | * 124 | * @throws \Symfony\Component\DependencyInjection\Exception\BadMethodCallException 125 | * @throws \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException 126 | * 127 | * @return \Symfony\Component\DependencyInjection\Definition 128 | */ 129 | protected function createHandler(ContainerBuilder $container, string $clientName, array $options, bool $profiling) : Definition 130 | { 131 | // Event Dispatching service 132 | $eventServiceName = sprintf('eight_points_guzzle.middleware.event_dispatch.%s', $clientName); 133 | $eventService = $this->createEventMiddleware($clientName); 134 | $container->setDefinition($eventServiceName, $eventService); 135 | 136 | // Create the event Dispatch Middleware 137 | $eventExpression = new Expression(sprintf("service('%s').dispatchEvent()", $eventServiceName)); 138 | 139 | $handler = new Definition(HandlerStack::class); 140 | $handler->setFactory([HandlerStack::class, 'create']); 141 | if (isset($options['handler'])) { 142 | 143 | $handlerServiceName = sprintf('eight_points_guzzle.handler.%s', $clientName); 144 | $handlerService = new Definition($options['handler']); 145 | $container->setDefinition($handlerServiceName, $handlerService); 146 | 147 | $handler->addArgument(new Reference($handlerServiceName)); 148 | } 149 | $handler->setPublic(true); 150 | $handler->setLazy($options['lazy']); 151 | 152 | $handlerStackServiceName = sprintf('eight_points_guzzle.handler_stack.%s', $clientName); 153 | $container->setDefinition($handlerStackServiceName, $handler); 154 | 155 | if ($profiling) { 156 | $this->defineProfileMiddleware($container, $handler, $clientName); 157 | } 158 | 159 | foreach ($this->plugins as $plugin) { 160 | if (isset($options['plugin'][$plugin->getPluginName()])) { 161 | $plugin->loadForClient($options['plugin'][$plugin->getPluginName()], $container, $clientName, $handler); 162 | } 163 | } 164 | 165 | $logMode = $this->convertLogMode($options['logging']); 166 | if ($logMode > Logger::LOG_MODE_NONE) { 167 | $loggerName = $this->defineLogger($container, $logMode, $clientName); 168 | $this->defineLogMiddleware($container, $handler, $clientName, $loggerName); 169 | $this->defineRequestTimeMiddleware($container, $handler, $clientName, $loggerName); 170 | $this->attachSymfonyLogMiddlewareToHandler($handler); 171 | } 172 | 173 | // goes on the end of the stack. 174 | $handler->addMethodCall('unshift', [$eventExpression, 'events']); 175 | 176 | return $handler; 177 | } 178 | 179 | /** 180 | * @param int|bool $logMode 181 | * @return int 182 | */ 183 | private function convertLogMode($logMode) : int 184 | { 185 | if ($logMode === true) { 186 | return Logger::LOG_MODE_REQUEST_AND_RESPONSE; 187 | } elseif ($logMode === false) { 188 | return Logger::LOG_MODE_NONE; 189 | } else { 190 | return $logMode; 191 | } 192 | } 193 | 194 | /** 195 | * @param ContainerBuilder $container 196 | * 197 | * @return void 198 | */ 199 | protected function defineTwigDebugExtension(ContainerBuilder $container) : void 200 | { 201 | $twigDebugExtensionDefinition = new Definition(DebugExtension::class); 202 | $twigDebugExtensionDefinition->addTag('twig.extension'); 203 | $twigDebugExtensionDefinition->setPublic(false); 204 | $container->setDefinition('eight_points_guzzle.twig_extension.debug', $twigDebugExtensionDefinition); 205 | } 206 | 207 | /** 208 | * Define Logger 209 | * 210 | * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container 211 | * 212 | * @throws \Symfony\Component\DependencyInjection\Exception\BadMethodCallException 213 | * 214 | * @return void 215 | */ 216 | protected function defineLogger(ContainerBuilder $container, int $logMode, string $clientName) : string 217 | { 218 | $loggerDefinition = new Definition('%eight_points_guzzle.logger.class%'); 219 | $loggerDefinition->setPublic(false); 220 | $loggerDefinition->setArgument(0, $logMode); 221 | $loggerDefinition->addTag('eight_points_guzzle.logger'); 222 | 223 | $loggerName = sprintf('eight_points_guzzle.%s_logger', $clientName); 224 | $container->setDefinition($loggerName, $loggerDefinition); 225 | 226 | return $loggerName; 227 | } 228 | 229 | /** 230 | * Define Data Collector 231 | * 232 | * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container 233 | * @param float $slowResponseTime 234 | * 235 | * @throws \Symfony\Component\DependencyInjection\Exception\BadMethodCallException 236 | * 237 | * @return void 238 | */ 239 | protected function defineDataCollector(ContainerBuilder $container, float $slowResponseTime) : void 240 | { 241 | $dataCollectorDefinition = new Definition('%eight_points_guzzle.data_collector.class%'); 242 | $dataCollectorDefinition->addArgument(array_map(function($loggerId) : Reference { 243 | return new Reference($loggerId); 244 | }, array_keys($container->findTaggedServiceIds('eight_points_guzzle.logger')))); 245 | 246 | $dataCollectorDefinition->addArgument($slowResponseTime); 247 | $dataCollectorDefinition->setPublic(false); 248 | $dataCollectorDefinition->addTag('data_collector', [ 249 | 'id' => 'eight_points_guzzle', 250 | 'template' => '@EightPointsGuzzle/debug.html.twig', 251 | ]); 252 | $container->setDefinition('eight_points_guzzle.data_collector', $dataCollectorDefinition); 253 | } 254 | 255 | /** 256 | * Define Formatter 257 | * 258 | * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container 259 | * 260 | * @throws \Symfony\Component\DependencyInjection\Exception\BadMethodCallException 261 | * 262 | * @return void 263 | */ 264 | protected function defineFormatter(ContainerBuilder $container) : void 265 | { 266 | $formatterDefinition = new Definition('%eight_points_guzzle.formatter.class%'); 267 | $formatterDefinition->setPublic(true); 268 | $container->setDefinition('eight_points_guzzle.formatter', $formatterDefinition); 269 | } 270 | 271 | /** 272 | * Define Request Time Middleware 273 | * 274 | * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container 275 | * @param \Symfony\Component\DependencyInjection\Definition $handler 276 | * @param string $clientName 277 | * 278 | * @return void 279 | */ 280 | protected function defineRequestTimeMiddleware(ContainerBuilder $container, Definition $handler, string $clientName, string $loggerName) : void 281 | { 282 | $requestTimeMiddlewareDefinitionName = sprintf('eight_points_guzzle.middleware.request_time.%s', $clientName); 283 | $requestTimeMiddlewareDefinition = new Definition('%eight_points_guzzle.middleware.request_time.class%'); 284 | $requestTimeMiddlewareDefinition->addArgument(new Reference($loggerName)); 285 | $requestTimeMiddlewareDefinition->addArgument(new Reference('eight_points_guzzle.data_collector')); 286 | $requestTimeMiddlewareDefinition->setPublic(true); 287 | $container->setDefinition($requestTimeMiddlewareDefinitionName, $requestTimeMiddlewareDefinition); 288 | 289 | $requestTimeExpression = new Expression(sprintf("service('%s')", $requestTimeMiddlewareDefinitionName)); 290 | $handler->addMethodCall('after', ['log', $requestTimeExpression, 'request_time']); 291 | } 292 | 293 | /** 294 | * Define Log Middleware for client 295 | * 296 | * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container 297 | * @param \Symfony\Component\DependencyInjection\Definition $handler 298 | * @param string $clientName 299 | * 300 | * @return void 301 | */ 302 | protected function defineLogMiddleware(ContainerBuilder $container, Definition $handler, string $clientName, string $loggerName) : void 303 | { 304 | $logMiddlewareDefinitionName = sprintf('eight_points_guzzle.middleware.log.%s', $clientName); 305 | $logMiddlewareDefinition = new Definition('%eight_points_guzzle.middleware.log.class%'); 306 | $logMiddlewareDefinition->addArgument(new Reference($loggerName)); 307 | $logMiddlewareDefinition->addArgument(new Reference('eight_points_guzzle.formatter')); 308 | $logMiddlewareDefinition->setPublic(true); 309 | $container->setDefinition($logMiddlewareDefinitionName, $logMiddlewareDefinition); 310 | 311 | $logExpression = new Expression(sprintf("service('%s').log()", $logMiddlewareDefinitionName)); 312 | $handler->addMethodCall('push', [$logExpression, 'log']); 313 | } 314 | 315 | /** 316 | * Define Profile Middleware for client 317 | * 318 | * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container 319 | * @param \Symfony\Component\DependencyInjection\Definition $handler 320 | * @param string $clientName 321 | * 322 | * @return void 323 | */ 324 | protected function defineProfileMiddleware(ContainerBuilder $container, Definition $handler, string $clientName) : void 325 | { 326 | $profileMiddlewareDefinitionName = sprintf('eight_points_guzzle.middleware.profile.%s', $clientName); 327 | $profileMiddlewareDefinition = new Definition('%eight_points_guzzle.middleware.profile.class%'); 328 | $profileMiddlewareDefinition->addArgument(new Reference('debug.stopwatch')); 329 | $profileMiddlewareDefinition->setPublic(true); 330 | $container->setDefinition($profileMiddlewareDefinitionName, $profileMiddlewareDefinition); 331 | 332 | $profileExpression = new Expression(sprintf("service('%s').profile()", $profileMiddlewareDefinitionName)); 333 | $handler->addMethodCall('push', [$profileExpression, 'profile']); 334 | } 335 | 336 | /** 337 | * @param \Symfony\Component\DependencyInjection\Definition $handler 338 | * 339 | * @return void 340 | */ 341 | protected function attachSymfonyLogMiddlewareToHandler(Definition $handler) : void 342 | { 343 | $logExpression = new Expression(sprintf("service('%s')", 'eight_points_guzzle.middleware.symfony_log')); 344 | $handler->addMethodCall('push', [$logExpression, 'symfony_log']); 345 | } 346 | 347 | /** 348 | * Create Middleware For dispatching events 349 | * 350 | * @param string $name 351 | * 352 | * @return \Symfony\Component\DependencyInjection\Definition 353 | */ 354 | protected function createEventMiddleware(string $name) : Definition 355 | { 356 | $eventMiddleWare = new Definition('%eight_points_guzzle.middleware.event_dispatcher.class%'); 357 | $eventMiddleWare->addArgument(new Reference('event_dispatcher')); 358 | $eventMiddleWare->addArgument($name); 359 | $eventMiddleWare->setPublic(true); 360 | 361 | return $eventMiddleWare; 362 | } 363 | 364 | /** 365 | * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container 366 | * 367 | * @return void 368 | */ 369 | protected function defineSymfonyLogFormatter(ContainerBuilder $container) : void 370 | { 371 | $formatterDefinition = new Definition('%eight_points_guzzle.symfony_log_formatter.class%'); 372 | $formatterDefinition->setArguments(['%eight_points_guzzle.symfony_log_formatter.pattern%']); 373 | $formatterDefinition->setPublic(true); 374 | $container->setDefinition('eight_points_guzzle.symfony_log_formatter', $formatterDefinition); 375 | } 376 | 377 | /** 378 | * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container 379 | * 380 | * @return void 381 | */ 382 | protected function defineSymfonyLogMiddleware(ContainerBuilder $container) : void 383 | { 384 | $logMiddlewareDefinition = new Definition('%eight_points_guzzle.middleware.symfony_log.class%'); 385 | $logMiddlewareDefinition->addArgument(new Reference('logger')); 386 | $logMiddlewareDefinition->addArgument(new Reference('eight_points_guzzle.symfony_log_formatter')); 387 | $logMiddlewareDefinition->setPublic(true); 388 | $logMiddlewareDefinition->addTag('monolog.logger', ['channel' => 'eight_points_guzzle']); 389 | $container->setDefinition('eight_points_guzzle.middleware.symfony_log', $logMiddlewareDefinition); 390 | } 391 | 392 | /** 393 | * Returns alias of extension 394 | * 395 | * @return string 396 | */ 397 | public function getAlias() : string 398 | { 399 | return 'eight_points_guzzle'; 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /src/EightPointsGuzzleBundle.php: -------------------------------------------------------------------------------- 1 | registerPlugin($plugin); 23 | } 24 | } 25 | 26 | /** 27 | * Build EightPointsGuzzleBundle 28 | * 29 | * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container 30 | * 31 | * @return void 32 | */ 33 | public function build(ContainerBuilder $container) 34 | { 35 | parent::build($container); 36 | 37 | foreach ($this->plugins as $plugin) { 38 | $plugin->build($container); 39 | } 40 | } 41 | 42 | /** 43 | * Overwrite getContainerExtension 44 | * - no naming convention of alias needed 45 | * - extension class can be moved easily now 46 | * 47 | * @return \Symfony\Component\DependencyInjection\Extension\ExtensionInterface The container extension 48 | */ 49 | public function getContainerExtension() : ExtensionInterface 50 | { 51 | if ($this->extension === null) { 52 | $this->extension = new EightPointsGuzzleExtension($this->plugins); 53 | } 54 | 55 | return $this->extension; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | * 61 | * @return void 62 | */ 63 | public function boot(): void 64 | { 65 | foreach ($this->plugins as $plugin) { 66 | $plugin->boot(); 67 | } 68 | } 69 | 70 | /** 71 | * @param \EightPoints\Bundle\GuzzleBundle\PluginInterface $plugin 72 | * 73 | * @throws \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException 74 | * 75 | * @return void 76 | */ 77 | protected function registerPlugin(PluginInterface $plugin) : void 78 | { 79 | // Check plugins name duplication 80 | foreach ($this->plugins as $registeredPlugin) { 81 | if ($registeredPlugin->getPluginName() === $plugin->getPluginName()) { 82 | throw new InvalidConfigurationException(sprintf( 83 | 'Trying to connect two plugins with same name: %s', 84 | $plugin->getPluginName() 85 | )); 86 | } 87 | } 88 | 89 | $this->plugins[] = $plugin; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Events/Event.php: -------------------------------------------------------------------------------- 1 | response = $response; 22 | $this->serviceName = $serviceName; 23 | } 24 | 25 | /** 26 | * Get the transaction from the event. 27 | * 28 | * This returns the transaction we are working with. 29 | * 30 | * @return \Psr\Http\Message\ResponseInterface|null 31 | */ 32 | public function getTransaction() : ?ResponseInterface 33 | { 34 | return $this->response; 35 | } 36 | 37 | /** 38 | * Sets the transaction inline with the event. 39 | * 40 | * @param \Psr\Http\Message\ResponseInterface|null $response 41 | * 42 | * @return void 43 | */ 44 | public function setTransaction(?ResponseInterface $response) : void 45 | { 46 | $this->response = $response; 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function getServiceName() : string 53 | { 54 | return $this->serviceName; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Events/PreTransactionEvent.php: -------------------------------------------------------------------------------- 1 | requestTransaction = $requestTransaction; 22 | $this->serviceName = $serviceName; 23 | } 24 | 25 | /** 26 | * Access the transaction from the Guzzle HTTP request 27 | * 28 | * This returns the actual Request Object from the Guzzle HTTP Request. 29 | * This object will be modified by the event listener. 30 | * 31 | * @return \Psr\Http\Message\RequestInterface 32 | */ 33 | public function getTransaction() : RequestInterface 34 | { 35 | return $this->requestTransaction; 36 | } 37 | 38 | /** 39 | * Replaces the transaction with the modified one. 40 | * 41 | * Guzzles transaction returns a modified request object, 42 | * so once it has been modified, we need to put it back on the 43 | * event so it can become part of the transaction. 44 | * 45 | * @param \Psr\Http\Message\RequestInterface $requestTransaction 46 | * 47 | * @return void 48 | */ 49 | public function setTransaction(RequestInterface $requestTransaction) : void 50 | { 51 | $this->requestTransaction = $requestTransaction; 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getServiceName() : string 58 | { 59 | return $this->serviceName; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Log/DevNullLogger.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class DevNullLogger implements LoggerInterface 11 | { 12 | use LoggerTrait; 13 | 14 | /** 15 | * {@inheritdoc} 16 | */ 17 | public function log($level, $message, array $context = []): void 18 | { 19 | // do nothing!! 20 | } 21 | 22 | /** 23 | * Clear messages list 24 | * 25 | * @return void 26 | */ 27 | public function clear() : void 28 | { 29 | // do nothing!! 30 | } 31 | 32 | /** 33 | * Return if messages exist or not 34 | * 35 | * @return boolean 36 | */ 37 | public function hasMessages() : bool 38 | { 39 | return false; 40 | } 41 | 42 | /** 43 | * Return log messages 44 | * 45 | * @return array 46 | */ 47 | public function getMessages() : array 48 | { 49 | return []; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Log/LogGroup.php: -------------------------------------------------------------------------------- 1 | requestName = $value; 23 | } 24 | 25 | /** 26 | * Get Request Name 27 | * 28 | * @return string 29 | */ 30 | public function getRequestName() : ?string 31 | { 32 | return $this->requestName; 33 | } 34 | 35 | /** 36 | * Set Log Messages 37 | * 38 | * @param array $value 39 | * 40 | * @return void 41 | */ 42 | public function setMessages(array $value) : void 43 | { 44 | $this->messages = $value; 45 | } 46 | 47 | /** 48 | * Add Log Messages 49 | * 50 | * @param array $value 51 | * 52 | * @return void 53 | */ 54 | public function addMessages(array $value) : void 55 | { 56 | $this->messages = array_merge($this->messages, $value); 57 | } 58 | 59 | /** 60 | * Return Log Messages 61 | * 62 | * @return array 63 | */ 64 | public function getMessages() : array 65 | { 66 | return $this->messages; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Log/LogMessage.php: -------------------------------------------------------------------------------- 1 | message = $message; 31 | } 32 | 33 | /** 34 | * Set log level 35 | * 36 | * @param string $level 37 | * 38 | * @return void 39 | */ 40 | public function setLevel($level) : void 41 | { 42 | $this->level = $level; 43 | } 44 | 45 | /** 46 | * Returning log level 47 | * 48 | * @return string 49 | */ 50 | public function getLevel() 51 | { 52 | return $this->level; 53 | } 54 | 55 | /** 56 | * Returning log message 57 | * 58 | * @return string 59 | */ 60 | public function getMessage() 61 | { 62 | return $this->message; 63 | } 64 | 65 | /** 66 | * Set Log Request 67 | * 68 | * @param \EightPoints\Bundle\GuzzleBundle\Log\LogRequest $value 69 | * 70 | * @return void 71 | */ 72 | public function setRequest(LogRequest $value) : void 73 | { 74 | $this->request = $value; 75 | } 76 | 77 | /** 78 | * Get Log Request 79 | * 80 | * @return \EightPoints\Bundle\GuzzleBundle\Log\LogRequest 81 | */ 82 | public function getRequest() 83 | { 84 | return $this->request; 85 | } 86 | 87 | /** 88 | * Set Log Response 89 | * 90 | * @param \EightPoints\Bundle\GuzzleBundle\Log\LogResponse $value 91 | * 92 | * @return void 93 | */ 94 | public function setResponse(LogResponse $value) 95 | { 96 | $this->response = $value; 97 | } 98 | 99 | /** 100 | * Get Log Response 101 | * 102 | * @return \EightPoints\Bundle\GuzzleBundle\Log\LogResponse 103 | */ 104 | public function getResponse() 105 | { 106 | return $this->response; 107 | } 108 | 109 | /** 110 | * @return float|null 111 | */ 112 | public function getTransferTime() 113 | { 114 | return $this->transferTime; 115 | } 116 | 117 | /** 118 | * @param float|null $transferTime 119 | * 120 | * @return void 121 | */ 122 | public function setTransferTime($transferTime) : void 123 | { 124 | $this->transferTime = $transferTime; 125 | } 126 | 127 | /** 128 | * @return null|string 129 | */ 130 | public function getCurlCommand() 131 | { 132 | return $this->curlCommand; 133 | } 134 | 135 | /** 136 | * @param string $curlCommand 137 | * 138 | * @return void 139 | */ 140 | public function setCurlCommand($curlCommand) : void 141 | { 142 | $this->curlCommand = $curlCommand; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Log/LogRequest.php: -------------------------------------------------------------------------------- 1 | save($request); 42 | } 43 | 44 | /** 45 | * Save data 46 | * 47 | * @param \Psr\Http\Message\RequestInterface $request 48 | * 49 | * @return void 50 | */ 51 | protected function save(RequestInterface $request) : void 52 | { 53 | $uri = $request->getUri(); 54 | 55 | $this->setHost($uri->getHost()); 56 | $this->setPort($uri->getPort()); 57 | $this->setUrl((string) $uri); 58 | $this->setPath($uri->getPath()); 59 | $this->setScheme($uri->getScheme()); 60 | $this->setHeaders($request->getHeaders()); 61 | $this->setProtocolVersion($request->getProtocolVersion()); 62 | $this->setMethod($request->getMethod()); 63 | 64 | // rewind to previous position after logging request 65 | $readPosition = null; 66 | if ($request->getBody() && $request->getBody()->isSeekable()) { 67 | $readPosition = $request->getBody()->tell(); 68 | } 69 | 70 | $this->setBody($request->getBody() ? $request->getBody()->__toString() : null); 71 | 72 | if ($readPosition !== null) { 73 | $request->getBody()->seek($readPosition); 74 | } 75 | } 76 | 77 | /** 78 | * Return host 79 | * 80 | * @return string 81 | */ 82 | public function getHost() : string 83 | { 84 | return $this->host; 85 | } 86 | 87 | /** 88 | * Set request host 89 | * 90 | * @param string $value 91 | * 92 | * @return void 93 | */ 94 | public function setHost(string $value) : void 95 | { 96 | $this->host = $value; 97 | } 98 | 99 | /** 100 | * Return port 101 | * 102 | * @return integer|null 103 | */ 104 | public function getPort() : ?int 105 | { 106 | return $this->port; 107 | } 108 | 109 | /** 110 | * Set port 111 | * 112 | * @param integer|null $value 113 | * 114 | * @return void 115 | */ 116 | public function setPort(?int $value): void 117 | { 118 | $this->port = $value; 119 | } 120 | 121 | /** 122 | * Return url 123 | * 124 | * @return string 125 | */ 126 | public function getUrl() : string 127 | { 128 | return $this->url; 129 | } 130 | 131 | /** 132 | * Set url 133 | * 134 | * @param string $value 135 | * 136 | * @return void 137 | */ 138 | public function setUrl(string $value) : void 139 | { 140 | $this->url = $value; 141 | } 142 | 143 | /** 144 | * Return path 145 | * 146 | * @return string 147 | */ 148 | public function getPath() : string 149 | { 150 | return $this->path; 151 | } 152 | 153 | /** 154 | * Set path 155 | * 156 | * @param string $value 157 | * 158 | * @return void 159 | */ 160 | public function setPath(string $value) : void 161 | { 162 | $this->path = $value; 163 | } 164 | 165 | /** 166 | * Return scheme 167 | * 168 | * @return string 169 | */ 170 | public function getScheme() : string 171 | { 172 | return $this->scheme; 173 | } 174 | 175 | /** 176 | * Set scheme 177 | * 178 | * @param string $value 179 | * 180 | * @return void 181 | */ 182 | public function setScheme(string $value) : void 183 | { 184 | $this->scheme = $value; 185 | } 186 | 187 | /** 188 | * Return headers 189 | * 190 | * @return array 191 | */ 192 | public function getHeaders() : array 193 | { 194 | return $this->headers; 195 | } 196 | 197 | /** 198 | * Set headers 199 | * 200 | * @param array $value 201 | * 202 | * @return void 203 | */ 204 | public function setHeaders(array $value) : void 205 | { 206 | $this->headers = $value; 207 | } 208 | 209 | /** 210 | * Return protocol version 211 | * 212 | * @return string 213 | */ 214 | public function getProtocolVersion() : string 215 | { 216 | return $this->protocolVersion; 217 | } 218 | 219 | /** 220 | * Set protocol version 221 | * 222 | * @param string $value 223 | * 224 | * @return void 225 | */ 226 | public function setProtocolVersion(string $value) : void 227 | { 228 | $this->protocolVersion = $value; 229 | } 230 | 231 | /** 232 | * Return method 233 | * 234 | * @return string 235 | */ 236 | public function getMethod() : string 237 | { 238 | return $this->method; 239 | } 240 | 241 | /** 242 | * Set method 243 | * 244 | * @param string $value 245 | * 246 | * @return void 247 | */ 248 | public function setMethod(string $value) : void 249 | { 250 | $this->method = $value; 251 | } 252 | 253 | /** 254 | * Return body 255 | * 256 | * @return string|null 257 | */ 258 | public function getBody() : ?string 259 | { 260 | return $this->body; 261 | } 262 | 263 | /** 264 | * Set body 265 | * 266 | * @param string|null $value 267 | * 268 | * @return void 269 | */ 270 | public function setBody(?string $value) : void 271 | { 272 | $this->body = $value; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/Log/LogResponse.php: -------------------------------------------------------------------------------- 1 | logBody = $logBody; 35 | $this->save($response); 36 | } 37 | 38 | /** 39 | * Save data 40 | * 41 | * @param \Psr\Http\Message\ResponseInterface $response 42 | * 43 | * @return void 44 | */ 45 | public function save(ResponseInterface $response) : void 46 | { 47 | $this->setStatusCode($response->getStatusCode()); 48 | $this->setStatusPhrase($response->getReasonPhrase()); 49 | 50 | $this->setHeaders($response->getHeaders()); 51 | $this->setProtocolVersion($response->getProtocolVersion()); 52 | 53 | if ($this->logBody) { 54 | $this->setBody($response->getBody()->getContents()); 55 | 56 | // rewind to previous position after reading response body 57 | if ($response->getBody()->isSeekable()) { 58 | $response->getBody()->rewind(); 59 | } 60 | } else { 61 | $this->setBody(EightPointsGuzzleBundle::class . ': [response body log disabled]'); 62 | } 63 | } 64 | 65 | /** 66 | * Return HTTP status code 67 | * 68 | * @return integer 69 | */ 70 | public function getStatusCode() : int 71 | { 72 | return $this->statusCode; 73 | } 74 | 75 | /** 76 | * Set HTTP status code 77 | * 78 | * @param integer $value 79 | * 80 | * @return void 81 | */ 82 | public function setStatusCode(int $value) : void 83 | { 84 | $this->statusCode = $value; 85 | } 86 | 87 | /** 88 | * Return HTTP status phrase 89 | * 90 | * @return string 91 | */ 92 | public function getStatusPhrase() : string 93 | { 94 | return $this->statusPhrase; 95 | } 96 | 97 | /** 98 | * Set HTTP status phrase 99 | * 100 | * @param string $value 101 | * 102 | * @return void 103 | */ 104 | public function setStatusPhrase(string $value) : void 105 | { 106 | $this->statusPhrase = $value; 107 | } 108 | 109 | /** 110 | * Return response body 111 | * 112 | * @return string 113 | */ 114 | public function getBody() 115 | { 116 | return $this->body; 117 | } 118 | 119 | /** 120 | * Set response body 121 | * 122 | * @param string $value 123 | * 124 | * @return void 125 | */ 126 | public function setBody(string $value) : void 127 | { 128 | $this->body = $value; 129 | } 130 | 131 | /** 132 | * Return protocol version 133 | * 134 | * @return string 135 | */ 136 | public function getProtocolVersion() : string 137 | { 138 | return $this->protocolVersion; 139 | } 140 | 141 | /** 142 | * Set protocol version 143 | * 144 | * @param string $value 145 | * 146 | * @return void 147 | */ 148 | public function setProtocolVersion(string $value) : void 149 | { 150 | $this->protocolVersion = $value; 151 | } 152 | 153 | /** 154 | * Return response headers 155 | * 156 | * @return array 157 | */ 158 | public function getHeaders() : array 159 | { 160 | return $this->headers; 161 | } 162 | 163 | /** 164 | * Set response headers 165 | * 166 | * @param array $value 167 | * 168 | * @return void 169 | */ 170 | public function setHeaders(array $value) : void 171 | { 172 | $this->headers = $value; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Log/Logger.php: -------------------------------------------------------------------------------- 1 | logMode = $logMode; 26 | } 27 | 28 | /** 29 | * Log message 30 | * 31 | * @param string $level 32 | * @param string $message 33 | * @param array $context 34 | * 35 | * @return void 36 | */ 37 | public function log($level, $message, array $context = []): void 38 | { 39 | $requestId = isset($context['requestId']) ? $context['requestId'] : uniqid('eight_points_guzzle_'); 40 | 41 | if (array_key_exists($requestId, $this->messages)) { 42 | $logMessage = $this->messages[$requestId]; 43 | } else { 44 | $logMessage = new LogMessage($message); 45 | } 46 | 47 | $logMessage->setLevel($level); 48 | 49 | if (!empty($context)) { 50 | if (!empty($context['request']) && $this->logMode > self::LOG_MODE_NONE) { 51 | $logMessage->setRequest(new LogRequest($context['request'])); 52 | 53 | if (class_exists(CurlFormatter::class)) { 54 | $logMessage->setCurlCommand((new CurlFormatter())->format($context['request'])); 55 | } 56 | } 57 | 58 | if (!empty($context['response']) && $this->logMode > self::LOG_MODE_REQUEST) { 59 | $logMessage->setResponse(new LogResponse( 60 | $context['response'], 61 | $this->logMode > self::LOG_MODE_REQUEST_AND_RESPONSE_HEADERS 62 | )); 63 | } 64 | } 65 | 66 | $this->messages[$requestId] = $logMessage; 67 | } 68 | 69 | /** 70 | * Clear messages list 71 | * 72 | * @return void 73 | */ 74 | public function clear() : void 75 | { 76 | $this->messages = []; 77 | } 78 | 79 | /** 80 | * Return if messages exist or not 81 | * 82 | * @return boolean 83 | */ 84 | public function hasMessages() : bool 85 | { 86 | return $this->getMessages() ? true : false; 87 | } 88 | 89 | /** 90 | * Return log messages 91 | * 92 | * @return \EightPoints\Bundle\GuzzleBundle\Log\LogMessage[] 93 | */ 94 | public function getMessages() : array 95 | { 96 | return $this->messages; 97 | } 98 | 99 | /** 100 | * @param string|null $requestId 101 | * @param float $transferTime 102 | * 103 | * @return void 104 | */ 105 | public function addTransferTimeByRequestId(?string $requestId, float $transferTime) : void 106 | { 107 | if (array_key_exists($requestId, $this->messages)) { 108 | $this->messages[$requestId]->setTransferTime($transferTime); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Log/LoggerInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface LoggerInterface extends PsrLoggerInterface 11 | { 12 | /** 13 | * Clear messages list 14 | * 15 | * @return void 16 | */ 17 | public function clear() : void; 18 | 19 | /** 20 | * Return if messages exist or not 21 | * 22 | * @return boolean 23 | */ 24 | public function hasMessages() : bool; 25 | 26 | /** 27 | * Return log messages 28 | * 29 | * @return array 30 | */ 31 | public function getMessages() : array; 32 | } 33 | -------------------------------------------------------------------------------- /src/Middleware/EventDispatchMiddleware.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher; 36 | $this->serviceName = $serviceName; 37 | } 38 | 39 | /** 40 | * @return \Closure 41 | */ 42 | public function dispatchEvent() : \Closure 43 | { 44 | return function (callable $handler) { 45 | 46 | return function ( 47 | RequestInterface $request, 48 | array $options 49 | ) use ($handler) { 50 | // Create the Pre Transaction event. 51 | $preTransactionEvent = new PreTransactionEvent($request, $this->serviceName); 52 | 53 | // Dispatch it through the symfony Dispatcher. 54 | $this->doDispatch($preTransactionEvent, GuzzleEvents::PRE_TRANSACTION); 55 | $this->doDispatch($preTransactionEvent, GuzzleEvents::preTransactionFor($this->serviceName)); 56 | 57 | // Continue the handler chain. 58 | $promise = $handler($preTransactionEvent->getTransaction(), $options); 59 | 60 | // Handle the response form the server. 61 | return $promise->then( 62 | function (ResponseInterface $response) { 63 | // Create the Post Transaction event. 64 | $postTransactionEvent = new PostTransactionEvent($response, $this->serviceName); 65 | 66 | // Dispatch the event on the symfony event dispatcher. 67 | $this->doDispatch($postTransactionEvent, GuzzleEvents::POST_TRANSACTION); 68 | $this->doDispatch($postTransactionEvent, GuzzleEvents::postTransactionFor($this->serviceName)); 69 | 70 | // Continue down the chain. 71 | return $postTransactionEvent->getTransaction(); 72 | }, 73 | function (Throwable $reason) { 74 | // Get the response. The response in a RequestException can be null too. 75 | $response = $reason instanceof RequestException ? $reason->getResponse() : null; 76 | 77 | // Create the Post Transaction event. 78 | $postTransactionEvent = new PostTransactionEvent($response, $this->serviceName); 79 | 80 | // Dispatch the event on the symfony event dispatcher. 81 | $this->doDispatch($postTransactionEvent, GuzzleEvents::POST_TRANSACTION); 82 | $this->doDispatch($postTransactionEvent, GuzzleEvents::postTransactionFor($this->serviceName)); 83 | 84 | // Continue down the chain. 85 | return \GuzzleHttp\Promise\Create::rejectionFor($reason); 86 | } 87 | ); 88 | }; 89 | }; 90 | } 91 | 92 | private function doDispatch(Event $event, string $name): void 93 | { 94 | if ($this->eventDispatcher instanceof ContractsEventDispatcherInterface) { 95 | $this->eventDispatcher->dispatch($event, $name); 96 | 97 | return; 98 | } 99 | 100 | // BC compatibility 101 | $this->eventDispatcher->dispatch($name, $event); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Middleware/LogMiddleware.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 24 | $this->formatter = $formatter; 25 | } 26 | 27 | /** 28 | * Logging each Request 29 | * 30 | * @return \Closure 31 | */ 32 | public function log() : \Closure 33 | { 34 | $logger = $this->logger; 35 | $formatter = $this->formatter; 36 | 37 | return function (callable $handler) use ($logger, $formatter) { 38 | 39 | return function ($request, array $options) use ($handler, $logger, $formatter) { 40 | // generate id that will be used to supplement the log with information 41 | $requestId = uniqid('eight_points_guzzle_'); 42 | 43 | // initial registration of log 44 | $logger->info('', compact('request', 'requestId')); 45 | 46 | // this id will be used by RequestTimeMiddleware 47 | $options['request_id'] = $requestId; 48 | 49 | return $handler($request, $options)->then( 50 | 51 | function ($response) use ($logger, $request, $formatter, $requestId) { 52 | 53 | $message = $formatter->format($request, $response); 54 | $context = compact('request', 'response', 'requestId'); 55 | 56 | $logger->info($message, $context); 57 | 58 | return $response; 59 | }, 60 | 61 | function ($reason) use ($logger, $request, $formatter, $requestId) { 62 | 63 | $response = $reason instanceof RequestException ? $reason->getResponse() : null; 64 | $message = $formatter->format($request, $response, $reason); 65 | $context = compact('request', 'response', 'requestId'); 66 | 67 | $logger->notice($message, $context); 68 | 69 | return \GuzzleHttp\Promise\Create::rejectionFor($reason); 70 | } 71 | ); 72 | }; 73 | }; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Middleware/ProfileMiddleware.php: -------------------------------------------------------------------------------- 1 | stopwatch = $stopwatch; 23 | } 24 | 25 | /** 26 | * Profiling each Request 27 | * 28 | * @return \Closure 29 | */ 30 | public function profile() : \Closure 31 | { 32 | $stopwatch = $this->stopwatch; 33 | 34 | return function (callable $handler) use ($stopwatch) { 35 | 36 | return function ($request, array $options) use ($handler, $stopwatch) { 37 | $event = $stopwatch->start( 38 | sprintf('%s %s', $request->getMethod(), $request->getUri()), 39 | 'eight_points_guzzle' 40 | ); 41 | 42 | return $handler($request, $options)->then( 43 | 44 | function ($response) use ($event) { 45 | $event->stop(); 46 | 47 | return $response; 48 | }, 49 | 50 | function ($reason) use ($event) { 51 | $event->stop(); 52 | 53 | return \GuzzleHttp\Promise\Create::rejectionFor($reason); 54 | } 55 | ); 56 | }; 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Middleware/RequestTimeMiddleware.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 26 | $this->dataCollector = $dataCollector; 27 | } 28 | 29 | /** 30 | * @param callable $handler 31 | * 32 | * @return \Closure 33 | */ 34 | public function __invoke(callable $handler) : \Closure 35 | { 36 | return function (RequestInterface $request, array $options) use ($handler) { 37 | $options['on_stats'] = $this->getOnStatsCallback( 38 | isset($options['on_stats']) ? $options['on_stats'] : null, 39 | isset($options['request_id']) ? $options['request_id'] : null 40 | ); 41 | 42 | // Continue the handler chain. 43 | return $handler($request, $options); 44 | }; 45 | } 46 | 47 | /** 48 | * Create callback for on_stats options. 49 | * If request has on_stats option, it will be called inside of this callback. 50 | * 51 | * @param null|callable $initialOnStats 52 | * @param null|string $requestId 53 | * 54 | * @return \Closure 55 | */ 56 | protected function getOnStatsCallback(?callable $initialOnStats, ?string $requestId) : \Closure 57 | { 58 | return function (TransferStats $stats) use ($initialOnStats, $requestId) { 59 | if (is_callable($initialOnStats)) { 60 | call_user_func($initialOnStats, $stats); 61 | } 62 | 63 | $this->dataCollector->addTotalTime((float)$stats->getTransferTime()); 64 | 65 | if (($this->logger instanceof Logger) && $requestId) { 66 | $this->logger->addTransferTimeByRequestId($requestId, (float)$stats->getTransferTime()); 67 | } 68 | }; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Middleware/SymfonyLogMiddleware.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 24 | $this->formatter = $formatter; 25 | } 26 | 27 | /** 28 | * @param callable $handler 29 | * 30 | * @return \Closure 31 | */ 32 | public function __invoke(callable $handler) : \Closure 33 | { 34 | $logger = $this->logger; 35 | $formatter = $this->formatter; 36 | 37 | return function ($request, array $options) use ($handler, $logger, $formatter) { 38 | 39 | return $handler($request, $options)->then( 40 | 41 | function ($response) use ($logger, $request, $formatter) { 42 | $message = $formatter->format($request, $response); 43 | 44 | $logger->info($message); 45 | 46 | return $response; 47 | }, 48 | 49 | function ($reason) use ($logger, $request, $formatter) { 50 | $response = $reason instanceof RequestException ? $reason->getResponse() : null; 51 | $message = $formatter->format($request, $response, $reason); 52 | 53 | $logger->notice($message); 54 | 55 | return \GuzzleHttp\Promise\Create::rejectionFor($reason); 56 | } 57 | ); 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/PluginInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | GuzzleHttp\Client 9 | GuzzleHttp\MessageFormatter 10 | GuzzleHttp\MessageFormatter 11 | EightPoints\Bundle\GuzzleBundle\DataCollector\HttpDataCollector 12 | EightPoints\Bundle\GuzzleBundle\Log\Logger 13 | 14 | 15 | EightPoints\Bundle\GuzzleBundle\Middleware\LogMiddleware 16 | EightPoints\Bundle\GuzzleBundle\Middleware\ProfileMiddleware 17 | EightPoints\Bundle\GuzzleBundle\Middleware\EventDispatchMiddleware 18 | EightPoints\Bundle\GuzzleBundle\Middleware\RequestTimeMiddleware 19 | EightPoints\Bundle\GuzzleBundle\Middleware\SymfonyLogMiddleware 20 | 21 | 22 | {method} {uri} {code} 23 | 24 | 25 | GuzzleHttp\Middleware 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Resources/doc/autowiring-clients.md: -------------------------------------------------------------------------------- 1 | # Autowiring Clients 2 | 3 | Autowiring was introduced in Symfony 3.3 and let's read how [Symfony Documentation][1] describes it: 4 | 5 | > Autowiring allows you to manage services in the container with minimal configuration. It reads the type-hints on your constructor (or other methods) and automatically passes the correct services to each method. Symfony's autowiring is designed to be predictable: if it is not absolutely clear which dependency should be passed, you'll see an actionable exception. 6 | 7 | ## Symfony >= 4.2 8 | In Symfony 4.2, it is made possible to [bind services by type and name](https://symfony.com/blog/new-in-symfony-4-2-autowiring-by-type-and-name). This feature makes using Guzzle clients a lot easier. Given the following configuration: 9 | 10 | ```yaml 11 | eight_points_guzzle: 12 | clients: 13 | api_payment: 14 | base_url: "http://api.domain1.tld" 15 | api_crm: 16 | class: App\Client\ApiCrmClient 17 | base_url: "http://api.domain2.tld" 18 | ``` 19 | The clients can be autowired without further configuration (but mandatory variable names), like this: 20 | 21 | ```php 22 | namespace App\Controller; 23 | 24 | use GuzzleHttp\ClientInterface; 25 | use App\Client\ApiCrmClient; 26 | 27 | class FooController extends AbstractController 28 | { 29 | public function bar(ClientInterface $apiPaymentClient) 30 | { 31 | // Default Client class 32 | } 33 | 34 | public function baz(ApiCrmClient $apiCrmClient) 35 | { 36 | // Custom Client class (must extend GuzzleHttp\Client) 37 | } 38 | } 39 | ``` 40 | 41 | Autowiring takes place by the combination of the class name and the variable name, as described in [this blog]. 42 | 43 | ## Symfony < 4.2 44 | 45 | Getting in consideration, that Guzzle Bundle creates clients of same class, it becomes obvious that Symfony will not be able to guess what to inject. 46 | With some small configurations we can help Symfony to do it. 47 | 48 | For example you have configured next client: 49 | 50 | ```yaml 51 | eight_points_guzzle: 52 | clients: 53 | api_payment: 54 | base_url: "http://api.domain.tld" 55 | ``` 56 | 57 | By default, Guzzle Bundle uses `GuzzleHttp\Client` class but we have to use another one. 58 | For example, let's create file `ApiPaymentClient.php` in folder `src/Client`: 59 | 60 | ```php 61 | namespace App\Client; 62 | 63 | use GuzzleHttp\Client; 64 | 65 | class ApiPaymentClient extends Client 66 | { 67 | 68 | } 69 | ``` 70 | 71 | Configure Guzzle Bundle to use this class for `api_payment` client: 72 | 73 | ```yaml 74 | eight_points_guzzle: 75 | clients: 76 | api_payment: 77 | class: App\Client\ApiPaymentClient 78 | base_url: "http://api.domain.tld" 79 | ``` 80 | 81 | Forbid to use classes from `src/Client` as services in `config/services.yaml` file: 82 | 83 | ```diff 84 | services: 85 | App\: 86 | resource: '../src/*' 87 | - exclude: '../src/{Entity,Migrations,Tests,Kernel.php}' 88 | + exclude: '../src/{Entity,Migrations,Tests,Kernel.php,Client}' 89 | ``` 90 | *Note: Guzzle Bundle will create services with these classes. DI system do not need to do this.* 91 | 92 | Link client created by Guzzle Bundle with class on the level of DI: 93 | 94 | ```yaml 95 | services: 96 | # ... 97 | 98 | App\Client\ApiPaymentClient: '@eight_points_guzzle.client.api_payment' 99 | ``` 100 | 101 | Use it anywhere: 102 | 103 | ```php 104 | namespace App\Controller; 105 | 106 | use App\Client\ApiPaymentClient; 107 | 108 | class FooController extends AbstractController 109 | { 110 | /** 111 | * @param ApiPaymentClient $client 112 | */ 113 | public function bar(ApiPaymentClient $client) 114 | { 115 | 116 | } 117 | } 118 | ``` 119 | 120 | Note that: 121 | - this flow should be repeated for each client. 122 | - don't use the same client class for more than one client 123 | 124 | That's all. 125 | 126 | [1]: https://symfony.com/doc/current/service_container/autowiring.html 127 | -------------------------------------------------------------------------------- /src/Resources/doc/configuration-reference.md: -------------------------------------------------------------------------------- 1 | # Configuration Reference 2 | 3 | ##### Minimal Configuration 4 | 5 | ```yaml 6 | eight_points_guzzle: 7 | clients: 8 | api_payment: ~ 9 | ``` 10 | 11 | ##### Full Configuration 12 | 13 | ```yaml 14 | eight_points_guzzle: 15 | # (de)activate logging; default: %kernel.debug% 16 | logging: true 17 | 18 | # (de)activate profiler; default: %kernel.debug% 19 | profiling: true 20 | 21 | # configure when a response is considered to be slow (in ms); default 0 (disabled) 22 | slow_response_time: 500 23 | 24 | clients: 25 | api_payment: 26 | base_url: "http://api.domain.tld" 27 | 28 | # Read more here: https://github.com/8p/EightPointsGuzzleBundle/blob/master/src/Resources/doc/redefine-client-class.md 29 | class: 'Namespace\Of\Your\Client\AwesomeClient' 30 | 31 | # NOTE: This option makes Guzzle Client as lazy (https://symfony.com/doc/master/service_container/lazy_services.html) 32 | lazy: true # Default `false` 33 | 34 | # Allows to configure logging mode on a specific client 35 | logging: null # Default null, possible values: null (global settings), true, false, request, request_and_response_headers 36 | 37 | # Handler class to be used for the client 38 | handler: 'GuzzleHttp\Handler\MockHandler' 39 | 40 | # guzzle client options (full description here: https://guzzle.readthedocs.org/en/latest/request-options.html) 41 | options: 42 | auth: 43 | - acme # login 44 | - pa55w0rd # password 45 | 46 | headers: 47 | Accept: "application/json" 48 | 49 | # Find proper php const, for example CURLOPT_SSLVERSION, remove CURLOPT_ and transform to lower case. 50 | # List of curl options: http://php.net/manual/en/function.curl-setopt.php 51 | curl: 52 | sslversion: 1 # or !php/const:CURL_HTTP_VERSION_1_0 for symfony >= 3.2 53 | 54 | timeout: 30 55 | 56 | connect_timeout: 3.14 57 | 58 | allow_redirects: true 59 | 60 | query: 61 | foo: bar 62 | 63 | debug: true 64 | 65 | decode_content: true 66 | 67 | delay: 1000 68 | 69 | http_errors: true 70 | 71 | synchronous: false 72 | 73 | verify: true 74 | 75 | version: 1.1 76 | 77 | cert: ['/path/server.pem', 'password'] 78 | 79 | form_params: 80 | foo: 'bar' 81 | baz: ['hi', 'there!'] 82 | 83 | multipart: 84 | - name: 'foo' 85 | contents: 'data' 86 | headers: 87 | 'X-Baz' => 'bar' 88 | 89 | sink: '/path/to/file' 90 | 91 | expect: 1048576 92 | 93 | ssl_key: '/path/to/file' 94 | 95 | stream: false 96 | 97 | cookies: false 98 | 99 | proxy: 'tcp://localhost:8125' 100 | 101 | # plugin settings 102 | plugin: 103 | # More information: https://packagist.org/packages/gregurco/guzzle-bundle-oauth2-plugin 104 | oauth2: 105 | base_uri: "https://example.com" 106 | token_url: "/oauth/token" 107 | client_id: "test-client-id" 108 | client_secret: "test-client-secret" # optional 109 | scope: "administration" 110 | 111 | # More information: https://packagist.org/packages/gregurco/guzzle-bundle-cache-plugin 112 | cache: 113 | enabled: true 114 | 115 | # More information: https://packagist.org/packages/gregurco/guzzle-bundle-wsse-plugin 116 | wsse: 117 | username: "acme" 118 | password: "pa55w0rd" 119 | created_at: "-10 seconds" # optional 120 | 121 | # More information: https://packagist.org/packages/neirda24/guzzle-bundle-header-forward-plugin 122 | header_forward: 123 | enabled: true 124 | headers: 125 | - 'Accept-Language' 126 | 127 | # More information: https://packagist.org/packages/neirda24/guzzle-bundle-header-disable-cache-plugin 128 | header_disable_cache: 129 | enabled: true 130 | header: 'X-Guzzle-Skip-Cache' # Optional 131 | ``` 132 | 133 | Description for all options and examples of parameters can be found [here][1]. 134 | 135 | 136 | 137 | [1]: http://docs.guzzlephp.org/en/latest/request-options.html 138 | -------------------------------------------------------------------------------- /src/Resources/doc/disable-exception-on-http-error.md: -------------------------------------------------------------------------------- 1 | # Disable throwing exceptions on HTTP errors 2 | 3 | For example you are having configured next client: 4 | 5 | ```yaml 6 | eight_points_guzzle: 7 | clients: 8 | api_payment: 9 | base_url: "http://api.domain.tld" 10 | ``` 11 | 12 | and you are doing request, but exception is thrown in case when API returns, for example, response with code 400. 13 | It can be `RequestException`, `ClientException` or `ServerException`, but for some reasons you don't want to catch them. 14 | You just want to receive response and to work with it. 15 | 16 | It's possible! Just setup `http_errors` to false: 17 | 18 | ```yaml 19 | eight_points_guzzle: 20 | clients: 21 | api_payment: 22 | base_url: "http://api.domain.tld" 23 | options: 24 | http_errors: false 25 | ``` 26 | 27 | Read more about this option in the [official Guzzle documentation][1]. 28 | 29 | [1]: http://docs.guzzlephp.org/en/latest/request-options.html#http-errors 30 | -------------------------------------------------------------------------------- /src/Resources/doc/environment-variables-integration.md: -------------------------------------------------------------------------------- 1 | # Environment variables integration 2 | 3 | Environment variable processors were [introduced][1] in Symfony 3.4 and with each symfony release this functionality is constantly improved. 4 | We also want to support this direction and let's see how to use environment variables with Guzzle Bundle. 5 | 6 | For example you are having configured next client: 7 | 8 | ```yaml 9 | # config/packages/eight_points_guzzle.yaml 10 | 11 | eight_points_guzzle: 12 | clients: 13 | api_payment: 14 | base_url: "http://api.domain.tld" 15 | ``` 16 | 17 | and you want to move `base_url` value to env variables: 18 | 19 | ```dotenv 20 | # .env 21 | 22 | API_PAYMENT_URL=http://api.domain.tld 23 | ``` 24 | 25 | Next let's do some small adjustments in configuration file: 26 | 27 | ```yaml 28 | parameters: 29 | env(API_PAYMENT_URL): '' 30 | 31 | eight_points_guzzle: 32 | clients: 33 | api_payment: 34 | base_url: '%env(string:API_PAYMENT_URL)%' 35 | ``` 36 | 37 | We added `env(API_PAYMENT_URL): ''` to define this variable if it was not defined not to stop the build process (for example in CI system). 38 | Also we used `'%env(string:API_PAYMENT_URL)%'` to insert the value of environment variable with type casting. 39 | 40 | That's all! 41 | 42 | ## Learn more 43 | - [Symfony blog: New in Symfony 3.4 - Advanced environment variables][1] 44 | - [Symfony doc: How to Set external Parameters in the Service Container][2] 45 | 46 | [1]: https://symfony.com/blog/new-in-symfony-3-4-advanced-environment-variables 47 | [2]: https://symfony.com/doc/current/configuration/external_parameters.html 48 | -------------------------------------------------------------------------------- /src/Resources/doc/how-to-create-a-single-file-plugin.md: -------------------------------------------------------------------------------- 1 | # How to create a single-file plugin 2 | 3 | The possibility of creating plugins was introduced in version [7.0][1] of Guzzle Bundle and several plugins have already been published and downloaded thousands of times. 4 | 5 | Let’s see how easy it is to create your own single-file plugin! 6 | 7 | ### Middleware 8 | 9 | > If you are not familiar with `middleware` definition, then first read this great article provided by Guzzle team: 10 | > [http://docs.guzzlephp.org/en/stable/handlers-and-middleware.html#middleware][6] 11 | 12 | The primary goal of plugins is to provide possibility of configuration of middlewares for each guzzle client individually. 13 | 14 | So, first we need a middleware. Suppose we have one that adds some "magic header" with "magic value" to all the requests: 15 | 16 | ```php 17 | // src/GuzzleMiddleware/MagicHeaderMiddleware.php 18 | 19 | namespace App\GuzzleMiddleware; 20 | 21 | use Psr\Http\Message\RequestInterface; 22 | 23 | class MagicHeaderMiddleware 24 | { 25 | private $magicHeaderValue; 26 | 27 | /** 28 | * @param $magicHeaderValue 29 | */ 30 | public function __construct($magicHeaderValue) 31 | { 32 | $this->magicHeaderValue = $magicHeaderValue; 33 | } 34 | 35 | /** 36 | * @param callable $handler 37 | * 38 | * @return \Closure 39 | */ 40 | public function __invoke(callable $handler) : \Closure 41 | { 42 | return function ( 43 | RequestInterface $request, 44 | array $options 45 | ) use ($handler) { 46 | $request = $request->withHeader('magic-header', $this->magicHeaderValue); 47 | 48 | // Continue the handler chain. 49 | return $handler($request, $options); 50 | }; 51 | } 52 | } 53 | ``` 54 | 55 | ### Client 56 | 57 | Also we need a guzzle client, that will be used to perform requests: 58 | 59 | ```yaml 60 | # config/packages/eight_points_guzzle.yaml 61 | 62 | eight_points_guzzle: 63 | clients: 64 | httpbin_client: 65 | base_url: "http://httpbin.org" 66 | 67 | plugin: ~ 68 | ``` 69 | 70 | Now it’s time to connect middleware with the client through plugin system! 71 | 72 | ```yaml 73 | # config/packages/eight_points_guzzle.yaml 74 | 75 | eight_points_guzzle: 76 | clients: 77 | httpbin_client: 78 | base_url: "http://httpbin.org" 79 | 80 | plugin: 81 | magic_header: 82 | header_value: magic-value 83 | ``` 84 | 85 | Adjust your configuration file and try to clear cache using command `bin/console cache:clear` and you will observe an error: 86 | 87 | ```bash 88 | In ArrayNode.php line 311: 89 | Unrecognized option "magic_header" under "eight_points_guzzle.clients.httpbin_client.plugin" 90 | ``` 91 | 92 | It’s OK, because Guzzle Bundle does not know anything about `magic_header` plugin and such a configuration. 93 | The plugin we plan to make will help Guzzle Bundle to do it. 94 | 95 | ### Plugin 96 | 97 | Create plugin file: 98 | 99 | ```php 100 | // src/GuzzlePlugin/MagicHeaderPlugin.php 101 | 102 | namespace App\GuzzlePlugin; 103 | 104 | use EightPoints\Bundle\GuzzleBundle\PluginInterface; 105 | use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; 106 | use Symfony\Component\DependencyInjection\ContainerBuilder; 107 | use Symfony\Component\DependencyInjection\Definition; 108 | use Symfony\Component\HttpKernel\Bundle\Bundle; 109 | 110 | class MagicHeaderPlugin extends Bundle implements PluginInterface 111 | { 112 | /** 113 | * @return string 114 | */ 115 | public function getPluginName() : string 116 | { 117 | 118 | } 119 | 120 | /** 121 | * @param ArrayNodeDefinition $pluginNode 122 | */ 123 | public function addConfiguration(ArrayNodeDefinition $pluginNode) 124 | { 125 | 126 | } 127 | 128 | /** 129 | * @param array $config 130 | * @param ContainerBuilder $container 131 | * @param string $clientName 132 | * @param Definition $handler 133 | */ 134 | public function loadForClient(array $config, ContainerBuilder $container, string $clientName, Definition $handler) 135 | { 136 | 137 | } 138 | 139 | /** 140 | * @param array $configs 141 | * @param ContainerBuilder $container 142 | */ 143 | public function load(array $configs, ContainerBuilder $container) 144 | { 145 | 146 | } 147 | } 148 | ``` 149 | 150 | Note that we implemented `PluginInterface` interface and defined 4 methods: 151 | - `load` - used to load xml/yaml/etc configuration 152 | - `loadForClient` - called after clients services are defined in container builder 153 | - `addConfiguration` - called when configuration tree of Guzzle Bundle is being built 154 | - `getPluginName` - called to get plugin identifier *(plugin name)* 155 | 156 | First define plugin name: 157 | 158 | ```php 159 | // ... 160 | 161 | class MagicHeaderPlugin extends Bundle implements PluginInterface 162 | { 163 | /** 164 | * @return string 165 | */ 166 | public function getPluginName() : string 167 | { 168 | return 'magic_header'; 169 | } 170 | 171 | // ... 172 | } 173 | ``` 174 | 175 | Next add possibility to configure `header_value` value in configuration file: 176 | 177 | ```php 178 | // ... 179 | 180 | class MagicHeaderPlugin extends Bundle implements PluginInterface 181 | { 182 | // ... 183 | 184 | /** 185 | * @param ArrayNodeDefinition $pluginNode 186 | */ 187 | public function addConfiguration(ArrayNodeDefinition $pluginNode) 188 | { 189 | $pluginNode 190 | ->addDefaultsIfNotSet() 191 | ->children() 192 | ->scalarNode('header_value')->defaultNull()->end() 193 | ->end(); 194 | } 195 | 196 | // ... 197 | } 198 | ``` 199 | 200 | During the construction of configuration tree of Guzzle Bundle the node is created for each plugin and this node is passed to each plugin in `addConfiguration` method. 201 | Here you have possibility to add any nodes you want. In out case we added just one scalar and nullable node with key `header_value`. 202 | 203 | You can read more about configuration [here][2]. 204 | 205 | ### Connect Plugin with Guzzle Bundle 206 | 207 | The plugin does not do anything significant, but it is ready to be connected to Guzzle Bundle: 208 | 209 | ```diff 210 | // src/Kernel.php 211 | 212 | // ... 213 | + use App\GuzzlePlugin\MagicHeaderPlugin; 214 | 215 | class Kernel extends BaseKernel 216 | { 217 | // ... 218 | 219 | public function registerBundles() 220 | { 221 | $contents = require $this->getProjectDir().'/config/bundles.php'; 222 | foreach ($contents as $class => $envs) { 223 | if (isset($envs['all']) || isset($envs[$this->environment])) { 224 | - yield new $class(); 225 | + if ($class === \EightPoints\Bundle\GuzzleBundle\EightPointsGuzzleBundle::class) { 226 | + yield new $class([ 227 | + new MagicHeaderPlugin(), 228 | + ]); 229 | + } else { 230 | + yield new $class(); 231 | + } 232 | } 233 | } 234 | } 235 | 236 | // ... 237 | } 238 | ``` 239 | 240 | Clearing of cache will end with success. 241 | 242 | ### Connect Plugin with Middleware 243 | 244 | To connect the plugin and middleware we need to modify `loadForClient` method from `MagicHeaderPlugin` class. 245 | At this stage we get control over the process of building the container. 246 | Having the value of `header_value` option provided from configuration file we can define middleware as a service and inject it to handler stack of the client: 247 | 248 | ```php 249 | // ... 250 | use Symfony\Component\ExpressionLanguage\Expression; 251 | use App\GuzzleMiddleware\MagicHeaderMiddleware; 252 | 253 | class MagicHeaderPlugin extends Bundle implements PluginInterface 254 | { 255 | // ... 256 | 257 | /** 258 | * @param array $config 259 | * @param ContainerBuilder $container 260 | * @param string $clientName 261 | * @param Definition $handler 262 | */ 263 | public function loadForClient(array $config, ContainerBuilder $container, string $clientName, Definition $handler) 264 | { 265 | if ($config['header_value']) { 266 | // Create DI definition of middleware 267 | $middleware = new Definition(MagicHeaderMiddleware::class); 268 | $middleware->setArguments([$config['header_value']]); 269 | $middleware->setPublic(true); 270 | 271 | // Register Middleware as a Service 272 | $middlewareServiceName = sprintf('guzzle_bundle_magic_header_plugin.middleware.magic_header.%s', $clientName); 273 | $container->setDefinition($middlewareServiceName, $middleware); 274 | 275 | // Inject this service to given Handler Stack 276 | $middlewareExpression = new Expression(sprintf('service("%s")', $middlewareServiceName)); 277 | $handler->addMethodCall('unshift', [$middlewareExpression]); 278 | } 279 | } 280 | 281 | // ... 282 | } 283 | ``` 284 | 285 | Note that `loadForClient` method is executed for each client defined in `eight_points_guzzle` configuration file. 286 | 287 | Read more about handler stack [here][6]. 288 | 289 | ### Testing 290 | 291 | It’s time to test! 292 | 293 | Just call anywhere the client and execute GET request: 294 | 295 | ```php 296 | $this->get('eight_points_guzzle.client.httpbin_client')->get(''); 297 | ``` 298 | 299 | Trigger this action and open Symfony Profiler: 300 | 301 | ![Symfony Profiler: check magic-header](./img/magic_header_middleware.png) 302 | 303 | Note in the request information the header with name `magic-header` and value provided from configuration: `magic-value`. 304 | 305 | ### Conclusion 306 | 307 | In this article we found out how easy it is to create single-file plugin and to extend base functionality provided by Guzzle Bundle. 308 | 309 | In the next article, we'll figure out how to create standalone plugin ready to be published on [packagist.org][3]. 310 | 311 | ### Additional information 312 | 313 | - [Known and Supported Guzzle Bundle Plugins][4] 314 | - [More Guzzle Middlewares][5] 315 | 316 | [1]: https://github.com/8p/EightPointsGuzzleBundle/releases/tag/v7.0.0 317 | [2]: https://symfony.com/doc/current/components/config/definition.html 318 | [3]: https://packagist.org 319 | [4]: https://github.com/8p/EightPointsGuzzleBundle#known-and-supported-plugins 320 | [5]: https://packagist.org/?query=middleware&tags=guzzle~middleware 321 | [6]: http://docs.guzzlephp.org/en/stable/handlers-and-middleware.html#handlerstack 322 | [7]: http://docs.guzzlephp.org/en/stable/handlers-and-middleware.html#middleware 323 | -------------------------------------------------------------------------------- /src/Resources/doc/img/debug_logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8p/EightPointsGuzzleBundle/5df72be234fb0e22d2750e7013003968ec373bff/src/Resources/doc/img/debug_logs.png -------------------------------------------------------------------------------- /src/Resources/doc/img/icon_slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8p/EightPointsGuzzleBundle/5df72be234fb0e22d2750e7013003968ec373bff/src/Resources/doc/img/icon_slack.png -------------------------------------------------------------------------------- /src/Resources/doc/img/magic_header_middleware.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8p/EightPointsGuzzleBundle/5df72be234fb0e22d2750e7013003968ec373bff/src/Resources/doc/img/magic_header_middleware.png -------------------------------------------------------------------------------- /src/Resources/doc/intercept-request-and-response.md: -------------------------------------------------------------------------------- 1 | # Intercept request and response 2 | 3 | In some situations you might want to change a request before it's send out from 4 | a Guzzle client, or change the response that comes back from a Guzzle client. 5 | 6 | This bundle allows you to do just that using Symfony event dispatch component. 7 | 8 | Let's assume you've configured the following clients in your application: 9 | 10 | ```yaml 11 | eight_points_guzzle: 12 | clients: 13 | payment: 14 | base_url: 'http://api.payment.example' 15 | crm: 16 | base_url: 'http://api.crm.tld' 17 | ``` 18 | 19 | And suppose that `payment` requires authorization using some token in the request header. 20 | How can we do that? 21 | 22 | ## Event listener for intercepting requests 23 | 24 | First of all we have to write an event listener, that will see all requests from our `payment` 25 | client, and can modify them. 26 | 27 | ```php 28 | namespace App\EventListener; 29 | 30 | use EightPoints\Bundle\GuzzleBundle\Events\PreTransactionEvent; 31 | 32 | class PaymentApiGuzzleEventListener 33 | { 34 | /** 35 | * @param PreTransactionEvent $event 36 | */ 37 | public function onPreTransaction(PreTransactionEvent $event) 38 | { 39 | // get request from the event 40 | $request = $event->getTransaction(); 41 | 42 | // setup new header to request 43 | $modifiedRequest = $request->withHeader('Authorization', 'Bearer longLongLongToken'); 44 | 45 | // replace request in event 46 | $event->setTransaction($modifiedRequest); 47 | } 48 | } 49 | ``` 50 | 51 | It's important to note that `Response` is immutable. As such, `withHeader` method does not change the request 52 | by reference, but rather clones it, changes it and then returns it. 53 | 54 | Now, just writing this class won't make it fire for every request on the `payment` client. 55 | For that we need to register it in the Symfony configuration as an event listener, as follows: 56 | 57 | ```yaml 58 | services: 59 | App\EventListener\PaymentApiGuzzleEventListener: 60 | class: App\EventListener\PaymentApiGuzzleEventListener 61 | tags: 62 | - { name: kernel.event_listener, event: eight_points_guzzle.pre_transaction.payment, method: onPreTransaction } 63 | ``` 64 | 65 | Because this listener listens for the event `eight_points_guzzle.pre_transaction.payment` it will _only_ receive 66 | events regarding the `payment` client, no other clients. If you want a listener that receives events for all clients, 67 | subscribe to the `eight_points_guzzle.pre_transaction` event instead. This can be useful for logging, auditing, etc. 68 | 69 | Note that if a generic listener and a client specific listener both change a request in the same way 70 | (for example, both add the same header), the value from the client specific listener overrides the value 71 | from the generic listener. 72 | 73 | ## Event listener for intercepting responses 74 | 75 | In previous step we intercepted the request and changed it, but we want to track the response too. 76 | For example we can invalidate token if the `payment` API rejected it. 77 | 78 | Let's subscribe our service to one more event: 79 | 80 | ```yaml 81 | services: 82 | App\EventListener\PaymentApiGuzzleEventListener: 83 | class: App\EventListener\PaymentApiGuzzleEventListener 84 | tags: 85 | - { name: kernel.event_listener, event: eight_points_guzzle.pre_transaction.payment, method: onPreTransaction } 86 | - { name: kernel.event_listener, event: eight_points_guzzle.post_transaction.payment, method: onPostTransaction } 87 | ``` 88 | 89 | Again, because this listener listens for the event `eight_points_guzzle.post_transaction.payment` it will _only_ receive 90 | events regarding the `payment` client, no other clients. Just like with pre transaction events, if you want a listener that 91 | receives events for all clients, subscribe to the `eight_points_guzzle.post_transaction` event instead. 92 | This can be useful for logging, auditing, etc. 93 | 94 | Note that if a generic listener and a client specific listener both change a response in the same way 95 | (for example, both add the same header), the value from the client specific listener overrides the value 96 | from the generic listener. 97 | 98 | Now we can implement the `onPostTransaction` method on our service: 99 | 100 | ```php 101 | namespace App\EventListener; 102 | 103 | use EightPoints\Bundle\GuzzleBundle\Events\PostTransactionEvent; 104 | 105 | class PaymentApiGuzzleEventListener 106 | { 107 | // ... 108 | 109 | /** 110 | * @param PostTransactionEvent $event 111 | */ 112 | public function onPostTransaction(PostTransactionEvent $event) 113 | { 114 | // get response from the event 115 | $response = $event->getTransaction(); 116 | 117 | // check if response status code is 403 118 | if ($response->getStatusCode() === Response::HTTP_FORBIDDEN) { 119 | // invalidate token 120 | } 121 | } 122 | } 123 | ``` 124 | 125 | ## Using event subscribers instead of listeners 126 | 127 | If you want to you can also use an event subscriber to the same as above 128 | 129 | ```php 130 | namespace App\EventSubscriber; 131 | 132 | use EightPoints\Bundle\GuzzleBundle\Events\GuzzleEvents; 133 | use EightPoints\Bundle\GuzzleBundle\Events\PreTransactionEvent; 134 | use EightPoints\Bundle\GuzzleBundle\Events\PostTransactionEvent; 135 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 136 | 137 | class PaymentApiGuzzleEventSubscriber implements EventSubscriberInterface 138 | { 139 | /** 140 | * @param PreTransactionEvent $event 141 | */ 142 | public function onPreTransaction(PreTransactionEvent $event) 143 | { 144 | // get request from the event 145 | $request = $event->getTransaction(); 146 | 147 | // setup new header to request 148 | $modifiedRequest = $request->withHeader('Authorization', 'Bearer longLongLongToken'); 149 | 150 | // replace request in event 151 | $event->setTransaction($modifiedRequest); 152 | } 153 | 154 | /** 155 | * @param PostTransactionEvent $event 156 | */ 157 | public function onPostTransaction(PostTransactionEvent $event) 158 | { 159 | // get response from the event 160 | $response = $event->getTransaction(); 161 | 162 | // check if response status code is 403 163 | if ($response->getStatusCode() === Response::HTTP_FORBIDDEN) { 164 | // invalidate token 165 | } 166 | } 167 | 168 | public static function getSubscribedEvents() 169 | { 170 | return [ 171 | GuzzleEvents::preTransactionFor('payment') => 'onPreTransaction', 172 | GuzzleEvents::postTransactionFor('payment') => 'onPostTransaction' 173 | ]; 174 | } 175 | } 176 | ``` 177 | 178 | And configure the Symfony service as usual for event subscribers: 179 | 180 | ```yaml 181 | services: 182 | App\EventSubscriber\PaymentApiGuzzleEventSubscriber: 183 | class: App\EventSubscriber\PaymentApiGuzzleEventSubscriber 184 | tags: 185 | - { name: kernel.event_subscriber } 186 | ``` 187 | 188 | (This is not required when your project uses autoconfiguration, it will 189 | be tagged automatically) 190 | 191 | ## Learn more 192 | - [Symfony doc: Events and Event Listeners][1] 193 | 194 | [1]: https://symfony.com/doc/current/event_dispatcher.html 195 | -------------------------------------------------------------------------------- /src/Resources/doc/redefine-client-class.md: -------------------------------------------------------------------------------- 1 | # How to redefine class used for clients 2 | 3 | GuzzleBundle is using `GuzzleHttp\Client` class to create clients. In some cases you may need to extend/rewrite it. 4 | First of all you need to create your own class, but don't forget to extend `GuzzleHttp\Client`: 5 | 6 | ```php 7 | 8 | namespace Namespace\Of\Your\Client; 9 | 10 | use GuzzleHttp\Client; 11 | 12 | class AwesomeClient extends Client 13 | { 14 | 15 | } 16 | ``` 17 | 18 | And now we have two possibilities to change the default class: 19 | 20 | #### Global 21 | 22 | Redefine `eight_points_guzzle.http_client.class` parameter. 23 | For example in `config/services.yaml` for Symfony 4 and in `app/config/parameters.yml` in Symfony 2 and 3: 24 | 25 | ```yaml 26 | parameters: 27 | eight_points_guzzle.http_client.class: Namespace\Of\Your\Client\AwesomeClient 28 | ``` 29 | 30 | Note that this method will redefine class for **all** clients. 31 | 32 | #### For specific client 33 | 34 | ```yaml 35 | eight_points_guzzle: 36 | clients: 37 | api_payment: 38 | class: 'Namespace\Of\Your\Client\AwesomeClient' 39 | ``` 40 | 41 | This method will redefine client class only for specific GuzzleBundle client. 42 | -------------------------------------------------------------------------------- /src/Resources/views/Icons/logo.svg.twig: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /src/Resources/views/debug.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block toolbar %} 4 | {% set profiler_markup_version = profiler_markup_version|default(1) %} 5 | 6 | {% if collector.callCount %} 7 | {% if collector.errorCount %} 8 | {% set color = 'red' %} 9 | {% else %} 10 | {% set color = 'green' %} 11 | {% endif %} 12 | {% set status_color = collector.errorCount ? 'red' : (collector.hasSlowResponses() ? 'yellow' : 'normal') %} 13 | 14 | {% set icon %} 15 | {# Symfony <2.8 toolbar #} 16 | {% if profiler_markup_version == 1 %} 17 | {{ include("@EightPointsGuzzle/Icons/logo.svg.twig") }} 18 | 19 | {{ collector.callCount }} 20 | 21 | {% else %} 22 | {{ include("@EightPointsGuzzle/Icons/logo.svg.twig") }} 23 | 24 | {{ collector.callCount }} 25 | 26 | {% endif %} 27 | {% endset %} 28 | 29 | {% set text %} 30 |
31 | API Calls 32 | {{ collector.callCount }} 33 |
34 | 35 |
36 | Total time 37 | {% if collector.totalTime > 1.0 %} 38 | {{ '%0.2f'|format(collector.totalTime) }} s 39 | {% else %} 40 | {{ '%0.0f'|format(collector.totalTime * 1000) }} ms 41 | {% endif %} 42 |
43 | {% endset %} 44 | 45 | {% include "@WebProfiler/Profiler/toolbar_item.html.twig" with { "link": profiler_url, status: status_color } %} 46 | {% endif %} 47 | {% endblock %} 48 | 49 | {% block menu %} 50 | {% if collector.callCount %} 51 | {% set label_class = collector.errorCount ? 'label-status-error' : (collector.hasSlowResponses() ? 'label-status-warning' : '') %} 52 | {% else %} 53 | {% set label_class = 'disabled' %} 54 | {% endif %} 55 | 56 | 57 | {{ include("@EightPointsGuzzle/Icons/logo.svg.twig") }} 58 | 59 | 60 | Guzzle 61 | 62 | {% if collector.callCount > 0 %} 63 | 64 | {{ collector.callCount }} 65 | 66 | {% endif %} 67 | 68 | {% endblock %} 69 | 70 | {% block head %} 71 | {{ parent() }} 72 | 73 | 76 | {% endblock %} 77 | 78 | {% block panel %} 79 |

Logs

80 | 81 | {% include '@EightPointsGuzzle/profiler.html.twig' with { 'collector': collector } %} 82 | {% endblock %} 83 | -------------------------------------------------------------------------------- /src/Resources/views/main.css.twig: -------------------------------------------------------------------------------- 1 | #gb_profiler { 2 | line-height: 1; 3 | font-family: "Droid Sans", sans-serif; 4 | font-size: 0.9em; 5 | margin-left: auto; 6 | margin-right: auto; 7 | } 8 | 9 | #collector-content #gb_profiler pre, 10 | #gb_profiler pre { 11 | background: var(--metric-value-background); 12 | border: 1px solid var(--table-border); 13 | border-radius: 6px; 14 | } 15 | 16 | #gb_profiler textarea { 17 | background: var(--metric-value-background); 18 | border: 1px solid var(--table-border); 19 | border-radius: 6px; 20 | min-height: 100px; 21 | 22 | overflow: auto; 23 | color: var(--base-6); 24 | line-height: 1.2em; 25 | font: 12px Menlo, Monaco, Consolas, monospace; 26 | white-space: pre-wrap; 27 | word-wrap: break-word; 28 | width:100%; 29 | max-height:90%; 30 | resize: vertical; 31 | } 32 | 33 | #gb_profiler .gb_request, #gb_profiler .gb_overview { 34 | width: 100%; 35 | margin-bottom: 7px; 36 | border-radius: 6px; 37 | } 38 | 39 | #gb_profiler table { 40 | margin-bottom: 0; 41 | } 42 | 43 | #gb_profiler table th { 44 | width: 130px; 45 | vertical-align: top; 46 | } 47 | 48 | #gb_profiler h3 .gb_request__method { 49 | text-transform: uppercase; 50 | text-decoration: none; 51 | color: white; 52 | display: inline-block; 53 | width: 50px; 54 | font-size: 0.8em; 55 | text-align: center; 56 | padding: 7px 0 5px; 57 | border-radius: 4px; 58 | } 59 | 60 | #gb_profiler h3 .gb_request__url { 61 | padding-left: 10px; 62 | color: var(--base-6); 63 | text-decoration: none; 64 | font-weight: normal; 65 | } 66 | 67 | #gb_profiler h3 .gb_request__response { 68 | text-decoration: none; 69 | color: white; 70 | display: inline-block; 71 | font-size: 0.8em; 72 | text-align: center; 73 | padding: 7px 10px 5px 10px; 74 | float: right; 75 | border-radius: 4px; 76 | margin-left: 10px; 77 | } 78 | 79 | #gb_profiler h3 .gb_request__request_time { 80 | text-decoration: none; 81 | color: white; 82 | display: inline-block; 83 | font-size: 0.8em; 84 | text-align: center; 85 | padding: 7px 10px 5px 10px; 86 | float: right; 87 | border-radius: 4px; 88 | } 89 | 90 | #gb_profiler .gb_overview, 91 | #gb_profiler .gb_overview h2, 92 | #gb_profiler .gb_overview table, 93 | #gb_profiler .gb_overview table tr, 94 | #gb_profiler .gb_overview table th, 95 | #gb_profiler .gb_overview table td { 96 | background: var(--metric-value-background); 97 | border: 1px solid var(--table-border); 98 | } 99 | 100 | #gb_profiler .gb_request.gb_request--get, 101 | #gb_profiler .gb_request.gb_request--get h3, 102 | #gb_profiler .gb_request.gb_request--get table, 103 | #gb_profiler .gb_request.gb_request--get table tr, 104 | #gb_profiler .gb_request.gb_request--get table th, 105 | #gb_profiler .gb_request.gb_request--get table td { 106 | background: var(--metric-value-background); 107 | border: 1px solid var(--table-border); 108 | } 109 | 110 | #gb_profiler .gb_request.gb_request--get h3 .gb_request__method, 111 | #gb_profiler .gb_request.gb_request--get h3 .gb_request__response, 112 | #gb_profiler .gb_request.gb_request--get h3 .gb_request__request_time { 113 | background-color: #0f6ab4; 114 | } 115 | 116 | #gb_profiler .gb_request.gb_request--put, 117 | #gb_profiler .gb_request.gb_request--put h3, 118 | #gb_profiler .gb_request.gb_request--put table, 119 | #gb_profiler .gb_request.gb_request--put table tr, 120 | #gb_profiler .gb_request.gb_request--put table th, 121 | #gb_profiler .gb_request.gb_request--put table td { 122 | background: var(--metric-value-background); 123 | border: 1px solid var(--table-border); 124 | } 125 | 126 | #gb_profiler .gb_request.gb_request--put h3 .gb_request__method, 127 | #gb_profiler .gb_request.gb_request--put h3 .gb_request__response, 128 | #gb_profiler .gb_request.gb_request--put h3 .gb_request__request_time { 129 | background-color: #c5862b; 130 | } 131 | 132 | #gb_profiler .gb_request.gb_request--post, 133 | #gb_profiler .gb_request.gb_request--post h3, 134 | #gb_profiler .gb_request.gb_request--post table, 135 | #gb_profiler .gb_request.gb_request--post table tr, 136 | #gb_profiler .gb_request.gb_request--post table th, 137 | #gb_profiler .gb_request.gb_request--post table td { 138 | background: var(--metric-value-background); 139 | border: 1px solid var(--table-border); 140 | } 141 | 142 | #gb_profiler .gb_request.gb_request--post h3 .gb_request__method, 143 | #gb_profiler .gb_request.gb_request--post h3 .gb_request__response, 144 | #gb_profiler .gb_request.gb_request--post h3 .gb_request__request_time { 145 | background-color: #10a54a; 146 | } 147 | 148 | #gb_profiler .gb_request.gb_request--patch, 149 | #gb_profiler .gb_request.gb_request--patch h3, 150 | #gb_profiler .gb_request.gb_request--patch table, 151 | #gb_profiler .gb_request.gb_request--patch table tr, 152 | #gb_profiler .gb_request.gb_request--patch table th, 153 | #gb_profiler .gb_request.gb_request--patch table td { 154 | background: var(--metric-value-background); 155 | border: 1px solid var(--table-border); 156 | } 157 | 158 | #gb_profiler .gb_request.gb_request--patch h3 .gb_request__method, 159 | #gb_profiler .gb_request.gb_request--patch h3 .gb_request__response, 160 | #gb_profiler .gb_request.gb_request--patch h3 .gb_request__request_time { 161 | background-color: #d07e44; 162 | } 163 | 164 | #gb_profiler .gb_request.gb_request--delete, 165 | #gb_profiler .gb_request.gb_request--delete h3, 166 | #gb_profiler .gb_request.gb_request--delete table, 167 | #gb_profiler .gb_request.gb_request--delete table tr, 168 | #gb_profiler .gb_request.gb_request--delete table th, 169 | #gb_profiler .gb_request.gb_request--delete table td { 170 | background: var(--metric-value-background); 171 | border: 1px solid var(--table-border); 172 | } 173 | 174 | #gb_profiler .gb_request.gb_request--delete h3 .gb_request__method, 175 | #gb_profiler .gb_request.gb_request--delete h3 .gb_request__response, 176 | #gb_profiler .gb_request.gb_request--delete h3 .gb_request__request_time { 177 | background-color: #a41e22; 178 | } 179 | 180 | #gb_profiler h2 { 181 | font-size: 1.1em; 182 | margin-top: 25px; 183 | } 184 | 185 | #gb_profiler .gb_request h3 { 186 | font-size: 1em; 187 | border-width: 0 0 1px 0 !important; 188 | padding: 5px; 189 | margin: 0; 190 | } 191 | 192 | #gb_profiler h3 a { 193 | text-decoration: none; 194 | } 195 | 196 | #gb_profiler h3 a .gb_request__url { 197 | text-decoration: underline; 198 | } 199 | 200 | #gb_profiler h4 { 201 | font-size: 1em; 202 | margin: 16px 0 10px 0; 203 | } 204 | 205 | #gb_profiler .gb_content { 206 | padding: 10px; 207 | } 208 | 209 | #gb_profiler .gb_content.gb_content--hide { 210 | display: none; 211 | } 212 | -------------------------------------------------------------------------------- /src/Resources/views/profiler.html.twig: -------------------------------------------------------------------------------- 1 | {% if not collector.logs %} 2 |

3 | No calls 4 |

5 | {% else %} 6 |
7 |

Overview

8 |
9 |
10 | 11 | 12 | 13 | 20 | 21 |
Total time 14 | {% if collector.totalTime > 1.0 %} 15 | {{ '%0.2f'|format(collector.totalTime) }} s 16 | {% else %} 17 | {{ '%0.0f'|format(collector.totalTime * 1000) }} ms 18 | {% endif %} 19 |
22 |
23 |
24 | {% for group in collector.logs %} 25 |

Group {{ group.requestName }}

26 | 27 | {% for message in group.messages %} 28 |
29 |

30 | 31 | {{ message.request.method }} 32 | {{ message.request.url }} 33 | 34 | 35 | {% if message.response %} 36 | {{ message.response.statusPhrase }} ({{ message.response.statusCode }}) 37 | {% else %} 38 | N/A 39 | {% endif %} 40 | 41 | 42 | 43 | {% if message.transferTime %} 44 | {{ message.transferTime * 1000 }} ms 45 | {% else %} 46 | N/A 47 | {% endif %} 48 | 49 | 50 |

51 | 52 |
53 |

Basic

54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
Log Level{{ message.level }}
Message{{ message.message }}
66 | 67 |
68 |

69 | 70 | Request 71 | 72 |

73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 90 | 91 | 92 | {% if message.curlCommand %} 93 | 94 | 95 | 98 | 99 | {% endif %} 100 |
Protocol Version{{ message.request.protocolVersion }}
Headers{{ eight_points_guzzle_dump(message.request.headers) }}
Body 88 | 89 |
Curl command 96 |
{{ message.curlCommand }}
97 |
101 |
102 | 103 |
104 |

105 | 106 | Response 107 | 108 |

109 | 110 | {% if message.response %} 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 127 | 128 |
Protocol Version{{ message.response.protocolVersion }}
Headers{{ eight_points_guzzle_dump(message.response.headers) }}
Body 125 | 126 |
129 | {% else %} 130 | No response available. 131 | {% endif %} 132 |
133 |
134 |
135 | {% else %} 136 |

No Calls

137 | {% endfor %} 138 | {% endfor %} 139 |
140 | 141 | 189 | {% endif %} 190 | -------------------------------------------------------------------------------- /src/Twig/Extension/DebugExtension.php: -------------------------------------------------------------------------------- 1 | ['html'], 'needs_environment' => true] 23 | ), 24 | ]; 25 | } 26 | 27 | /** 28 | * @param Environment $env 29 | * @param $value 30 | * 31 | * @throws \Exception 32 | * 33 | * @return bool|string 34 | */ 35 | public function dump(Environment $env, $value) 36 | { 37 | $cloner = new VarCloner(); 38 | 39 | $dump = fopen('php://memory', 'r+b'); 40 | $dumper = new HtmlDumper($dump, $env->getCharset()); 41 | 42 | $dumper->dump($cloner->cloneVar($value)); 43 | rewind($dump); 44 | 45 | return stream_get_contents($dump); 46 | } 47 | 48 | /** 49 | * This method is removed from interface in Twig v2.0 50 | * 51 | * @TODO Remove this method when drop support of Symfony < 5.0 52 | * 53 | * @return string 54 | */ 55 | public function getName() : string 56 | { 57 | return get_class($this); 58 | } 59 | } 60 | --------------------------------------------------------------------------------