├── .php-cs-fixer.php ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── ClassDiscovery.php ├── Composer └── Plugin.php ├── Exception.php ├── Exception ├── ClassInstantiationFailedException.php ├── DiscoveryFailedException.php ├── NoCandidateFoundException.php ├── NotFoundException.php ├── PuliUnavailableException.php └── StrategyUnavailableException.php ├── HttpAsyncClientDiscovery.php ├── HttpClientDiscovery.php ├── MessageFactoryDiscovery.php ├── NotFoundException.php ├── Psr17Factory.php ├── Psr17FactoryDiscovery.php ├── Psr18Client.php ├── Psr18ClientDiscovery.php ├── Strategy ├── CommonClassesStrategy.php ├── CommonPsr17ClassesStrategy.php ├── DiscoveryStrategy.php ├── MockClientStrategy.php └── PuliBetaStrategy.php ├── StreamFactoryDiscovery.php └── UriFactoryDiscovery.php /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in(__DIR__.'/src') 5 | ->name('*.php') 6 | ; 7 | 8 | $config = (new PhpCsFixer\Config()) 9 | ->setRiskyAllowed(true) 10 | ->setRules([ 11 | '@Symfony' => true, 12 | 'trailing_comma_in_multiline' => false, // for methods this is incompatible with PHP 7 13 | ]) 14 | ->setFinder($finder) 15 | ; 16 | 17 | return $config; 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.20.0 - 2024-10-02 4 | 5 | - [#268](https://github.com/php-http/discovery/pull/268) - Do not attempt to update lock file when it is not existing. 6 | - [#267](https://github.com/php-http/discovery/pull/267) - Test with PHP 8.3 and 8.4 7 | - [#266](https://github.com/php-http/discovery/pull/266) - If wrapped client implements factories, use those instead of discovering new factories. 8 | 9 | ## 1.19.4 - 2024-03-29 10 | 11 | - [#264](https://github.com/php-http/discovery/pull/264) - Do not report a general conflict with `sebastian/comparator` but make sure we install the correct version for our tests. 12 | 13 | ## 1.19.3 - 2024-03-28 14 | 15 | - [#261](https://github.com/php-http/discovery/pull/261) - explicitly mark nullable parameters as nullable (avoid deprecation in PHP 8.4) 16 | 17 | ## 1.19.2 - 2023-11-30 18 | 19 | - [#253](https://github.com/php-http/discovery/pull/253) - Symfony 7 dropped the deprecated PHP-HTTP `HttpClient` interface from their HTTP client, do not discover the version 7 client when looking for the old interface. 20 | 21 | ## 1.19.1 - 2023-07-11 22 | 23 | - [#250](https://github.com/php-http/discovery/pull/250) - Fix: Buzz client instantiation using deprecated Message Factory Discovery, use PSR-17 factory discovery instead. 24 | 25 | ## 1.19.0 - 2023-06-19 26 | 27 | - [#249](https://github.com/php-http/discovery/pull/249) - Have composer plugin correctly install Symfony http client when nothing explicitly requires psr 18 resp. httplug. 28 | - [#241](https://github.com/php-http/discovery/pull/241) - Support discovering PSR-17 factories of `httpsoft/http-message` package 29 | 30 | ## 1.18.1 - 2023-05-17 31 | 32 | - [#242](https://github.com/php-http/discovery/pull/242) - Better exception message when no legacy php-http message factories can be built. Also needs php-http/message-factory package and they are deprecated in favor of PSR-17 anyways. 33 | 34 | ## 1.18.0 - 2023-05-03 35 | 36 | - [#235](https://github.com/php-http/discovery/pull/235) - Deprecate HttpClientDiscovery, use Psr18ClientDiscovery instead 37 | - [#238](https://github.com/php-http/discovery/pull/238) - Skip requiring php-http/message-factory when installing symfony/http-client 6.3+ 38 | - [#239](https://github.com/php-http/discovery/pull/239) - Skip auto-installing when the root package's extra.discovery is enough 39 | 40 | ## 1.17.0 - 2023-04-26 41 | 42 | - [#230](https://github.com/php-http/discovery/pull/230) - Add Psr18Client to make it straightforward to use PSR-18 43 | - [#232](https://github.com/php-http/discovery/pull/232) - Allow pinning the preferred implementations in composer.json 44 | - [#233](https://github.com/php-http/discovery/pull/233) - Fix Psr17Factory::createServerRequestFromGlobals() when uploaded files have been moved 45 | 46 | ## 1.16.0 - 2023-04-26 47 | 48 | - [#225](https://github.com/php-http/discovery/pull/225) - Remove support for the abandoned Zend Diactoros which has been replaced with Laminas Diactoros; marked the zend library as conflict in composer.json to avoid confusion 49 | - [#227](https://github.com/php-http/discovery/pull/227) - Fix handling requests with nested files 50 | 51 | ## 1.15.3 - 2023-03-31 52 | 53 | - [#224](https://github.com/php-http/discovery/pull/224) - Fix regression with Magento classloader 54 | 55 | ## 1.15.2 - 2023-02-11 56 | 57 | - [#219](https://github.com/php-http/discovery/pull/219) - Fix handling of replaced packages 58 | 59 | ## 1.15.1 - 2023-02-10 60 | 61 | - [#214](https://github.com/php-http/discovery/pull/214) - Fix resolving deps for psr/http-message-implementation 62 | - [#216](https://github.com/php-http/discovery/pull/216) - Fix keeping platform requirements when rebooting composer 63 | - [#217](https://github.com/php-http/discovery/pull/217) - Set extra.plugin-optional composer flag 64 | 65 | ## 1.15.0 - 2023-02-09 66 | 67 | - [#209](https://github.com/php-http/discovery/pull/209) - Add generic `Psr17Factory` class 68 | - [#208](https://github.com/php-http/discovery/pull/208) - Add composer plugin to auto-install missing implementations. 69 | When libraries require an http implementation but no packages providing that implementation is installed in the application, the plugin will automatically install one. 70 | This is only done for libraries that directly require php-http/discovery to avoid unexpected dependency installation. 71 | 72 | ## 1.14.3 - 2022-07-11 73 | 74 | - [#207](https://github.com/php-http/discovery/pull/207) - Updates Exception to extend Throwable solving static analysis errors for consumers 75 | 76 | ## 1.14.2 - 2022-05-25 77 | 78 | - [#202](https://github.com/php-http/discovery/pull/202) - Avoid error when the Symfony PSR-18 client exists but its dependencies are not installed 79 | 80 | ## 1.14.1 - 2021-09-18 81 | 82 | - [#199](https://github.com/php-http/discovery/pull/199) - Fixes message factory discovery for `laminas-diactoros ^2.7` 83 | 84 | ## 1.14.0 - 2021-06-21 85 | 86 | - Deprecate puli as it has been unmaintained for a long time and is not compatible with composer 2 https://github.com/php-http/discovery/pull/195 87 | 88 | ## 1.13.0 - 2020-11-27 89 | 90 | - Support discovering PSR-17 factories of `slim/psr7` package https://github.com/php-http/discovery/pull/192 91 | 92 | ## 1.12.0 - 2020-09-22 93 | 94 | - Support discovering HttpClient of `php-http/guzzle7-adapter` https://github.com/php-http/discovery/pull/189 95 | 96 | ## 1.11.0 - 2020-09-22 97 | 98 | - Use correct method name to find Uri Factory in PSR17 https://github.com/php-http/discovery/pull/181 99 | 100 | ## 1.10.0 - 2020-09-04 101 | 102 | - Discover PSR-18 implementation of phalcon 103 | 104 | ## 1.9.1 - 2020-07-13 105 | 106 | ### Fixed 107 | 108 | - Support PHP 7.4 and 8.0 109 | 110 | ## 1.9.0 - 2020-07-02 111 | 112 | ### Added 113 | 114 | - Support discovering PSR-18 factories of `guzzlehttp/guzzle` 7+ 115 | 116 | ## 1.8.0 - 2020-06-14 117 | 118 | ### Added 119 | 120 | - Support discovering PSR-17 factories of `guzzlehttp/psr7` package 121 | - Support discovering PSR-17 factories of `laminas/laminas-diactoros` package 122 | - `ClassDiscovery::getStrategies()` to retrieve the list of current strategies. 123 | 124 | ### Fixed 125 | 126 | - Ignore exception during discovery when Symfony HttplugClient checks if HTTPlug is available. 127 | 128 | ## 1.7.4 - 2020-01-03 129 | 130 | ### Fixed 131 | 132 | - Improve conditions on Symfony's async HTTPlug client. 133 | 134 | ## 1.7.3 - 2019-12-27 135 | 136 | ### Fixed 137 | 138 | - Enough conditions to only use Symfony HTTP client if all needed components are available. 139 | 140 | ## 1.7.2 - 2019-12-27 141 | 142 | ### Fixed 143 | 144 | - Allow a condition to specify an interface and not just classes. 145 | 146 | ## 1.7.1 - 2019-12-26 147 | 148 | ### Fixed 149 | 150 | - Better conditions to see if Symfony's HTTP clients are available. 151 | 152 | ## 1.7.0 - 2019-06-30 153 | 154 | ### Added 155 | 156 | - Dropped support for PHP < 7.1 157 | - Support for `symfony/http-client` 158 | 159 | ## 1.6.1 - 2019-02-23 160 | 161 | ### Fixed 162 | 163 | - MockClientStrategy also provides the mock client when requesting an async client 164 | 165 | ## 1.6.0 - 2019-01-23 166 | 167 | ### Added 168 | 169 | - Support for PSR-17 factories 170 | - Support for PSR-18 clients 171 | 172 | ## 1.5.2 - 2018-12-31 173 | 174 | Corrected mistakes in 1.5.1. The different between 1.5.2 and 1.5.0 is that 175 | we removed some PHP 7 code. 176 | 177 | https://github.com/php-http/discovery/compare/1.5.0...1.5.2 178 | 179 | ## 1.5.1 - 2018-12-31 180 | 181 | This version added new features by mistake. These are reverted in 1.5.2. 182 | 183 | Do not use 1.5.1. 184 | 185 | ### Fixed 186 | 187 | - Removed PHP 7 code 188 | 189 | ## 1.5.0 - 2018-12-30 190 | 191 | ### Added 192 | 193 | - Support for `nyholm/psr7` version 1.0. 194 | - `ClassDiscovery::safeClassExists` which will help Magento users. 195 | - Support for HTTPlug 2.0 196 | - Support for Buzz 1.0 197 | - Better error message when nothing found by introducing a new exception: `NoCandidateFoundException`. 198 | 199 | ### Fixed 200 | 201 | - Fixed condition evaluation, it should stop after first invalid condition. 202 | 203 | ## 1.4.0 - 2018-02-06 204 | 205 | ### Added 206 | 207 | - Discovery support for nyholm/psr7 208 | 209 | ## 1.3.0 - 2017-08-03 210 | 211 | ### Added 212 | 213 | - Discovery support for CakePHP adapter 214 | - Discovery support for Zend adapter 215 | - Discovery support for Artax adapter 216 | 217 | ## 1.2.1 - 2017-03-02 218 | 219 | ### Fixed 220 | 221 | - Fixed minor issue with `MockClientStrategy`, also added more tests. 222 | 223 | ## 1.2.0 - 2017-02-12 224 | 225 | ### Added 226 | 227 | - MockClientStrategy class. 228 | 229 | ## 1.1.1 - 2016-11-27 230 | 231 | ### Changed 232 | 233 | - Made exception messages clearer. `StrategyUnavailableException` is no longer the previous exception to `DiscoveryFailedException`. 234 | - `CommonClassesStrategy` is using `self` instead of `static`. Using `static` makes no sense when `CommonClassesStrategy` is final. 235 | 236 | ## 1.1.0 - 2016-10-20 237 | 238 | ### Added 239 | 240 | - Discovery support for Slim Framework factories 241 | 242 | ## 1.0.0 - 2016-07-18 243 | 244 | ### Added 245 | 246 | - Added back `Http\Discovery\NotFoundException` to preserve BC with 0.8 version. You may upgrade from 0.8.x and 0.9.x to 1.0.0 without any BC breaks. 247 | - Added interface `Http\Discovery\Exception` which is implemented by all our exceptions 248 | 249 | ### Changed 250 | 251 | - Puli strategy renamed to Puli Beta strategy to prevent incompatibility with a future Puli stable 252 | 253 | ### Deprecated 254 | 255 | - For BC reasons, the old `Http\Discovery\NotFoundException` (extending the new exception) will be thrown until version 2.0 256 | 257 | 258 | ## 0.9.1 - 2016-06-28 259 | 260 | ### Changed 261 | 262 | - Dropping PHP 5.4 support because we use the ::class constant. 263 | 264 | 265 | ## 0.9.0 - 2016-06-25 266 | 267 | ### Added 268 | 269 | - Discovery strategies to find classes 270 | 271 | ### Changed 272 | 273 | - [Puli](http://puli.io) made optional 274 | - Improved exceptions 275 | - **[BC] `NotFoundException` moved to `Http\Discovery\Exception\NotFoundException`** 276 | 277 | 278 | ## 0.8.0 - 2016-02-11 279 | 280 | ### Changed 281 | 282 | - Puli composer plugin must be installed separately 283 | 284 | 285 | ## 0.7.0 - 2016-01-15 286 | 287 | ### Added 288 | 289 | - Temporary puli.phar (Beta 10) executable 290 | 291 | ### Changed 292 | 293 | - Updated HTTPlug dependencies 294 | - Updated Puli dependencies 295 | - Local configuration to make tests passing 296 | 297 | ### Removed 298 | 299 | - Puli CLI dependency 300 | 301 | 302 | ## 0.6.4 - 2016-01-07 303 | 304 | ### Fixed 305 | 306 | - Puli [not working](https://twitter.com/PuliPHP/status/685132540588507137) with the latest json-schema 307 | 308 | 309 | ## 0.6.3 - 2016-01-04 310 | 311 | ### Changed 312 | 313 | - Adjust Puli dependencies 314 | 315 | 316 | ## 0.6.2 - 2016-01-04 317 | 318 | ### Changed 319 | 320 | - Make Puli CLI a requirement 321 | 322 | 323 | ## 0.6.1 - 2016-01-03 324 | 325 | ### Changed 326 | 327 | - More flexible Puli requirement 328 | 329 | 330 | ## 0.6.0 - 2015-12-30 331 | 332 | ### Changed 333 | 334 | - Use [Puli](http://puli.io) for discovery 335 | - Improved exception messages 336 | 337 | 338 | ## 0.5.0 - 2015-12-25 339 | 340 | ### Changed 341 | 342 | - Updated message factory dependency (php-http/message) 343 | 344 | 345 | ## 0.4.0 - 2015-12-17 346 | 347 | ### Added 348 | 349 | - Array condition evaluation in the Class Discovery 350 | 351 | ### Removed 352 | 353 | - Message factories (moved to php-http/utils) 354 | 355 | 356 | ## 0.3.0 - 2015-11-18 357 | 358 | ### Added 359 | 360 | - HTTP Async Client Discovery 361 | - Stream factories 362 | 363 | ### Changed 364 | 365 | - Discoveries and Factories are final 366 | - Message and Uri factories have the type in their names 367 | - Diactoros Message factory uses Stream factory internally 368 | 369 | ### Fixed 370 | 371 | - Improved docblocks for API documentation generation 372 | 373 | 374 | ## 0.2.0 - 2015-10-31 375 | 376 | ### Changed 377 | 378 | - Renamed AdapterDiscovery to ClientDiscovery 379 | 380 | 381 | ## 0.1.1 - 2015-06-13 382 | 383 | ### Fixed 384 | 385 | - Bad HTTP Adapter class name for Guzzle 5 386 | 387 | 388 | ## 0.1.0 - 2015-06-12 389 | 390 | ### Added 391 | 392 | - Initial release 393 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 PHP HTTP Team 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTPlug Discovery 2 | 3 | [![Latest Version](https://img.shields.io/github/release/php-http/discovery.svg?style=flat-square)](https://github.com/php-http/discovery/releases) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 5 | [![Tests](https://github.com/php-http/discovery/actions/workflows/ci.yml/badge.svg?branch=1.x)](https://github.com/php-http/discovery/actions/workflows/ci.yml?query=branch%3A1.x) 6 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/discovery.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/discovery) 7 | [![Quality Score](https://img.shields.io/scrutinizer/g/php-http/discovery.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/discovery) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/php-http/discovery.svg?style=flat-square)](https://packagist.org/packages/php-http/discovery) 9 | 10 | **This library provides auto-discovery and auto-installation of well-known PSR-17, PSR-18 and HTTPlug implementations.** 11 | 12 | 13 | ## Install 14 | 15 | Via Composer 16 | 17 | ``` bash 18 | composer require php-http/discovery 19 | ``` 20 | 21 | 22 | ## Usage as a library author 23 | 24 | Please see the [official documentation](http://php-http.readthedocs.org/en/latest/discovery.html). 25 | 26 | If your library/SDK needs a PSR-18 client, here is a quick example. 27 | 28 | First, you need to install a PSR-18 client and a PSR-17 factory implementations. 29 | This should be done only for dev dependencies as you don't want to force a 30 | specific implementation on your users: 31 | 32 | ```bash 33 | composer require --dev symfony/http-client 34 | composer require --dev nyholm/psr7 35 | ``` 36 | 37 | Then, you can disable the Composer plugin embeded in `php-http/discovery` 38 | because you just installed the dev dependencies you need for testing: 39 | 40 | ```bash 41 | composer config allow-plugins.php-http/discovery false 42 | ``` 43 | 44 | Finally, you need to require `php-http/discovery` and the generic implementations 45 | that your library is going to need: 46 | 47 | ```bash 48 | composer require 'php-http/discovery:^1.17' 49 | composer require 'psr/http-client-implementation:*' 50 | composer require 'psr/http-factory-implementation:*' 51 | ``` 52 | 53 | Now, you're ready to make an HTTP request: 54 | 55 | ```php 56 | use Http\Discovery\Psr18Client; 57 | 58 | $client = new Psr18Client(); 59 | 60 | $request = $client->createRequest('GET', 'https://example.com'); 61 | $response = $client->sendRequest($request); 62 | ``` 63 | 64 | Internally, this code will use whatever PSR-7, PSR-17 and PSR-18 implementations 65 | that your users have installed. 66 | 67 | 68 | ## Usage as a library user 69 | 70 | If you use a library/SDK that requires `php-http/discovery`, you can configure 71 | the auto-discovery mechanism to use a specific implementation when many are 72 | available in your project. 73 | 74 | For example, if you have both `nyholm/psr7` and `guzzlehttp/guzzle` in your 75 | project, you can tell `php-http/discovery` to use `guzzlehttp/guzzle` instead of 76 | `nyholm/psr7` by running the following command: 77 | 78 | ```bash 79 | composer config extra.discovery.psr/http-factory-implementation GuzzleHttp\\Psr7\\HttpFactory 80 | ``` 81 | 82 | This will update your `composer.json` file to add the following configuration: 83 | 84 | ```json 85 | { 86 | "extra": { 87 | "discovery": { 88 | "psr/http-factory-implementation": "GuzzleHttp\\Psr7\\HttpFactory" 89 | } 90 | } 91 | } 92 | ``` 93 | 94 | Don't forget to run `composer install` to apply the changes, and ensure that 95 | the composer plugin is enabled: 96 | 97 | ```bash 98 | composer config allow-plugins.php-http/discovery true 99 | composer install 100 | ``` 101 | 102 | 103 | ## Testing 104 | 105 | ``` bash 106 | composer test 107 | ``` 108 | 109 | 110 | ## Contributing 111 | 112 | Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html). 113 | 114 | 115 | ## Security 116 | 117 | If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org). 118 | 119 | 120 | ## License 121 | 122 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 123 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-http/discovery", 3 | "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", 4 | "type": "composer-plugin", 5 | "license": "MIT", 6 | "keywords": ["http", "discovery", "client", "adapter", "message", "factory", "psr7", "psr17"], 7 | "homepage": "http://php-http.org", 8 | "authors": [ 9 | { 10 | "name": "Márk Sági-Kazár", 11 | "email": "mark.sagikazar@gmail.com" 12 | } 13 | ], 14 | "provide": { 15 | "php-http/async-client-implementation": "*", 16 | "php-http/client-implementation": "*", 17 | "psr/http-client-implementation": "*", 18 | "psr/http-factory-implementation": "*", 19 | "psr/http-message-implementation": "*" 20 | }, 21 | "require": { 22 | "php": "^7.1 || ^8.0", 23 | "composer-plugin-api": "^1.0|^2.0" 24 | }, 25 | "require-dev": { 26 | "composer/composer": "^1.0.2|^2.0", 27 | "graham-campbell/phpspec-skip-example-extension": "^5.0", 28 | "php-http/httplug": "^1.0 || ^2.0", 29 | "php-http/message-factory": "^1.0", 30 | "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", 31 | "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1", 32 | "sebastian/comparator": "^3.0.5 || ^4.0.8" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Http\\Discovery\\": "src/" 37 | }, 38 | "exclude-from-classmap": [ 39 | "src/Composer/Plugin.php" 40 | ] 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "spec\\Http\\Discovery\\": "spec/" 45 | } 46 | }, 47 | "scripts": { 48 | "test": [ 49 | "vendor/bin/phpspec run", 50 | "vendor/bin/simple-phpunit --group NothingInstalled" 51 | ], 52 | "test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml" 53 | }, 54 | "extra": { 55 | "class": "Http\\Discovery\\Composer\\Plugin", 56 | "plugin-optional": true 57 | }, 58 | "conflict": { 59 | "nyholm/psr7": "<1.0", 60 | "zendframework/zend-diactoros": "*" 61 | }, 62 | "prefer-stable": true, 63 | "minimum-stability": "beta" 64 | } 65 | -------------------------------------------------------------------------------- /src/ClassDiscovery.php: -------------------------------------------------------------------------------- 1 | 15 | * @author Márk Sági-Kazár 16 | * @author Tobias Nyholm 17 | */ 18 | abstract class ClassDiscovery 19 | { 20 | /** 21 | * A list of strategies to find classes. 22 | * 23 | * @var DiscoveryStrategy[] 24 | */ 25 | private static $strategies = [ 26 | Strategy\GeneratedDiscoveryStrategy::class, 27 | Strategy\CommonClassesStrategy::class, 28 | Strategy\CommonPsr17ClassesStrategy::class, 29 | Strategy\PuliBetaStrategy::class, 30 | ]; 31 | 32 | private static $deprecatedStrategies = [ 33 | Strategy\PuliBetaStrategy::class => true, 34 | ]; 35 | 36 | /** 37 | * Discovery cache to make the second time we use discovery faster. 38 | * 39 | * @var array 40 | */ 41 | private static $cache = []; 42 | 43 | /** 44 | * Finds a class. 45 | * 46 | * @param string $type 47 | * 48 | * @return string|\Closure 49 | * 50 | * @throws DiscoveryFailedException 51 | */ 52 | protected static function findOneByType($type) 53 | { 54 | // Look in the cache 55 | if (null !== ($class = self::getFromCache($type))) { 56 | return $class; 57 | } 58 | 59 | static $skipStrategy; 60 | $skipStrategy ?? $skipStrategy = self::safeClassExists(Strategy\GeneratedDiscoveryStrategy::class) ? false : Strategy\GeneratedDiscoveryStrategy::class; 61 | 62 | $exceptions = []; 63 | foreach (self::$strategies as $strategy) { 64 | if ($skipStrategy === $strategy) { 65 | continue; 66 | } 67 | 68 | try { 69 | $candidates = $strategy::getCandidates($type); 70 | } catch (StrategyUnavailableException $e) { 71 | if (!isset(self::$deprecatedStrategies[$strategy])) { 72 | $exceptions[] = $e; 73 | } 74 | 75 | continue; 76 | } 77 | 78 | foreach ($candidates as $candidate) { 79 | if (isset($candidate['condition'])) { 80 | if (!self::evaluateCondition($candidate['condition'])) { 81 | continue; 82 | } 83 | } 84 | 85 | // save the result for later use 86 | self::storeInCache($type, $candidate); 87 | 88 | return $candidate['class']; 89 | } 90 | 91 | $exceptions[] = new NoCandidateFoundException($strategy, $candidates); 92 | } 93 | 94 | throw DiscoveryFailedException::create($exceptions); 95 | } 96 | 97 | /** 98 | * Get a value from cache. 99 | * 100 | * @param string $type 101 | * 102 | * @return string|null 103 | */ 104 | private static function getFromCache($type) 105 | { 106 | if (!isset(self::$cache[$type])) { 107 | return; 108 | } 109 | 110 | $candidate = self::$cache[$type]; 111 | if (isset($candidate['condition'])) { 112 | if (!self::evaluateCondition($candidate['condition'])) { 113 | return; 114 | } 115 | } 116 | 117 | return $candidate['class']; 118 | } 119 | 120 | /** 121 | * Store a value in cache. 122 | * 123 | * @param string $type 124 | * @param string $class 125 | */ 126 | private static function storeInCache($type, $class) 127 | { 128 | self::$cache[$type] = $class; 129 | } 130 | 131 | /** 132 | * Set new strategies and clear the cache. 133 | * 134 | * @param string[] $strategies list of fully qualified class names that implement DiscoveryStrategy 135 | */ 136 | public static function setStrategies(array $strategies) 137 | { 138 | self::$strategies = $strategies; 139 | self::clearCache(); 140 | } 141 | 142 | /** 143 | * Returns the currently configured discovery strategies as fully qualified class names. 144 | * 145 | * @return string[] 146 | */ 147 | public static function getStrategies(): iterable 148 | { 149 | return self::$strategies; 150 | } 151 | 152 | /** 153 | * Append a strategy at the end of the strategy queue. 154 | * 155 | * @param string $strategy Fully qualified class name of a DiscoveryStrategy 156 | */ 157 | public static function appendStrategy($strategy) 158 | { 159 | self::$strategies[] = $strategy; 160 | self::clearCache(); 161 | } 162 | 163 | /** 164 | * Prepend a strategy at the beginning of the strategy queue. 165 | * 166 | * @param string $strategy Fully qualified class name to a DiscoveryStrategy 167 | */ 168 | public static function prependStrategy($strategy) 169 | { 170 | array_unshift(self::$strategies, $strategy); 171 | self::clearCache(); 172 | } 173 | 174 | public static function clearCache() 175 | { 176 | self::$cache = []; 177 | } 178 | 179 | /** 180 | * Evaluates conditions to boolean. 181 | * 182 | * @return bool 183 | */ 184 | protected static function evaluateCondition($condition) 185 | { 186 | if (is_string($condition)) { 187 | // Should be extended for functions, extensions??? 188 | return self::safeClassExists($condition); 189 | } 190 | if (is_callable($condition)) { 191 | return (bool) $condition(); 192 | } 193 | if (is_bool($condition)) { 194 | return $condition; 195 | } 196 | if (is_array($condition)) { 197 | foreach ($condition as $c) { 198 | if (false === static::evaluateCondition($c)) { 199 | // Immediately stop execution if the condition is false 200 | return false; 201 | } 202 | } 203 | 204 | return true; 205 | } 206 | 207 | return false; 208 | } 209 | 210 | /** 211 | * Get an instance of the $class. 212 | * 213 | * @param string|\Closure $class a FQCN of a class or a closure that instantiate the class 214 | * 215 | * @return object 216 | * 217 | * @throws ClassInstantiationFailedException 218 | */ 219 | protected static function instantiateClass($class) 220 | { 221 | try { 222 | if (is_string($class)) { 223 | return new $class(); 224 | } 225 | 226 | if (is_callable($class)) { 227 | return $class(); 228 | } 229 | } catch (\Exception $e) { 230 | throw new ClassInstantiationFailedException('Unexpected exception when instantiating class.', 0, $e); 231 | } 232 | 233 | throw new ClassInstantiationFailedException('Could not instantiate class because parameter is neither a callable nor a string'); 234 | } 235 | 236 | /** 237 | * We need a "safe" version of PHP's "class_exists" because Magento has a bug 238 | * (or they call it a "feature"). Magento is throwing an exception if you do class_exists() 239 | * on a class that ends with "Factory" and if that file does not exits. 240 | * 241 | * This function catches all potential exceptions and makes sure to always return a boolean. 242 | * 243 | * @param string $class 244 | * 245 | * @return bool 246 | */ 247 | public static function safeClassExists($class) 248 | { 249 | try { 250 | return class_exists($class) || interface_exists($class); 251 | } catch (\Exception $e) { 252 | return false; 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/Composer/Plugin.php: -------------------------------------------------------------------------------- 1 | 36 | * 37 | * @internal 38 | */ 39 | class Plugin implements PluginInterface, EventSubscriberInterface 40 | { 41 | /** 42 | * Describes, for every supported virtual implementation, which packages 43 | * provide said implementation and which extra dependencies each package 44 | * requires to provide the implementation. 45 | */ 46 | private const PROVIDE_RULES = [ 47 | 'php-http/async-client-implementation' => [ 48 | 'symfony/http-client:>=6.3' => ['guzzlehttp/promises', 'psr/http-factory-implementation', 'php-http/httplug'], 49 | 'symfony/http-client' => ['guzzlehttp/promises', 'php-http/message-factory', 'psr/http-factory-implementation', 'php-http/httplug'], 50 | 'php-http/guzzle7-adapter' => [], 51 | 'php-http/guzzle6-adapter' => [], 52 | 'php-http/curl-client' => [], 53 | 'php-http/react-adapter' => [], 54 | ], 55 | 'php-http/client-implementation' => [ 56 | 'symfony/http-client:>=6.3' => ['psr/http-factory-implementation', 'php-http/httplug'], 57 | 'symfony/http-client' => ['php-http/message-factory', 'psr/http-factory-implementation', 'php-http/httplug'], 58 | 'php-http/guzzle7-adapter' => [], 59 | 'php-http/guzzle6-adapter' => [], 60 | 'php-http/cakephp-adapter' => [], 61 | 'php-http/curl-client' => [], 62 | 'php-http/react-adapter' => [], 63 | 'php-http/buzz-adapter' => [], 64 | 'php-http/artax-adapter' => [], 65 | 'kriswallsmith/buzz:^1' => [], 66 | ], 67 | 'psr/http-client-implementation' => [ 68 | 'symfony/http-client' => ['psr/http-factory-implementation', 'psr/http-client'], 69 | 'guzzlehttp/guzzle' => [], 70 | 'kriswallsmith/buzz:^1' => [], 71 | ], 72 | 'psr/http-message-implementation' => [ 73 | 'php-http/discovery' => ['psr/http-factory-implementation'], 74 | ], 75 | 'psr/http-factory-implementation' => [ 76 | 'nyholm/psr7' => [], 77 | 'guzzlehttp/psr7:>=2' => [], 78 | 'slim/psr7' => [], 79 | 'laminas/laminas-diactoros' => [], 80 | 'phalcon/cphalcon:^4' => [], 81 | 'http-interop/http-factory-guzzle' => [], 82 | 'http-interop/http-factory-diactoros' => [], 83 | 'http-interop/http-factory-slim' => [], 84 | 'httpsoft/http-message' => [], 85 | ], 86 | ]; 87 | 88 | /** 89 | * Describes which package should be preferred on the left side 90 | * depending on which one is already installed on the right side. 91 | */ 92 | private const STICKYNESS_RULES = [ 93 | 'symfony/http-client' => 'symfony/framework-bundle', 94 | 'php-http/guzzle7-adapter' => 'guzzlehttp/guzzle:^7', 95 | 'php-http/guzzle6-adapter' => 'guzzlehttp/guzzle:^6', 96 | 'php-http/guzzle5-adapter' => 'guzzlehttp/guzzle:^5', 97 | 'php-http/cakephp-adapter' => 'cakephp/cakephp', 98 | 'php-http/react-adapter' => 'react/event-loop', 99 | 'php-http/buzz-adapter' => 'kriswallsmith/buzz:^0.15.1', 100 | 'php-http/artax-adapter' => 'amphp/artax:^3', 101 | 'http-interop/http-factory-guzzle' => 'guzzlehttp/psr7:^1', 102 | 'http-interop/http-factory-slim' => 'slim/slim:^3', 103 | ]; 104 | 105 | private const INTERFACE_MAP = [ 106 | 'php-http/async-client-implementation' => [ 107 | 'Http\Client\HttpAsyncClient', 108 | ], 109 | 'php-http/client-implementation' => [ 110 | 'Http\Client\HttpClient', 111 | ], 112 | 'psr/http-client-implementation' => [ 113 | 'Psr\Http\Client\ClientInterface', 114 | ], 115 | 'psr/http-factory-implementation' => [ 116 | 'Psr\Http\Message\RequestFactoryInterface', 117 | 'Psr\Http\Message\ResponseFactoryInterface', 118 | 'Psr\Http\Message\ServerRequestFactoryInterface', 119 | 'Psr\Http\Message\StreamFactoryInterface', 120 | 'Psr\Http\Message\UploadedFileFactoryInterface', 121 | 'Psr\Http\Message\UriFactoryInterface', 122 | ], 123 | ]; 124 | 125 | public static function getSubscribedEvents(): array 126 | { 127 | return [ 128 | ScriptEvents::PRE_AUTOLOAD_DUMP => 'preAutoloadDump', 129 | ScriptEvents::POST_UPDATE_CMD => 'postUpdate', 130 | ]; 131 | } 132 | 133 | public function activate(Composer $composer, IOInterface $io): void 134 | { 135 | } 136 | 137 | public function deactivate(Composer $composer, IOInterface $io) 138 | { 139 | } 140 | 141 | public function uninstall(Composer $composer, IOInterface $io) 142 | { 143 | } 144 | 145 | public function postUpdate(Event $event) 146 | { 147 | $composer = $event->getComposer(); 148 | $repo = $composer->getRepositoryManager()->getLocalRepository(); 149 | $requires = [ 150 | $composer->getPackage()->getRequires(), 151 | $composer->getPackage()->getDevRequires(), 152 | ]; 153 | $pinnedAbstractions = []; 154 | $pinned = $composer->getPackage()->getExtra()['discovery'] ?? []; 155 | foreach (self::INTERFACE_MAP as $abstraction => $interfaces) { 156 | foreach (isset($pinned[$abstraction]) ? [] : $interfaces as $interface) { 157 | if (!isset($pinned[$interface])) { 158 | continue 2; 159 | } 160 | } 161 | $pinnedAbstractions[$abstraction] = true; 162 | } 163 | 164 | $missingRequires = $this->getMissingRequires($repo, $requires, 'project' === $composer->getPackage()->getType(), $pinnedAbstractions); 165 | $missingRequires = [ 166 | 'require' => array_fill_keys(array_merge([], ...array_values($missingRequires[0])), '*'), 167 | 'require-dev' => array_fill_keys(array_merge([], ...array_values($missingRequires[1])), '*'), 168 | 'remove' => array_fill_keys(array_merge([], ...array_values($missingRequires[2])), '*'), 169 | ]; 170 | 171 | if (!$missingRequires = array_filter($missingRequires)) { 172 | return; 173 | } 174 | 175 | $composerJsonContents = file_get_contents(Factory::getComposerFile()); 176 | $this->updateComposerJson($missingRequires, $composer->getConfig()->get('sort-packages')); 177 | 178 | $installer = null; 179 | // Find the composer installer, hack borrowed from symfony/flex 180 | foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT) as $trace) { 181 | if (isset($trace['object']) && $trace['object'] instanceof Installer) { 182 | $installer = $trace['object']; 183 | break; 184 | } 185 | } 186 | 187 | if (!$installer) { 188 | return; 189 | } 190 | 191 | $event->stopPropagation(); 192 | 193 | $dispatcher = $composer->getEventDispatcher(); 194 | $disableScripts = !method_exists($dispatcher, 'setRunScripts') || !((array) $dispatcher)["\0*\0runScripts"]; 195 | $composer = Factory::create($event->getIO(), null, false, $disableScripts); 196 | 197 | /** @var Installer $installer */ 198 | $installer = clone $installer; 199 | if (method_exists($installer, 'setAudit')) { 200 | $trace['object']->setAudit(false); 201 | } 202 | // we need a clone of the installer to preserve its configuration state but with our own service objects 203 | $installer->__construct( 204 | $event->getIO(), 205 | $composer->getConfig(), 206 | $composer->getPackage(), 207 | $composer->getDownloadManager(), 208 | $composer->getRepositoryManager(), 209 | $composer->getLocker(), 210 | $composer->getInstallationManager(), 211 | $composer->getEventDispatcher(), 212 | $composer->getAutoloadGenerator() 213 | ); 214 | if (method_exists($installer, 'setPlatformRequirementFilter')) { 215 | $installer->setPlatformRequirementFilter(((array) $trace['object'])["\0*\0platformRequirementFilter"]); 216 | } 217 | 218 | if (0 !== $installer->run()) { 219 | file_put_contents(Factory::getComposerFile(), $composerJsonContents); 220 | 221 | return; 222 | } 223 | 224 | $versionSelector = new VersionSelector(ClassDiscovery::safeClassExists(RepositorySet::class) ? new RepositorySet() : new Pool()); 225 | $updateComposerJson = false; 226 | 227 | foreach ($composer->getRepositoryManager()->getLocalRepository()->getPackages() as $package) { 228 | foreach (['require', 'require-dev'] as $key) { 229 | if (!isset($missingRequires[$key][$package->getName()])) { 230 | continue; 231 | } 232 | $updateComposerJson = true; 233 | $missingRequires[$key][$package->getName()] = $versionSelector->findRecommendedRequireVersion($package); 234 | } 235 | } 236 | 237 | if ($updateComposerJson) { 238 | $this->updateComposerJson($missingRequires, $composer->getConfig()->get('sort-packages')); 239 | $this->updateComposerLock($composer, $event->getIO()); 240 | } 241 | } 242 | 243 | public function getMissingRequires(InstalledRepositoryInterface $repo, array $requires, bool $isProject, array $pinnedAbstractions): array 244 | { 245 | $allPackages = []; 246 | $devPackages = method_exists($repo, 'getDevPackageNames') ? array_fill_keys($repo->getDevPackageNames(), true) : []; 247 | 248 | // One must require "php-http/discovery" 249 | // to opt-in for auto-installation of virtual package implementations 250 | if (!isset($requires[0]['php-http/discovery'])) { 251 | $requires = [[], []]; 252 | } 253 | 254 | foreach ($repo->getPackages() as $package) { 255 | $allPackages[$package->getName()] = true; 256 | 257 | if (1 < \count($names = $package->getNames(false))) { 258 | $allPackages += array_fill_keys($names, false); 259 | 260 | if (isset($devPackages[$package->getName()])) { 261 | $devPackages += $names; 262 | } 263 | } 264 | 265 | if (isset($package->getRequires()['php-http/discovery'])) { 266 | $requires[(int) isset($devPackages[$package->getName()])] += $package->getRequires(); 267 | } 268 | } 269 | 270 | $missingRequires = [[], [], []]; 271 | $versionParser = new VersionParser(); 272 | 273 | if (ClassDiscovery::safeClassExists(\Phalcon\Http\Message\RequestFactory::class, false)) { 274 | $missingRequires[0]['psr/http-factory-implementation'] = []; 275 | $missingRequires[1]['psr/http-factory-implementation'] = []; 276 | } 277 | 278 | foreach ($requires as $dev => $rules) { 279 | $abstractions = []; 280 | $rules = array_intersect_key(self::PROVIDE_RULES, $rules); 281 | 282 | while ($rules) { 283 | $abstraction = key($rules); 284 | 285 | if (isset($pinnedAbstractions[$abstraction])) { 286 | unset($rules[$abstraction]); 287 | continue; 288 | } 289 | 290 | $abstractions[] = $abstraction; 291 | 292 | foreach (array_shift($rules) as $candidate => $deps) { 293 | [$candidate, $version] = explode(':', $candidate, 2) + [1 => null]; 294 | 295 | if (!isset($allPackages[$candidate])) { 296 | continue; 297 | } 298 | if (null !== $version && !$repo->findPackage($candidate, $versionParser->parseConstraints($version))) { 299 | continue; 300 | } 301 | if ($isProject && !$dev && isset($devPackages[$candidate])) { 302 | $missingRequires[0][$abstraction] = [$candidate]; 303 | $missingRequires[2][$abstraction] = [$candidate]; 304 | } else { 305 | $missingRequires[$dev][$abstraction] = []; 306 | } 307 | 308 | foreach ($deps as $dep) { 309 | if (isset(self::PROVIDE_RULES[$dep])) { 310 | $rules[$dep] = self::PROVIDE_RULES[$dep]; 311 | } elseif (!isset($allPackages[$dep])) { 312 | $missingRequires[$dev][$abstraction][] = $dep; 313 | } elseif ($isProject && !$dev && isset($devPackages[$dep])) { 314 | $missingRequires[0][$abstraction][] = $dep; 315 | $missingRequires[2][$abstraction][] = $dep; 316 | } 317 | } 318 | break; 319 | } 320 | } 321 | 322 | while ($abstractions) { 323 | $abstraction = array_shift($abstractions); 324 | 325 | if (isset($missingRequires[$dev][$abstraction])) { 326 | continue; 327 | } 328 | $candidates = self::PROVIDE_RULES[$abstraction]; 329 | 330 | foreach ($candidates as $candidate => $deps) { 331 | [$candidate, $version] = explode(':', $candidate, 2) + [1 => null]; 332 | 333 | if (null !== $version && !$repo->findPackage($candidate, $versionParser->parseConstraints($version))) { 334 | continue; 335 | } 336 | if (isset($allPackages[$candidate]) && (!$isProject || $dev || !isset($devPackages[$candidate]))) { 337 | continue 2; 338 | } 339 | } 340 | 341 | foreach (array_intersect_key(self::STICKYNESS_RULES, $candidates) as $candidate => $stickyRule) { 342 | [$stickyName, $stickyVersion] = explode(':', $stickyRule, 2) + [1 => null]; 343 | if (!isset($allPackages[$stickyName]) || ($isProject && !$dev && isset($devPackages[$stickyName]))) { 344 | continue; 345 | } 346 | if (null !== $stickyVersion && !$repo->findPackage($stickyName, $versionParser->parseConstraints($stickyVersion))) { 347 | continue; 348 | } 349 | 350 | $candidates = [$candidate => $candidates[$candidate]]; 351 | break; 352 | } 353 | 354 | $dep = key($candidates); 355 | [$dep] = explode(':', $dep, 2); 356 | $missingRequires[$dev][$abstraction] = [$dep]; 357 | 358 | if ($isProject && !$dev && isset($devPackages[$dep])) { 359 | $missingRequires[2][$abstraction][] = $dep; 360 | } 361 | } 362 | } 363 | 364 | $missingRequires[1] = array_diff_key($missingRequires[1], $missingRequires[0]); 365 | 366 | return $missingRequires; 367 | } 368 | 369 | public function preAutoloadDump(Event $event) 370 | { 371 | $filesystem = new Filesystem(); 372 | // Double realpath() on purpose, see https://bugs.php.net/72738 373 | $vendorDir = $filesystem->normalizePath(realpath(realpath($event->getComposer()->getConfig()->get('vendor-dir')))); 374 | $filesystem->ensureDirectoryExists($vendorDir.'/composer'); 375 | $pinned = $event->getComposer()->getPackage()->getExtra()['discovery'] ?? []; 376 | $candidates = []; 377 | 378 | $allInterfaces = array_merge(...array_values(self::INTERFACE_MAP)); 379 | foreach ($pinned as $abstraction => $class) { 380 | if (isset(self::INTERFACE_MAP[$abstraction])) { 381 | $interfaces = self::INTERFACE_MAP[$abstraction]; 382 | } elseif (false !== $k = array_search($abstraction, $allInterfaces, true)) { 383 | $interfaces = [$allInterfaces[$k]]; 384 | } else { 385 | throw new \UnexpectedValueException(sprintf('Invalid "extra.discovery" pinned in composer.json: "%s" is not one of ["%s"].', $abstraction, implode('", "', array_keys(self::INTERFACE_MAP)))); 386 | } 387 | 388 | foreach ($interfaces as $interface) { 389 | $candidates[] = sprintf("case %s: return [['class' => %s]];\n", var_export($interface, true), var_export($class, true)); 390 | } 391 | } 392 | 393 | $file = $vendorDir.'/composer/GeneratedDiscoveryStrategy.php'; 394 | 395 | if (!$candidates) { 396 | if (file_exists($file)) { 397 | unlink($file); 398 | } 399 | 400 | return; 401 | } 402 | 403 | $candidates = implode(' ', $candidates); 404 | $code = <<getComposer()->getPackage(); 428 | $autoload = $rootPackage->getAutoload(); 429 | $autoload['classmap'][] = $vendorDir.'/composer/GeneratedDiscoveryStrategy.php'; 430 | $rootPackage->setAutoload($autoload); 431 | } 432 | 433 | private function updateComposerJson(array $missingRequires, bool $sortPackages) 434 | { 435 | $file = Factory::getComposerFile(); 436 | $contents = file_get_contents($file); 437 | 438 | $manipulator = new JsonManipulator($contents); 439 | 440 | foreach ($missingRequires as $key => $packages) { 441 | foreach ($packages as $package => $constraint) { 442 | if ('remove' === $key) { 443 | $manipulator->removeSubNode('require-dev', $package); 444 | } else { 445 | $manipulator->addLink($key, $package, $constraint, $sortPackages); 446 | } 447 | } 448 | } 449 | 450 | file_put_contents($file, $manipulator->getContents()); 451 | } 452 | 453 | private function updateComposerLock(Composer $composer, IOInterface $io) 454 | { 455 | if (false === $composer->getConfig()->get('lock')) { 456 | return; 457 | } 458 | 459 | $lock = substr(Factory::getComposerFile(), 0, -4).'lock'; 460 | $composerJson = file_get_contents(Factory::getComposerFile()); 461 | $lockFile = new JsonFile($lock, null, $io); 462 | $locker = ClassDiscovery::safeClassExists(RepositorySet::class) 463 | ? new Locker($io, $lockFile, $composer->getInstallationManager(), $composerJson) 464 | : new Locker($io, $lockFile, $composer->getRepositoryManager(), $composer->getInstallationManager(), $composerJson); 465 | 466 | if (!$locker->isLocked()) { 467 | return; 468 | } 469 | 470 | $lockData = $locker->getLockData(); 471 | $lockData['content-hash'] = Locker::getContentHash($composerJson); 472 | $lockFile->write($lockData); 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface Exception extends \Throwable 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Exception/ClassInstantiationFailedException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ClassInstantiationFailedException extends \RuntimeException implements Exception 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/DiscoveryFailedException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class DiscoveryFailedException extends \Exception implements Exception 13 | { 14 | /** 15 | * @var \Exception[] 16 | */ 17 | private $exceptions; 18 | 19 | /** 20 | * @param string $message 21 | * @param \Exception[] $exceptions 22 | */ 23 | public function __construct($message, array $exceptions = []) 24 | { 25 | $this->exceptions = $exceptions; 26 | 27 | parent::__construct($message); 28 | } 29 | 30 | /** 31 | * @param \Exception[] $exceptions 32 | */ 33 | public static function create($exceptions) 34 | { 35 | $message = 'Could not find resource using any discovery strategy. Find more information at http://docs.php-http.org/en/latest/discovery.html#common-errors'; 36 | foreach ($exceptions as $e) { 37 | $message .= "\n - ".$e->getMessage(); 38 | } 39 | $message .= "\n\n"; 40 | 41 | return new self($message, $exceptions); 42 | } 43 | 44 | /** 45 | * @return \Exception[] 46 | */ 47 | public function getExceptions() 48 | { 49 | return $this->exceptions; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Exception/NoCandidateFoundException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class NoCandidateFoundException extends \Exception implements Exception 13 | { 14 | /** 15 | * @param string $strategy 16 | */ 17 | public function __construct($strategy, array $candidates) 18 | { 19 | $classes = array_map( 20 | function ($a) { 21 | return $a['class']; 22 | }, 23 | $candidates 24 | ); 25 | 26 | $message = sprintf( 27 | 'No valid candidate found using strategy "%s". We tested the following candidates: %s.', 28 | $strategy, 29 | implode(', ', array_map([$this, 'stringify'], $classes)) 30 | ); 31 | 32 | parent::__construct($message); 33 | } 34 | 35 | private function stringify($mixed) 36 | { 37 | if (is_string($mixed)) { 38 | return $mixed; 39 | } 40 | 41 | if (is_array($mixed) && 2 === count($mixed)) { 42 | return sprintf('%s::%s', $this->stringify($mixed[0]), $mixed[1]); 43 | } 44 | 45 | return is_object($mixed) ? get_class($mixed) : gettype($mixed); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Exception/NotFoundException.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | /* final */ class NotFoundException extends \RuntimeException implements Exception 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Exception/PuliUnavailableException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class PuliUnavailableException extends StrategyUnavailableException 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Exception/StrategyUnavailableException.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class StrategyUnavailableException extends \RuntimeException implements Exception 14 | { 15 | } 16 | -------------------------------------------------------------------------------- /src/HttpAsyncClientDiscovery.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class HttpAsyncClientDiscovery extends ClassDiscovery 14 | { 15 | /** 16 | * Finds an HTTP Async Client. 17 | * 18 | * @return HttpAsyncClient 19 | * 20 | * @throws Exception\NotFoundException 21 | */ 22 | public static function find() 23 | { 24 | try { 25 | $asyncClient = static::findOneByType(HttpAsyncClient::class); 26 | } catch (DiscoveryFailedException $e) { 27 | throw new NotFoundException('No HTTPlug async clients found. Make sure to install a package providing "php-http/async-client-implementation". Example: "php-http/guzzle6-adapter".', 0, $e); 28 | } 29 | 30 | return static::instantiateClass($asyncClient); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/HttpClientDiscovery.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * @deprecated This will be removed in 2.0. Consider using Psr18ClientDiscovery. 14 | */ 15 | final class HttpClientDiscovery extends ClassDiscovery 16 | { 17 | /** 18 | * Finds an HTTP Client. 19 | * 20 | * @return HttpClient 21 | * 22 | * @throws Exception\NotFoundException 23 | */ 24 | public static function find() 25 | { 26 | try { 27 | $client = static::findOneByType(HttpClient::class); 28 | } catch (DiscoveryFailedException $e) { 29 | throw new NotFoundException('No HTTPlug clients found. Make sure to install a package providing "php-http/client-implementation". Example: "php-http/guzzle6-adapter".', 0, $e); 30 | } 31 | 32 | return static::instantiateClass($client); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/MessageFactoryDiscovery.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery. 14 | */ 15 | final class MessageFactoryDiscovery extends ClassDiscovery 16 | { 17 | /** 18 | * Finds a Message Factory. 19 | * 20 | * @return MessageFactory 21 | * 22 | * @throws Exception\NotFoundException 23 | */ 24 | public static function find() 25 | { 26 | try { 27 | $messageFactory = static::findOneByType(MessageFactory::class); 28 | } catch (DiscoveryFailedException $e) { 29 | throw new NotFoundException('No php-http message factories found. Note that the php-http message factories are deprecated in favor of the PSR-17 message factories. To use the legacy Guzzle, Diactoros or Slim Framework factories of php-http, install php-http/message and php-http/message-factory and the chosen message implementation.', 0, $e); 30 | } 31 | 32 | return static::instantiateClass($messageFactory); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/NotFoundException.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @deprecated since since version 1.0, and will be removed in 2.0. Use {@link \Http\Discovery\Exception\NotFoundException} instead. 13 | */ 14 | final class NotFoundException extends RealNotFoundException 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Psr17Factory.php: -------------------------------------------------------------------------------- 1 | 33 | * Copyright (c) 2015 Michael Dowling 34 | * Copyright (c) 2015 Márk Sági-Kazár 35 | * Copyright (c) 2015 Graham Campbell 36 | * Copyright (c) 2016 Tobias Schultze 37 | * Copyright (c) 2016 George Mponos 38 | * Copyright (c) 2016-2018 Tobias Nyholm 39 | * 40 | * @author Nicolas Grekas 41 | */ 42 | class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface 43 | { 44 | private $requestFactory; 45 | private $responseFactory; 46 | private $serverRequestFactory; 47 | private $streamFactory; 48 | private $uploadedFileFactory; 49 | private $uriFactory; 50 | 51 | public function __construct( 52 | ?RequestFactoryInterface $requestFactory = null, 53 | ?ResponseFactoryInterface $responseFactory = null, 54 | ?ServerRequestFactoryInterface $serverRequestFactory = null, 55 | ?StreamFactoryInterface $streamFactory = null, 56 | ?UploadedFileFactoryInterface $uploadedFileFactory = null, 57 | ?UriFactoryInterface $uriFactory = null 58 | ) { 59 | $this->requestFactory = $requestFactory; 60 | $this->responseFactory = $responseFactory; 61 | $this->serverRequestFactory = $serverRequestFactory; 62 | $this->streamFactory = $streamFactory; 63 | $this->uploadedFileFactory = $uploadedFileFactory; 64 | $this->uriFactory = $uriFactory; 65 | 66 | $this->setFactory($requestFactory); 67 | $this->setFactory($responseFactory); 68 | $this->setFactory($serverRequestFactory); 69 | $this->setFactory($streamFactory); 70 | $this->setFactory($uploadedFileFactory); 71 | $this->setFactory($uriFactory); 72 | } 73 | 74 | /** 75 | * @param UriInterface|string $uri 76 | */ 77 | public function createRequest(string $method, $uri): RequestInterface 78 | { 79 | $factory = $this->requestFactory ?? $this->setFactory(Psr17FactoryDiscovery::findRequestFactory()); 80 | 81 | return $factory->createRequest(...\func_get_args()); 82 | } 83 | 84 | public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface 85 | { 86 | $factory = $this->responseFactory ?? $this->setFactory(Psr17FactoryDiscovery::findResponseFactory()); 87 | 88 | return $factory->createResponse(...\func_get_args()); 89 | } 90 | 91 | /** 92 | * @param UriInterface|string $uri 93 | */ 94 | public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface 95 | { 96 | $factory = $this->serverRequestFactory ?? $this->setFactory(Psr17FactoryDiscovery::findServerRequestFactory()); 97 | 98 | return $factory->createServerRequest(...\func_get_args()); 99 | } 100 | 101 | public function createServerRequestFromGlobals(?array $server = null, ?array $get = null, ?array $post = null, ?array $cookie = null, ?array $files = null, ?StreamInterface $body = null): ServerRequestInterface 102 | { 103 | $server = $server ?? $_SERVER; 104 | $request = $this->createServerRequest($server['REQUEST_METHOD'] ?? 'GET', $this->createUriFromGlobals($server), $server); 105 | 106 | return $this->buildServerRequestFromGlobals($request, $server, $files ?? $_FILES) 107 | ->withQueryParams($get ?? $_GET) 108 | ->withParsedBody($post ?? $_POST) 109 | ->withCookieParams($cookie ?? $_COOKIE) 110 | ->withBody($body ?? $this->createStreamFromFile('php://input', 'r+')); 111 | } 112 | 113 | public function createStream(string $content = ''): StreamInterface 114 | { 115 | $factory = $this->streamFactory ?? $this->setFactory(Psr17FactoryDiscovery::findStreamFactory()); 116 | 117 | return $factory->createStream($content); 118 | } 119 | 120 | public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface 121 | { 122 | $factory = $this->streamFactory ?? $this->setFactory(Psr17FactoryDiscovery::findStreamFactory()); 123 | 124 | return $factory->createStreamFromFile($filename, $mode); 125 | } 126 | 127 | /** 128 | * @param resource $resource 129 | */ 130 | public function createStreamFromResource($resource): StreamInterface 131 | { 132 | $factory = $this->streamFactory ?? $this->setFactory(Psr17FactoryDiscovery::findStreamFactory()); 133 | 134 | return $factory->createStreamFromResource($resource); 135 | } 136 | 137 | public function createUploadedFile(StreamInterface $stream, ?int $size = null, int $error = \UPLOAD_ERR_OK, ?string $clientFilename = null, ?string $clientMediaType = null): UploadedFileInterface 138 | { 139 | $factory = $this->uploadedFileFactory ?? $this->setFactory(Psr17FactoryDiscovery::findUploadedFileFactory()); 140 | 141 | return $factory->createUploadedFile(...\func_get_args()); 142 | } 143 | 144 | public function createUri(string $uri = ''): UriInterface 145 | { 146 | $factory = $this->uriFactory ?? $this->setFactory(Psr17FactoryDiscovery::findUriFactory()); 147 | 148 | return $factory->createUri(...\func_get_args()); 149 | } 150 | 151 | public function createUriFromGlobals(?array $server = null): UriInterface 152 | { 153 | return $this->buildUriFromGlobals($this->createUri(''), $server ?? $_SERVER); 154 | } 155 | 156 | private function setFactory($factory) 157 | { 158 | if (!$this->requestFactory && $factory instanceof RequestFactoryInterface) { 159 | $this->requestFactory = $factory; 160 | } 161 | if (!$this->responseFactory && $factory instanceof ResponseFactoryInterface) { 162 | $this->responseFactory = $factory; 163 | } 164 | if (!$this->serverRequestFactory && $factory instanceof ServerRequestFactoryInterface) { 165 | $this->serverRequestFactory = $factory; 166 | } 167 | if (!$this->streamFactory && $factory instanceof StreamFactoryInterface) { 168 | $this->streamFactory = $factory; 169 | } 170 | if (!$this->uploadedFileFactory && $factory instanceof UploadedFileFactoryInterface) { 171 | $this->uploadedFileFactory = $factory; 172 | } 173 | if (!$this->uriFactory && $factory instanceof UriFactoryInterface) { 174 | $this->uriFactory = $factory; 175 | } 176 | 177 | return $factory; 178 | } 179 | 180 | private function buildServerRequestFromGlobals(ServerRequestInterface $request, array $server, array $files): ServerRequestInterface 181 | { 182 | $request = $request 183 | ->withProtocolVersion(isset($server['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $server['SERVER_PROTOCOL']) : '1.1') 184 | ->withUploadedFiles($this->normalizeFiles($files)); 185 | 186 | $headers = []; 187 | foreach ($server as $k => $v) { 188 | if (0 === strpos($k, 'HTTP_')) { 189 | $k = substr($k, 5); 190 | } elseif (!\in_array($k, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true)) { 191 | continue; 192 | } 193 | $k = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $k)))); 194 | 195 | $headers[$k] = $v; 196 | } 197 | 198 | if (!isset($headers['Authorization'])) { 199 | if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { 200 | $headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; 201 | } elseif (isset($_SERVER['PHP_AUTH_USER'])) { 202 | $headers['Authorization'] = 'Basic '.base64_encode($_SERVER['PHP_AUTH_USER'].':'.($_SERVER['PHP_AUTH_PW'] ?? '')); 203 | } elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) { 204 | $headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST']; 205 | } 206 | } 207 | 208 | foreach ($headers as $k => $v) { 209 | try { 210 | $request = $request->withHeader($k, $v); 211 | } catch (\InvalidArgumentException $e) { 212 | // ignore invalid headers 213 | } 214 | } 215 | 216 | return $request; 217 | } 218 | 219 | private function buildUriFromGlobals(UriInterface $uri, array $server): UriInterface 220 | { 221 | $uri = $uri->withScheme(!empty($server['HTTPS']) && 'off' !== strtolower($server['HTTPS']) ? 'https' : 'http'); 222 | 223 | $hasPort = false; 224 | if (isset($server['HTTP_HOST'])) { 225 | $parts = parse_url('http://'.$server['HTTP_HOST']); 226 | 227 | $uri = $uri->withHost($parts['host'] ?? 'localhost'); 228 | 229 | if ($parts['port'] ?? false) { 230 | $hasPort = true; 231 | $uri = $uri->withPort($parts['port']); 232 | } 233 | } else { 234 | $uri = $uri->withHost($server['SERVER_NAME'] ?? $server['SERVER_ADDR'] ?? 'localhost'); 235 | } 236 | 237 | if (!$hasPort && isset($server['SERVER_PORT'])) { 238 | $uri = $uri->withPort($server['SERVER_PORT']); 239 | } 240 | 241 | $hasQuery = false; 242 | if (isset($server['REQUEST_URI'])) { 243 | $requestUriParts = explode('?', $server['REQUEST_URI'], 2); 244 | $uri = $uri->withPath($requestUriParts[0]); 245 | if (isset($requestUriParts[1])) { 246 | $hasQuery = true; 247 | $uri = $uri->withQuery($requestUriParts[1]); 248 | } 249 | } 250 | 251 | if (!$hasQuery && isset($server['QUERY_STRING'])) { 252 | $uri = $uri->withQuery($server['QUERY_STRING']); 253 | } 254 | 255 | return $uri; 256 | } 257 | 258 | private function normalizeFiles(array $files): array 259 | { 260 | foreach ($files as $k => $v) { 261 | if ($v instanceof UploadedFileInterface) { 262 | continue; 263 | } 264 | if (!\is_array($v)) { 265 | unset($files[$k]); 266 | } elseif (!isset($v['tmp_name'])) { 267 | $files[$k] = $this->normalizeFiles($v); 268 | } else { 269 | $files[$k] = $this->createUploadedFileFromSpec($v); 270 | } 271 | } 272 | 273 | return $files; 274 | } 275 | 276 | /** 277 | * Create and return an UploadedFile instance from a $_FILES specification. 278 | * 279 | * @param array $value $_FILES struct 280 | * 281 | * @return UploadedFileInterface|UploadedFileInterface[] 282 | */ 283 | private function createUploadedFileFromSpec(array $value) 284 | { 285 | if (!is_array($tmpName = $value['tmp_name'])) { 286 | $file = is_file($tmpName) ? $this->createStreamFromFile($tmpName, 'r') : $this->createStream(); 287 | 288 | return $this->createUploadedFile($file, $value['size'], $value['error'], $value['name'], $value['type']); 289 | } 290 | 291 | foreach ($tmpName as $k => $v) { 292 | $tmpName[$k] = $this->createUploadedFileFromSpec([ 293 | 'tmp_name' => $v, 294 | 'size' => $value['size'][$k] ?? null, 295 | 'error' => $value['error'][$k] ?? null, 296 | 'name' => $value['name'][$k] ?? null, 297 | 'type' => $value['type'][$k] ?? null, 298 | ]); 299 | } 300 | 301 | return $tmpName; 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/Psr17FactoryDiscovery.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | final class Psr17FactoryDiscovery extends ClassDiscovery 20 | { 21 | private static function createException($type, Exception $e) 22 | { 23 | return new RealNotFoundException( 24 | 'No PSR-17 '.$type.' found. Install a package from this list: https://packagist.org/providers/psr/http-factory-implementation', 25 | 0, 26 | $e 27 | ); 28 | } 29 | 30 | /** 31 | * @return RequestFactoryInterface 32 | * 33 | * @throws RealNotFoundException 34 | */ 35 | public static function findRequestFactory() 36 | { 37 | try { 38 | $messageFactory = static::findOneByType(RequestFactoryInterface::class); 39 | } catch (DiscoveryFailedException $e) { 40 | throw self::createException('request factory', $e); 41 | } 42 | 43 | return static::instantiateClass($messageFactory); 44 | } 45 | 46 | /** 47 | * @return ResponseFactoryInterface 48 | * 49 | * @throws RealNotFoundException 50 | */ 51 | public static function findResponseFactory() 52 | { 53 | try { 54 | $messageFactory = static::findOneByType(ResponseFactoryInterface::class); 55 | } catch (DiscoveryFailedException $e) { 56 | throw self::createException('response factory', $e); 57 | } 58 | 59 | return static::instantiateClass($messageFactory); 60 | } 61 | 62 | /** 63 | * @return ServerRequestFactoryInterface 64 | * 65 | * @throws RealNotFoundException 66 | */ 67 | public static function findServerRequestFactory() 68 | { 69 | try { 70 | $messageFactory = static::findOneByType(ServerRequestFactoryInterface::class); 71 | } catch (DiscoveryFailedException $e) { 72 | throw self::createException('server request factory', $e); 73 | } 74 | 75 | return static::instantiateClass($messageFactory); 76 | } 77 | 78 | /** 79 | * @return StreamFactoryInterface 80 | * 81 | * @throws RealNotFoundException 82 | */ 83 | public static function findStreamFactory() 84 | { 85 | try { 86 | $messageFactory = static::findOneByType(StreamFactoryInterface::class); 87 | } catch (DiscoveryFailedException $e) { 88 | throw self::createException('stream factory', $e); 89 | } 90 | 91 | return static::instantiateClass($messageFactory); 92 | } 93 | 94 | /** 95 | * @return UploadedFileFactoryInterface 96 | * 97 | * @throws RealNotFoundException 98 | */ 99 | public static function findUploadedFileFactory() 100 | { 101 | try { 102 | $messageFactory = static::findOneByType(UploadedFileFactoryInterface::class); 103 | } catch (DiscoveryFailedException $e) { 104 | throw self::createException('uploaded file factory', $e); 105 | } 106 | 107 | return static::instantiateClass($messageFactory); 108 | } 109 | 110 | /** 111 | * @return UriFactoryInterface 112 | * 113 | * @throws RealNotFoundException 114 | */ 115 | public static function findUriFactory() 116 | { 117 | try { 118 | $messageFactory = static::findOneByType(UriFactoryInterface::class); 119 | } catch (DiscoveryFailedException $e) { 120 | throw self::createException('url factory', $e); 121 | } 122 | 123 | return static::instantiateClass($messageFactory); 124 | } 125 | 126 | /** 127 | * @return UriFactoryInterface 128 | * 129 | * @throws RealNotFoundException 130 | * 131 | * @deprecated This will be removed in 2.0. Consider using the findUriFactory() method. 132 | */ 133 | public static function findUrlFactory() 134 | { 135 | return static::findUriFactory(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Psr18Client.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class Psr18Client extends Psr17Factory implements ClientInterface 24 | { 25 | private $client; 26 | 27 | public function __construct( 28 | ?ClientInterface $client = null, 29 | ?RequestFactoryInterface $requestFactory = null, 30 | ?ResponseFactoryInterface $responseFactory = null, 31 | ?ServerRequestFactoryInterface $serverRequestFactory = null, 32 | ?StreamFactoryInterface $streamFactory = null, 33 | ?UploadedFileFactoryInterface $uploadedFileFactory = null, 34 | ?UriFactoryInterface $uriFactory = null 35 | ) { 36 | $requestFactory ?? $requestFactory = $client instanceof RequestFactoryInterface ? $client : null; 37 | $responseFactory ?? $responseFactory = $client instanceof ResponseFactoryInterface ? $client : null; 38 | $serverRequestFactory ?? $serverRequestFactory = $client instanceof ServerRequestFactoryInterface ? $client : null; 39 | $streamFactory ?? $streamFactory = $client instanceof StreamFactoryInterface ? $client : null; 40 | $uploadedFileFactory ?? $uploadedFileFactory = $client instanceof UploadedFileFactoryInterface ? $client : null; 41 | $uriFactory ?? $uriFactory = $client instanceof UriFactoryInterface ? $client : null; 42 | 43 | parent::__construct($requestFactory, $responseFactory, $serverRequestFactory, $streamFactory, $uploadedFileFactory, $uriFactory); 44 | 45 | $this->client = $client ?? Psr18ClientDiscovery::find(); 46 | } 47 | 48 | public function sendRequest(RequestInterface $request): ResponseInterface 49 | { 50 | return $this->client->sendRequest($request); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Psr18ClientDiscovery.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class Psr18ClientDiscovery extends ClassDiscovery 15 | { 16 | /** 17 | * Finds a PSR-18 HTTP Client. 18 | * 19 | * @return ClientInterface 20 | * 21 | * @throws RealNotFoundException 22 | */ 23 | public static function find() 24 | { 25 | try { 26 | $client = static::findOneByType(ClientInterface::class); 27 | } catch (DiscoveryFailedException $e) { 28 | throw new RealNotFoundException('No PSR-18 clients found. Make sure to install a package providing "psr/http-client-implementation". Example: "php-http/guzzle7-adapter".', 0, $e); 29 | } 30 | 31 | return static::instantiateClass($client); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Strategy/CommonClassesStrategy.php: -------------------------------------------------------------------------------- 1 | 46 | * 47 | * Don't miss updating src/Composer/Plugin.php when adding a new supported class. 48 | */ 49 | final class CommonClassesStrategy implements DiscoveryStrategy 50 | { 51 | /** 52 | * @var array 53 | */ 54 | private static $classes = [ 55 | MessageFactory::class => [ 56 | ['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]], 57 | ['class' => GuzzleMessageFactory::class, 'condition' => [GuzzleRequest::class, GuzzleMessageFactory::class]], 58 | ['class' => DiactorosMessageFactory::class, 'condition' => [DiactorosRequest::class, DiactorosMessageFactory::class]], 59 | ['class' => SlimMessageFactory::class, 'condition' => [SlimRequest::class, SlimMessageFactory::class]], 60 | ], 61 | StreamFactory::class => [ 62 | ['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]], 63 | ['class' => GuzzleStreamFactory::class, 'condition' => [GuzzleRequest::class, GuzzleStreamFactory::class]], 64 | ['class' => DiactorosStreamFactory::class, 'condition' => [DiactorosRequest::class, DiactorosStreamFactory::class]], 65 | ['class' => SlimStreamFactory::class, 'condition' => [SlimRequest::class, SlimStreamFactory::class]], 66 | ], 67 | UriFactory::class => [ 68 | ['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]], 69 | ['class' => GuzzleUriFactory::class, 'condition' => [GuzzleRequest::class, GuzzleUriFactory::class]], 70 | ['class' => DiactorosUriFactory::class, 'condition' => [DiactorosRequest::class, DiactorosUriFactory::class]], 71 | ['class' => SlimUriFactory::class, 'condition' => [SlimRequest::class, SlimUriFactory::class]], 72 | ], 73 | HttpAsyncClient::class => [ 74 | ['class' => SymfonyHttplug::class, 'condition' => [SymfonyHttplug::class, Promise::class, [self::class, 'isPsr17FactoryInstalled']]], 75 | ['class' => Guzzle7::class, 'condition' => Guzzle7::class], 76 | ['class' => Guzzle6::class, 'condition' => Guzzle6::class], 77 | ['class' => Curl::class, 'condition' => Curl::class], 78 | ['class' => React::class, 'condition' => React::class], 79 | ], 80 | HttpClient::class => [ 81 | ['class' => SymfonyHttplug::class, 'condition' => [SymfonyHttplug::class, [self::class, 'isPsr17FactoryInstalled'], [self::class, 'isSymfonyImplementingHttpClient']]], 82 | ['class' => Guzzle7::class, 'condition' => Guzzle7::class], 83 | ['class' => Guzzle6::class, 'condition' => Guzzle6::class], 84 | ['class' => Guzzle5::class, 'condition' => Guzzle5::class], 85 | ['class' => Curl::class, 'condition' => Curl::class], 86 | ['class' => Socket::class, 'condition' => Socket::class], 87 | ['class' => Buzz::class, 'condition' => Buzz::class], 88 | ['class' => React::class, 'condition' => React::class], 89 | ['class' => Cake::class, 'condition' => Cake::class], 90 | ['class' => Artax::class, 'condition' => Artax::class], 91 | [ 92 | 'class' => [self::class, 'buzzInstantiate'], 93 | 'condition' => [\Buzz\Client\FileGetContents::class, \Buzz\Message\ResponseBuilder::class], 94 | ], 95 | ], 96 | Psr18Client::class => [ 97 | [ 98 | 'class' => [self::class, 'symfonyPsr18Instantiate'], 99 | 'condition' => [SymfonyPsr18::class, Psr17RequestFactory::class], 100 | ], 101 | [ 102 | 'class' => GuzzleHttp::class, 103 | 'condition' => [self::class, 'isGuzzleImplementingPsr18'], 104 | ], 105 | [ 106 | 'class' => [self::class, 'buzzInstantiate'], 107 | 'condition' => [\Buzz\Client\FileGetContents::class, \Buzz\Message\ResponseBuilder::class], 108 | ], 109 | ], 110 | ]; 111 | 112 | public static function getCandidates($type) 113 | { 114 | if (Psr18Client::class === $type) { 115 | return self::getPsr18Candidates(); 116 | } 117 | 118 | return self::$classes[$type] ?? []; 119 | } 120 | 121 | /** 122 | * @return array The return value is always an array with zero or more elements. Each 123 | * element is an array with two keys ['class' => string, 'condition' => mixed]. 124 | */ 125 | private static function getPsr18Candidates() 126 | { 127 | $candidates = self::$classes[Psr18Client::class]; 128 | 129 | // HTTPlug 2.0 clients implements PSR18Client too. 130 | foreach (self::$classes[HttpClient::class] as $c) { 131 | if (!is_string($c['class'])) { 132 | continue; 133 | } 134 | try { 135 | if (ClassDiscovery::safeClassExists($c['class']) && is_subclass_of($c['class'], Psr18Client::class)) { 136 | $candidates[] = $c; 137 | } 138 | } catch (\Throwable $e) { 139 | trigger_error(sprintf('Got exception "%s (%s)" while checking if a PSR-18 Client is available', get_class($e), $e->getMessage()), E_USER_WARNING); 140 | } 141 | } 142 | 143 | return $candidates; 144 | } 145 | 146 | public static function buzzInstantiate() 147 | { 148 | return new \Buzz\Client\FileGetContents(Psr17FactoryDiscovery::findResponseFactory()); 149 | } 150 | 151 | public static function symfonyPsr18Instantiate() 152 | { 153 | return new SymfonyPsr18(null, Psr17FactoryDiscovery::findResponseFactory(), Psr17FactoryDiscovery::findStreamFactory()); 154 | } 155 | 156 | public static function isGuzzleImplementingPsr18() 157 | { 158 | return defined('GuzzleHttp\ClientInterface::MAJOR_VERSION'); 159 | } 160 | 161 | public static function isSymfonyImplementingHttpClient() 162 | { 163 | return is_subclass_of(SymfonyHttplug::class, HttpClient::class); 164 | } 165 | 166 | /** 167 | * Can be used as a condition. 168 | * 169 | * @return bool 170 | */ 171 | public static function isPsr17FactoryInstalled() 172 | { 173 | try { 174 | Psr17FactoryDiscovery::findResponseFactory(); 175 | } catch (NotFoundException $e) { 176 | return false; 177 | } catch (\Throwable $e) { 178 | trigger_error(sprintf('Got exception "%s (%s)" while checking if a PSR-17 ResponseFactory is available', get_class($e), $e->getMessage()), E_USER_WARNING); 179 | 180 | return false; 181 | } 182 | 183 | return true; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Strategy/CommonPsr17ClassesStrategy.php: -------------------------------------------------------------------------------- 1 | 16 | * 17 | * Don't miss updating src/Composer/Plugin.php when adding a new supported class. 18 | */ 19 | final class CommonPsr17ClassesStrategy implements DiscoveryStrategy 20 | { 21 | /** 22 | * @var array 23 | */ 24 | private static $classes = [ 25 | RequestFactoryInterface::class => [ 26 | 'Phalcon\Http\Message\RequestFactory', 27 | 'Nyholm\Psr7\Factory\Psr17Factory', 28 | 'GuzzleHttp\Psr7\HttpFactory', 29 | 'Http\Factory\Diactoros\RequestFactory', 30 | 'Http\Factory\Guzzle\RequestFactory', 31 | 'Http\Factory\Slim\RequestFactory', 32 | 'Laminas\Diactoros\RequestFactory', 33 | 'Slim\Psr7\Factory\RequestFactory', 34 | 'HttpSoft\Message\RequestFactory', 35 | ], 36 | ResponseFactoryInterface::class => [ 37 | 'Phalcon\Http\Message\ResponseFactory', 38 | 'Nyholm\Psr7\Factory\Psr17Factory', 39 | 'GuzzleHttp\Psr7\HttpFactory', 40 | 'Http\Factory\Diactoros\ResponseFactory', 41 | 'Http\Factory\Guzzle\ResponseFactory', 42 | 'Http\Factory\Slim\ResponseFactory', 43 | 'Laminas\Diactoros\ResponseFactory', 44 | 'Slim\Psr7\Factory\ResponseFactory', 45 | 'HttpSoft\Message\ResponseFactory', 46 | ], 47 | ServerRequestFactoryInterface::class => [ 48 | 'Phalcon\Http\Message\ServerRequestFactory', 49 | 'Nyholm\Psr7\Factory\Psr17Factory', 50 | 'GuzzleHttp\Psr7\HttpFactory', 51 | 'Http\Factory\Diactoros\ServerRequestFactory', 52 | 'Http\Factory\Guzzle\ServerRequestFactory', 53 | 'Http\Factory\Slim\ServerRequestFactory', 54 | 'Laminas\Diactoros\ServerRequestFactory', 55 | 'Slim\Psr7\Factory\ServerRequestFactory', 56 | 'HttpSoft\Message\ServerRequestFactory', 57 | ], 58 | StreamFactoryInterface::class => [ 59 | 'Phalcon\Http\Message\StreamFactory', 60 | 'Nyholm\Psr7\Factory\Psr17Factory', 61 | 'GuzzleHttp\Psr7\HttpFactory', 62 | 'Http\Factory\Diactoros\StreamFactory', 63 | 'Http\Factory\Guzzle\StreamFactory', 64 | 'Http\Factory\Slim\StreamFactory', 65 | 'Laminas\Diactoros\StreamFactory', 66 | 'Slim\Psr7\Factory\StreamFactory', 67 | 'HttpSoft\Message\StreamFactory', 68 | ], 69 | UploadedFileFactoryInterface::class => [ 70 | 'Phalcon\Http\Message\UploadedFileFactory', 71 | 'Nyholm\Psr7\Factory\Psr17Factory', 72 | 'GuzzleHttp\Psr7\HttpFactory', 73 | 'Http\Factory\Diactoros\UploadedFileFactory', 74 | 'Http\Factory\Guzzle\UploadedFileFactory', 75 | 'Http\Factory\Slim\UploadedFileFactory', 76 | 'Laminas\Diactoros\UploadedFileFactory', 77 | 'Slim\Psr7\Factory\UploadedFileFactory', 78 | 'HttpSoft\Message\UploadedFileFactory', 79 | ], 80 | UriFactoryInterface::class => [ 81 | 'Phalcon\Http\Message\UriFactory', 82 | 'Nyholm\Psr7\Factory\Psr17Factory', 83 | 'GuzzleHttp\Psr7\HttpFactory', 84 | 'Http\Factory\Diactoros\UriFactory', 85 | 'Http\Factory\Guzzle\UriFactory', 86 | 'Http\Factory\Slim\UriFactory', 87 | 'Laminas\Diactoros\UriFactory', 88 | 'Slim\Psr7\Factory\UriFactory', 89 | 'HttpSoft\Message\UriFactory', 90 | ], 91 | ]; 92 | 93 | public static function getCandidates($type) 94 | { 95 | $candidates = []; 96 | if (isset(self::$classes[$type])) { 97 | foreach (self::$classes[$type] as $class) { 98 | $candidates[] = ['class' => $class, 'condition' => [$class]]; 99 | } 100 | } 101 | 102 | return $candidates; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Strategy/DiscoveryStrategy.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface DiscoveryStrategy 11 | { 12 | /** 13 | * Find a resource of a specific type. 14 | * 15 | * @param string $type 16 | * 17 | * @return array The return value is always an array with zero or more elements. Each 18 | * element is an array with two keys ['class' => string, 'condition' => mixed]. 19 | * 20 | * @throws StrategyUnavailableException if we cannot use this strategy 21 | */ 22 | public static function getCandidates($type); 23 | } 24 | -------------------------------------------------------------------------------- /src/Strategy/MockClientStrategy.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class MockClientStrategy implements DiscoveryStrategy 15 | { 16 | public static function getCandidates($type) 17 | { 18 | if (is_a(HttpClient::class, $type, true) || is_a(HttpAsyncClient::class, $type, true)) { 19 | return [['class' => Mock::class, 'condition' => Mock::class]]; 20 | } 21 | 22 | return []; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Strategy/PuliBetaStrategy.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Márk Sági-Kazár 19 | */ 20 | class PuliBetaStrategy implements DiscoveryStrategy 21 | { 22 | /** 23 | * @var GeneratedPuliFactory 24 | */ 25 | protected static $puliFactory; 26 | 27 | /** 28 | * @var Discovery 29 | */ 30 | protected static $puliDiscovery; 31 | 32 | /** 33 | * @return GeneratedPuliFactory 34 | * 35 | * @throws PuliUnavailableException 36 | */ 37 | private static function getPuliFactory() 38 | { 39 | if (null === self::$puliFactory) { 40 | if (!defined('PULI_FACTORY_CLASS')) { 41 | throw new PuliUnavailableException('Puli Factory is not available'); 42 | } 43 | 44 | $puliFactoryClass = PULI_FACTORY_CLASS; 45 | 46 | if (!ClassDiscovery::safeClassExists($puliFactoryClass)) { 47 | throw new PuliUnavailableException('Puli Factory class does not exist'); 48 | } 49 | 50 | self::$puliFactory = new $puliFactoryClass(); 51 | } 52 | 53 | return self::$puliFactory; 54 | } 55 | 56 | /** 57 | * Returns the Puli discovery layer. 58 | * 59 | * @return Discovery 60 | * 61 | * @throws PuliUnavailableException 62 | */ 63 | private static function getPuliDiscovery() 64 | { 65 | if (!isset(self::$puliDiscovery)) { 66 | $factory = self::getPuliFactory(); 67 | $repository = $factory->createRepository(); 68 | 69 | self::$puliDiscovery = $factory->createDiscovery($repository); 70 | } 71 | 72 | return self::$puliDiscovery; 73 | } 74 | 75 | public static function getCandidates($type) 76 | { 77 | $returnData = []; 78 | $bindings = self::getPuliDiscovery()->findBindings($type); 79 | 80 | foreach ($bindings as $binding) { 81 | $condition = true; 82 | if ($binding->hasParameterValue('depends')) { 83 | $condition = $binding->getParameterValue('depends'); 84 | } 85 | $returnData[] = ['class' => $binding->getClassName(), 'condition' => $condition]; 86 | } 87 | 88 | return $returnData; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/StreamFactoryDiscovery.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery. 14 | */ 15 | final class StreamFactoryDiscovery extends ClassDiscovery 16 | { 17 | /** 18 | * Finds a Stream Factory. 19 | * 20 | * @return StreamFactory 21 | * 22 | * @throws Exception\NotFoundException 23 | */ 24 | public static function find() 25 | { 26 | try { 27 | $streamFactory = static::findOneByType(StreamFactory::class); 28 | } catch (DiscoveryFailedException $e) { 29 | throw new NotFoundException('No stream factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.', 0, $e); 30 | } 31 | 32 | return static::instantiateClass($streamFactory); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/UriFactoryDiscovery.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery. 14 | */ 15 | final class UriFactoryDiscovery extends ClassDiscovery 16 | { 17 | /** 18 | * Finds a URI Factory. 19 | * 20 | * @return UriFactory 21 | * 22 | * @throws Exception\NotFoundException 23 | */ 24 | public static function find() 25 | { 26 | try { 27 | $uriFactory = static::findOneByType(UriFactory::class); 28 | } catch (DiscoveryFailedException $e) { 29 | throw new NotFoundException('No uri factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.', 0, $e); 30 | } 31 | 32 | return static::instantiateClass($uriFactory); 33 | } 34 | } 35 | --------------------------------------------------------------------------------