├── LICENSE ├── composer.json └── src └── GuzzleFactory.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2025 Graham Campbell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graham-campbell/guzzle-factory", 3 | "description": "Provides A Simple Guzzle Factory With Good Defaults", 4 | "keywords": ["guzzle", "http", "guzzle factory", "guzzle-factory", "Guzzle", "Guzzle Factory", "Guzzle-Factory", "Graham Campbell", "GrahamCampbell"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Graham Campbell", 9 | "email": "hello@gjcampbell.co.uk", 10 | "homepage": "https://github.com/GrahamCampbell" 11 | } 12 | ], 13 | "require": { 14 | "php": "^7.4.15 || ^8.0.2", 15 | "guzzlehttp/guzzle": "^7.9.2", 16 | "guzzlehttp/psr7": "^2.7.0" 17 | }, 18 | "require-dev": { 19 | "graham-campbell/analyzer": "^4.2.1 || ^5.0", 20 | "phpunit/phpunit": "^9.6.22 || ^10.5.45 || ^11.5.7" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "GrahamCampbell\\GuzzleFactory\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "GrahamCampbell\\Tests\\GuzzleFactory\\": "tests/" 30 | } 31 | }, 32 | "config": { 33 | "preferred-install": "dist" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/GuzzleFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace GrahamCampbell\GuzzleFactory; 15 | 16 | use Closure; 17 | use GuzzleHttp\BodySummarizer; 18 | use GuzzleHttp\Client; 19 | use GuzzleHttp\Exception\ConnectException; 20 | use GuzzleHttp\Exception\TransferException; 21 | use GuzzleHttp\HandlerStack; 22 | use GuzzleHttp\Middleware; 23 | use GuzzleHttp\RequestOptions; 24 | use GuzzleHttp\RetryMiddleware; 25 | use GuzzleHttp\Utils; 26 | use Psr\Http\Message\RequestInterface; 27 | use Psr\Http\Message\ResponseInterface; 28 | 29 | /** 30 | * This is the guzzle factory class. 31 | * 32 | * @author Graham Campbell 33 | */ 34 | final class GuzzleFactory 35 | { 36 | /** 37 | * The default crypto method. 38 | * 39 | * @var int 40 | */ 41 | private const CRYPTO_METHOD = \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; 42 | 43 | /** 44 | * The default connect timeout. 45 | * 46 | * @var int 47 | */ 48 | private const CONNECT_TIMEOUT = 10; 49 | 50 | /** 51 | * The default transport timeout. 52 | * 53 | * @var int 54 | */ 55 | private const TIMEOUT = 15; 56 | 57 | /** 58 | * The default backoff multiplier. 59 | * 60 | * @var int 61 | */ 62 | private const BACKOFF = 1000; 63 | 64 | /** 65 | * The default 4xx retry codes. 66 | * 67 | * @var int[] 68 | */ 69 | private const CODES = [429]; 70 | 71 | /** 72 | * The default amount of retries. 73 | */ 74 | private const RETRIES = 3; 75 | 76 | /** 77 | * Create a new guzzle client. 78 | * 79 | * @param array $options 80 | * @param int|null $backoff 81 | * @param int[]|null $codes 82 | * @param int|null $retries 83 | * 84 | * @return \GuzzleHttp\Client 85 | */ 86 | public static function make( 87 | array $options = [], 88 | ?int $backoff = null, 89 | ?array $codes = null, 90 | ?int $retries = null 91 | ): Client { 92 | $config = array_merge([ 93 | RequestOptions::CRYPTO_METHOD => self::CRYPTO_METHOD, 94 | RequestOptions::CONNECT_TIMEOUT => self::CONNECT_TIMEOUT, 95 | RequestOptions::TIMEOUT => self::TIMEOUT, 96 | ], $options); 97 | 98 | $config['handler'] = self::handler($backoff, $codes, $retries, $options['handler'] ?? null); 99 | 100 | return new Client($config); 101 | } 102 | 103 | /** 104 | * Create a new retrying handler stack. 105 | * 106 | * @param int|null $backoff 107 | * @param int[]|null $codes 108 | * @param int|null $retries 109 | * @param \GuzzleHttp\HandlerStack|null $stack 110 | * 111 | * @return \GuzzleHttp\HandlerStack 112 | */ 113 | public static function handler( 114 | ?int $backoff = null, 115 | ?array $codes = null, 116 | ?int $retries = null, 117 | ?HandlerStack $stack = null 118 | ): HandlerStack { 119 | $stack = $stack ?? self::innerHandler(); 120 | 121 | if ($retries === 0) { 122 | return $stack; 123 | } 124 | 125 | $stack->push(self::createRetryMiddleware($backoff ?? self::BACKOFF, $codes ?? self::CODES, $retries ?? self::RETRIES), 'retry'); 126 | 127 | return $stack; 128 | } 129 | 130 | /** 131 | * Create a new handler stack. 132 | * 133 | * @param callable|null $handler 134 | * 135 | * @return \GuzzleHttp\HandlerStack 136 | */ 137 | public static function innerHandler( 138 | ?callable $handler = null 139 | ): HandlerStack { 140 | $stack = new HandlerStack($handler ?? Utils::chooseHandler()); 141 | 142 | $stack->push(Middleware::httpErrors(new BodySummarizer(250)), 'http_errors'); 143 | $stack->push(Middleware::redirect(), 'allow_redirects'); 144 | $stack->push(Middleware::cookies(), 'cookies'); 145 | $stack->push(Middleware::prepareBody(), 'prepare_body'); 146 | 147 | return $stack; 148 | } 149 | 150 | /** 151 | * Create a new retry middleware. 152 | * 153 | * @param int $backoff 154 | * @param int[] $codes 155 | * @param int $maxRetries 156 | * 157 | * @return Closure 158 | */ 159 | private static function createRetryMiddleware( 160 | int $backoff, 161 | array $codes, 162 | int $maxRetries 163 | ): Closure { 164 | $decider = static function ($retries, RequestInterface $request, ?ResponseInterface $response = null, ?TransferException $exception = null) use ($codes, $maxRetries) { 165 | return $retries < $maxRetries && ($exception instanceof ConnectException || ($response && ($response->getStatusCode() >= 500 || in_array($response->getStatusCode(), $codes, true)))); 166 | }; 167 | 168 | $delay = static function ($retries) use ($backoff) { 169 | return (int) pow(2, $retries) * $backoff; 170 | }; 171 | 172 | return static function (callable $handler) use ($decider, $delay): RetryMiddleware { 173 | return new RetryMiddleware($decider, $handler, $delay); 174 | }; 175 | } 176 | } 177 | --------------------------------------------------------------------------------