├── .github └── FUNDING.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── ConnectionManagerDelay.php ├── ConnectionManagerReject.php ├── ConnectionManagerRepeat.php ├── ConnectionManagerSwappable.php ├── ConnectionManagerTimeout.php └── Multiple ├── ConnectionManagerConcurrent.php ├── ConnectionManagerConsecutive.php ├── ConnectionManagerRandom.php └── ConnectionManagerSelective.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: clue 2 | custom: https://clue.engineering/support 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.3.0 (2022-08-30) 4 | 5 | * Feature: Simplify usage by supporting new default loop. 6 | (#33 by @SimonFrings) 7 | 8 | ```php 9 | // old (still supported) 10 | $connector = new ConnectionManagerTimeout($connector, 3.0, $loop); 11 | $delayed = new ConnectionManagerDelayed($connector, 0.5, $loop); 12 | 13 | // new (using default loop) 14 | $connector = new ConnectionManagerTimeout($connector, 3.0); 15 | $delayed = new ConnectionManagerDelayed($connector, 0.5); 16 | ``` 17 | 18 | * Feature: Full support for PHP 8.1 and PHP 8.2. 19 | (#36 and #37 by @SimonFrings) 20 | 21 | * Feature: Forward compatibility with upcoming Promise v3. 22 | (#34 by @clue) 23 | 24 | * Improve test suite and add badge to show number of project installations. 25 | (#35 by @SimonFrings and #31 by @PaulRotmann) 26 | 27 | ## 1.2.0 (2020-12-12) 28 | 29 | * Improve test suite and add `.gitattributes` to exclude dev files from exports. 30 | Prepare PHP 8 support, update to PHPUnit 9 and simplify test matrix. 31 | (#24, #25 and #26 by @clue and #27, #28, #29 and #30 by @SimonFrings) 32 | 33 | ## 1.1.0 (2017-08-03) 34 | 35 | * Feature: Support custom rejection reason for ConnectionManagerReject 36 | (#23 by @clue) 37 | 38 | ```php 39 | $connector = new ConnectionManagerReject(function ($uri) { 40 | throw new RuntimeException($uri . ' blocked'); 41 | }); 42 | ``` 43 | 44 | ## 1.0.1 (2017-06-23) 45 | 46 | * Fix: Ignore URI scheme when matching selective connectors 47 | (#21 by @clue) 48 | 49 | * Fix HHVM build for now again and ignore future HHVM build errors 50 | (#22 by @clue) 51 | 52 | ## 1.0.0 (2017-05-09) 53 | 54 | * First stable release, now following SemVer 55 | 56 | > Contains no other changes, so it's actually fully compatible with the v0.7 releases. 57 | 58 | ## 0.7.1 (2017-05-09) 59 | 60 | * Fix: Reject promise for invalid URI passed to ConnectionManagerSelective 61 | (#19 by @clue) 62 | 63 | * Feature: Forward compatibility with upcoming Socket v1.0 and v0.8 and 64 | upcoming EventLoop v1.0 and v0.5 65 | (#18 and #20 by @clue) 66 | 67 | ## 0.7.0 (2017-04-10) 68 | 69 | * Feature / BC break: Replace deprecated SocketClient with new Socket component 70 | (#17 by @clue) 71 | 72 | This implies that all connectors from this package now implement the 73 | `React\Socket\ConnectorInterface` instead of the legacy 74 | `React\SocketClient\ConnectorInterface`. 75 | 76 | ## 0.6.0 (2017-04-07) 77 | 78 | * Feature / BC break: Update SocketClient to v0.7 or v0.6 79 | (#16 by @clue) 80 | 81 | * Improve test suite by adding PHPUnit to require-dev 82 | (#15 by @clue) 83 | 84 | ## 0.5.0 (2016-06-01) 85 | 86 | * BC break: Change $retries to $tries 87 | (#14 by @clue) 88 | 89 | ```php 90 | // old 91 | // 1 try plus 2 retries => 3 total tries 92 | $c = new ConnectionManagerRepeat($c, 2); 93 | 94 | // new 95 | // 3 total tries (1 try plus 2 retries) 96 | $c = new ConnectionManagerRepeat($c, 3); 97 | ``` 98 | 99 | * BC break: Timed connectors now use $loop as last argument 100 | (#13 by @clue) 101 | 102 | ```php 103 | // old 104 | // $c = new ConnectionManagerDelay($c, $loop, 1.0); 105 | $c = new ConnectionManagerTimeout($c, $loop, 1.0); 106 | 107 | // new 108 | $c = new ConnectionManagerTimeout($c, 1.0, $loop); 109 | ``` 110 | 111 | * BC break: Move all connector lists to the constructor 112 | (#12 by @clue) 113 | 114 | ```php 115 | // old 116 | // $c = new ConnectionManagerConcurrent(); 117 | // $c = new ConnectionManagerRandom(); 118 | $c = new ConnectionManagerConsecutive(); 119 | $c->addConnectionManager($c1); 120 | $c->addConnectionManager($c2); 121 | 122 | // new 123 | $c = new ConnectionManagerConsecutive(array( 124 | $c1, 125 | $c2 126 | )); 127 | ``` 128 | 129 | * BC break: ConnectionManagerSelective now accepts connector list in constructor 130 | (#11 by @clue) 131 | 132 | ```php 133 | // old 134 | $c = new ConnectionManagerSelective(); 135 | $c->addConnectionManagerFor($c1, 'host1'); 136 | $c->addConnectionManagerFor($c2, 'host2'); 137 | 138 | // new 139 | $c = new ConnectionManagerSelective(array( 140 | 'host1' => $c1, 141 | 'host2' => $c2 142 | )); 143 | ``` 144 | 145 | ## 0.4.0 (2016-05-30) 146 | 147 | * Feature: Add `ConnectionManagerConcurrent` 148 | (#10 by @clue) 149 | 150 | * Feature: Support Promise cancellation for all connectors 151 | (#9 by @clue) 152 | 153 | ## 0.3.3 (2016-05-29) 154 | 155 | * Fix repetitions for `ConnectionManagerRepeat` 156 | (#8 by @clue) 157 | 158 | * First class support for PHP 5.3 through PHP 7 and HHVM 159 | (#7 by @clue) 160 | 161 | ## 0.3.2 (2016-03-19) 162 | 163 | * Compatibility with react/socket-client:v0.5 (keeping full BC) 164 | (#6 by @clue) 165 | 166 | ## 0.3.1 (2014-09-27) 167 | 168 | * Support React PHP v0.4 (while preserving BC with React PHP v0.3) 169 | (#4) 170 | 171 | ## 0.3.0 (2013-06-24) 172 | 173 | * BC break: Switch from (deprecated) `clue/connection-manager` to `react/socket-client` 174 | and thus replace each occurance of `getConnect($host, $port)` with `create($host, $port)` 175 | (#1) 176 | 177 | * Fix: Timeouts in `ConnectionManagerTimeout` now actually work 178 | (#1) 179 | 180 | * Fix: Properly reject promise in `ConnectionManagerSelective` when no targets 181 | have been found 182 | (#1) 183 | 184 | ## 0.2.0 (2013-02-08) 185 | 186 | * Feature: Add `ConnectionManagerSelective` which works like a network/firewall ACL 187 | 188 | ## 0.1.0 (2013-01-12) 189 | 190 | * First tagged release 191 | 192 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Christian Lück 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 furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clue/reactphp-connection-manager-extra 2 | 3 | [![CI status](https://github.com/clue/reactphp-connection-manager-extra/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-connection-manager-extra/actions) 4 | [![installs on Packagist](https://img.shields.io/packagist/dt/clue/connection-manager-extra?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/connection-manager-extra) 5 | 6 | This project provides _extra_ (in terms of "additional", "extraordinary", "special" and "unusual") decorators, 7 | built on top of [ReactPHP's Socket](https://github.com/reactphp/socket). 8 | 9 | **Table of Contents** 10 | 11 | * [Support us](#support-us) 12 | * [Introduction](#introduction) 13 | * [Usage](#usage) 14 | * [Repeat](#repeat) 15 | * [Timeout](#timeout) 16 | * [Delay](#delay) 17 | * [Reject](#reject) 18 | * [Swappable](#swappable) 19 | * [Consecutive](#consecutive) 20 | * [Random](#random) 21 | * [Concurrent](#concurrent) 22 | * [Selective](#selective) 23 | * [Install](#install) 24 | * [Tests](#tests) 25 | * [License](#license) 26 | 27 | ## Support us 28 | 29 | We invest a lot of time developing, maintaining and updating our awesome 30 | open-source projects. You can help us sustain this high-quality of our work by 31 | [becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get 32 | numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue) 33 | for details. 34 | 35 | Let's take these projects to the next level together! 🚀 36 | 37 | ## Introduction 38 | 39 | If you're not already familar with [react/socket](https://github.com/reactphp/socket), 40 | think of it as an async (non-blocking) version of [`fsockopen()`](https://www.php.net/manual/en/function.fsockopen.php) 41 | or [`stream_socket_client()`](https://www.php.net/manual/en/function.stream-socket-client.php). 42 | I.e. before you can send and receive data to/from a remote server, you first have to establish a connection - which 43 | takes its time because it involves several steps. 44 | In order to be able to establish several connections at the same time, [react/socket](https://github.com/reactphp/socket) provides a simple 45 | API to establish simple connections in an async (non-blocking) way. 46 | 47 | This project includes several classes that extend this base functionality by implementing the same simple `ConnectorInterface`. 48 | This interface provides a single promise-based method `connect($uri)` which can be used to easily notify 49 | when the connection is successfully established or the `Connector` gives up and the connection fails. 50 | 51 | ```php 52 | $connector->connect('www.google.com:80')->then(function ($stream) { 53 | echo 'connection successfully established'; 54 | $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); 55 | $stream->end(); 56 | }, function ($exception) { 57 | echo 'connection attempt failed: ' . $exception->getMessage(); 58 | }); 59 | 60 | ``` 61 | 62 | Because everything uses the same simple API, the resulting `Connector` classes can be easily interchanged 63 | and be used in places that expect the normal `ConnectorInterface`. This can be used to stack them into each other, 64 | like using [timeouts](#timeout) for TCP connections, [delaying](#delay) SSL/TLS connections, 65 | [retrying](#repeat) failed connection attempts, [randomly](#random) picking a `Connector` or 66 | any combination thereof. 67 | 68 | ## Usage 69 | 70 | This section lists all features of this library along with some examples. 71 | The examples assume you've [installed](#install) this library and 72 | already [set up a `Socket/Connector` instance `$connector`](https://github.com/reactphp/socket#connector). 73 | 74 | All classes are located in the `ConnectionManager\Extra` namespace. 75 | 76 | ### Repeat 77 | 78 | The `ConnectionManagerRepeat($connector, $tries)` tries connecting to the given location up to a maximum 79 | of `$tries` times when the connection fails. 80 | 81 | If you pass a value of `3` to it, it will first issue a normal connection attempt 82 | and then retry up to 2 times if the connection attempt fails: 83 | 84 | ```php 85 | $connectorRepeater = new ConnectionManagerRepeat($connector, 3); 86 | 87 | $connectorRepeater->connect('www.google.com:80')->then(function ($stream) { 88 | echo 'connection successfully established'; 89 | $stream->close(); 90 | }); 91 | ``` 92 | 93 | ### Timeout 94 | 95 | The `ConnectionManagerTimeout($connector, $timeout, $loop = null)` sets a maximum `$timeout` in seconds on when to give up 96 | waiting for the connection to complete. 97 | 98 | ```php 99 | $connector = new ConnectionManagerTimeout($connector, 3.0); 100 | ``` 101 | 102 | ### Delay 103 | 104 | The `ConnectionManagerDelay($connector, $delay, $loop = null)` sets a fixed initial `$delay` in seconds before actually 105 | trying to connect. (Not to be confused with [`ConnectionManagerTimeout`](#timeout) which sets a _maximum timeout_.) 106 | 107 | ```php 108 | $delayed = new ConnectionManagerDelayed($connector, 0.5); 109 | ``` 110 | 111 | ### Reject 112 | 113 | The `ConnectionManagerReject(null|string|callable $reason)` simply rejects every single connection attempt. 114 | This is particularly useful for the below [`ConnectionManagerSelective`](#selective) to reject connection attempts 115 | to only certain destinations (for example blocking advertisements or harmful sites). 116 | 117 | The constructor accepts an optional rejection reason which will be used for 118 | rejecting the resulting promise. 119 | 120 | You can explicitly pass a `string` value which will be used as the message for 121 | the `Exception` instance: 122 | 123 | ```php 124 | $connector = new ConnectionManagerReject('Blocked'); 125 | $connector->connect('www.google.com:80')->then(null, function ($e) { 126 | assert($e instanceof \Exception); 127 | assert($e->getMessage() === 'Blocked'); 128 | }); 129 | ``` 130 | 131 | You can explicitly pass a `callable` value which will be used to either 132 | `throw` or `return` a custom `Exception` instance: 133 | 134 | ```php 135 | $connector = new ConnectionManagerReject(function ($uri) { 136 | throw new RuntimeException($uri . ' blocked'); 137 | }); 138 | $connector->connect('www.google.com:80')->then(null, function ($e) { 139 | assert($e instanceof \RuntimeException); 140 | assert($e->getMessage() === 'www.google.com:80 blocked'); 141 | }); 142 | ``` 143 | 144 | ### Swappable 145 | 146 | The `ConnectionManagerSwappable($connector)` is a simple decorator for other `ConnectionManager`s to 147 | simplify exchanging the actual `ConnectionManager` during runtime (`->setConnectionManager($connector)`). 148 | 149 | ### Consecutive 150 | 151 | The `ConnectionManagerConsecutive($connectors)` establishes connections by trying to connect through 152 | any of the given `ConnectionManager`s in consecutive order until the first one succeeds. 153 | 154 | ```php 155 | $consecutive = new ConnectionManagerConsecutive(array( 156 | $connector1, 157 | $connector2 158 | )); 159 | ``` 160 | 161 | ### Random 162 | 163 | The `ConnectionManagerRandom($connectors)` works much like `ConnectionManagerConsecutive` but instead 164 | of using a fixed order, it always uses a randomly shuffled order. 165 | 166 | ```php 167 | $random = new ConnectionManagerRandom(array( 168 | $connector1, 169 | $connector2 170 | )); 171 | ``` 172 | 173 | ### Concurrent 174 | 175 | The `ConnectionManagerConcurrent($connectors)` establishes connections by trying to connect through 176 | ALL of the given `ConnectionManager`s at once, until the first one succeeds. 177 | 178 | ```php 179 | $concurrent = new ConnectionManagerConcurrent(array( 180 | $connector1, 181 | $connector2 182 | )); 183 | ``` 184 | 185 | ### Selective 186 | 187 | The `ConnectionManagerSelective($connectors)` manages a list of `Connector`s and 188 | forwards each connection through the first matching one. 189 | This can be used to implement networking access control lists (ACLs) or firewall 190 | rules like a blacklist or whitelist. 191 | 192 | This allows fine-grained control on how to handle outgoing connections, like 193 | rejecting advertisements, delaying unencrypted HTTP requests or forwarding HTTPS 194 | connection through a foreign country. 195 | 196 | If none of the entries in the list matches, the connection will be rejected. 197 | This can be used to implement a very simple whitelist like this: 198 | 199 | ```php 200 | $selective = new ConnectionManagerSelective(array( 201 | 'github.com' => $connector, 202 | '*:443' => $connector 203 | )); 204 | ``` 205 | 206 | If you want to implement a blacklist (i.e. reject only certain targets), make 207 | sure to add a default target to the end of the list like this: 208 | 209 | ```php 210 | $reject = new ConnectionManagerReject(); 211 | $selective = new ConnectionManagerSelective(array( 212 | 'ads.example.com' => $reject, 213 | '*:80-81' => $reject, 214 | '*' => $connector 215 | )); 216 | ``` 217 | 218 | Similarly, you can also combine any of the other connectors to implement more 219 | advanced connection setups, such as delaying unencrypted connections only and 220 | retrying unreliable hosts: 221 | 222 | ```php 223 | // delay connection by 2 seconds 224 | $delayed = new ConnectionManagerDelay($connector, 2.0); 225 | 226 | // maximum of 3 tries, each taking no longer than 2.0 seconds 227 | $retry = new ConnectionManagerRepeat( 228 | new ConnectionManagerTimeout($connector, 2.0), 229 | 3 230 | ); 231 | 232 | $selective = new ConnectionManagerSelective(array( 233 | '*:80' => $delayed, 234 | 'unreliable.example.com' => $retry, 235 | '*' => $connector 236 | )); 237 | ``` 238 | 239 | Each entry in the list MUST be in the form `host` or `host:port`, where 240 | `host` may contain the `*` wildcard character and `port` may be given as 241 | either an exact port number or as a range in the form of `min-max`. 242 | Passing anything else will result in an `InvalidArgumentException`. 243 | 244 | > Note that the host will be matched exactly as-is otherwise. This means that 245 | if you only block `youtube.com`, this has no effect on `www.youtube.com`. 246 | You may want to add a second rule for `*.youtube.com` in this case. 247 | 248 | ## Install 249 | 250 | The recommended way to install this library is [through Composer](https://getcomposer.org/). 251 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) 252 | 253 | This project follows [SemVer](https://semver.org/). 254 | This will install the latest supported version: 255 | 256 | ```bash 257 | composer require clue/connection-manager-extra:^1.3 258 | ``` 259 | 260 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. 261 | 262 | This project aims to run on any platform and thus does not require any PHP 263 | extensions and supports running on legacy PHP 5.3 through current PHP 8+ and 264 | HHVM. 265 | It's *highly recommended to use the latest supported PHP version* for this project. 266 | 267 | ## Tests 268 | 269 | To run the test suite, you first need to clone this repo and then install all 270 | dependencies [through Composer](https://getcomposer.org/): 271 | 272 | ```bash 273 | composer install 274 | ``` 275 | 276 | To run the test suite, go to the project root and run: 277 | 278 | ```bash 279 | vendor/bin/phpunit 280 | ``` 281 | 282 | ## License 283 | 284 | This project is released under the permissive [MIT license](LICENSE). 285 | 286 | > Did you know that I offer custom development services and issuing invoices for 287 | sponsorships of releases and for contributions? Contact me (@clue) for details. 288 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clue/connection-manager-extra", 3 | "description": "Extra decorators for creating async TCP/IP connections, built on top of ReactPHP's Socket component", 4 | "keywords": ["Socket", "network", "connection", "timeout", "delay", "reject", "repeat", "retry", "random", "acl", "firewall", "ReactPHP"], 5 | "homepage": "https://github.com/clue/reactphp-connection-manager-extra", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Christian Lück", 10 | "email": "christian@clue.engineering" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.3", 15 | "react/event-loop": "^1.2", 16 | "react/promise": "^3.2 || ^2.1 || ^1.2.1", 17 | "react/promise-timer": "^1.11", 18 | "react/socket": "^1.16" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "ConnectionManager\\Extra\\": "src/" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "ConnectionManager\\Tests\\Extra\\": "tests/" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ConnectionManagerDelay.php: -------------------------------------------------------------------------------- 1 | connectionManager = $connectionManager; 33 | $this->delay = $delay; 34 | $this->loop = $loop ?: Loop::get(); 35 | } 36 | 37 | public function connect($uri) 38 | { 39 | $connectionManager = $this->connectionManager; 40 | 41 | return Timer\resolve($this->delay, $this->loop)->then(function () use ($connectionManager, $uri) { 42 | return $connectionManager->connect($uri); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ConnectionManagerReject.php: -------------------------------------------------------------------------------- 1 | reason = $reason; 21 | } 22 | } 23 | 24 | public function connect($uri) 25 | { 26 | $reason = $this->reason; 27 | if (!is_string($reason)) { 28 | try { 29 | $reason = $reason($uri); 30 | } catch (\Exception $e) { 31 | $reason = $e; 32 | } 33 | } 34 | 35 | if (!$reason instanceof \Exception) { 36 | $reason = new Exception($reason); 37 | } 38 | 39 | return Promise\reject($reason); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ConnectionManagerRepeat.php: -------------------------------------------------------------------------------- 1 | = 1'); 20 | } 21 | $this->connectionManager = $connectionManager; 22 | $this->maximumTries = $maximumTries; 23 | } 24 | 25 | public function connect($uri) 26 | { 27 | $tries = $this->maximumTries; 28 | $connector = $this->connectionManager; 29 | 30 | return new Promise(function ($resolve, $reject) use ($uri, &$pending, &$tries, $connector) { 31 | $try = function ($error = null) use (&$try, &$pending, &$tries, $uri, $connector, $resolve, $reject) { 32 | if ($tries > 0) { 33 | --$tries; 34 | $pending = $connector->connect($uri); 35 | $pending->then($resolve, $try); 36 | } else { 37 | $reject(new Exception('Connection still fails even after retrying', 0, $error)); 38 | } 39 | }; 40 | 41 | $try(); 42 | }, function ($_, $reject) use (&$pending, &$tries) { 43 | // stop retrying, reject results and cancel pending attempt 44 | $tries = 0; 45 | $reject(new \RuntimeException('Cancelled')); 46 | 47 | if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) { 48 | $pending->cancel(); 49 | } 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ConnectionManagerSwappable.php: -------------------------------------------------------------------------------- 1 | connectionManager = $connectionManager; 15 | } 16 | 17 | public function connect($uri) 18 | { 19 | return $this->connectionManager->connect($uri); 20 | } 21 | 22 | public function setConnectionManager(ConnectorInterface $connectionManager) 23 | { 24 | $this->connectionManager = $connectionManager; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ConnectionManagerTimeout.php: -------------------------------------------------------------------------------- 1 | connectionManager = $connectionManager; 33 | $this->timeout = $timeout; 34 | $this->loop = $loop ?: Loop::get(); 35 | } 36 | 37 | public function connect($uri) 38 | { 39 | $promise = $this->connectionManager->connect($uri); 40 | 41 | return Timer\timeout($promise, $this->timeout, $this->loop)->then(null, function ($e) use ($promise) { 42 | // connection successfully established but timeout already expired => close successful connection 43 | $promise->then(function ($connection) { 44 | $connection->end(); 45 | }); 46 | 47 | throw $e; 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Multiple/ConnectionManagerConcurrent.php: -------------------------------------------------------------------------------- 1 | managers as $connector) { 14 | $all []= $connector->connect($uri); 15 | } 16 | return Promise\any($all)->then(function ($conn) use ($all) { 17 | // a connection attempt succeeded 18 | // => cancel all pending connection attempts 19 | foreach ($all as $promise) { 20 | if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) { 21 | $promise->cancel(); 22 | } 23 | 24 | // if promise resolves despite cancellation, immediately close stream 25 | $promise->then(function ($stream) use ($conn) { 26 | if ($stream !== $conn) { 27 | $stream->close(); 28 | } 29 | }); 30 | } 31 | return $conn; 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Multiple/ConnectionManagerConsecutive.php: -------------------------------------------------------------------------------- 1 | managers = $managers; 24 | } 25 | 26 | public function connect($uri) 27 | { 28 | return $this->tryConnection($this->managers, $uri); 29 | } 30 | 31 | /** 32 | * 33 | * @param ConnectorInterface[] $managers 34 | * @param string $uri 35 | * @return Promise 36 | * @internal 37 | */ 38 | public function tryConnection(array $managers, $uri) 39 | { 40 | return new Promise\Promise(function ($resolve, $reject) use (&$managers, &$pending, $uri) { 41 | $try = function () use (&$try, &$managers, $uri, $resolve, $reject, &$pending) { 42 | if (!$managers) { 43 | return $reject(new UnderflowException('No more managers to try to connect through')); 44 | } 45 | 46 | $manager = array_shift($managers); 47 | $pending = $manager->connect($uri); 48 | $pending->then($resolve, $try); 49 | }; 50 | 51 | $try(); 52 | }, function ($_, $reject) use (&$managers, &$pending) { 53 | // stop retrying, reject results and cancel pending attempt 54 | $managers = array(); 55 | $reject(new \RuntimeException('Cancelled')); 56 | 57 | if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) { 58 | $pending->cancel(); 59 | } 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Multiple/ConnectionManagerRandom.php: -------------------------------------------------------------------------------- 1 | managers; 10 | shuffle($managers); 11 | 12 | return $this->tryConnection($managers, $uri); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Multiple/ConnectionManagerSelective.php: -------------------------------------------------------------------------------- 1 | $manager) { 21 | $host = $filter; 22 | $portMin = 0; 23 | $portMax = 65535; 24 | 25 | // search colon (either single one OR preceded by "]" due to IPv6) 26 | $colon = strrpos($host, ':'); 27 | if ($colon !== false && (strpos($host, ':') === $colon || substr($host, $colon - 1, 1) === ']' )) { 28 | if (!isset($host[$colon + 1])) { 29 | throw new InvalidArgumentException('Entry "' . $filter . '" has no port after colon'); 30 | } 31 | 32 | $minus = strpos($host, '-', $colon); 33 | if ($minus === false) { 34 | $portMin = $portMax = (int)substr($host, $colon + 1); 35 | 36 | if (substr($host, $colon + 1) !== (string)$portMin) { 37 | throw new InvalidArgumentException('Entry "' . $filter . '" has no valid port after colon'); 38 | } 39 | } else { 40 | $portMin = (int)substr($host, $colon + 1, ($minus - $colon)); 41 | $portMax = (int)substr($host, $minus + 1); 42 | 43 | if (substr($host, $colon + 1) !== ($portMin . '-' . $portMax)) { 44 | throw new InvalidArgumentException('Entry "' . $filter . '" has no valid port range after colon'); 45 | } 46 | 47 | if ($portMin > $portMax) { 48 | throw new InvalidArgumentException('Entry "' . $filter . '" has port range mixed up'); 49 | } 50 | } 51 | $host = substr($host, 0, $colon); 52 | } 53 | 54 | if ($host === '') { 55 | throw new InvalidArgumentException('Entry "' . $filter . '" has an empty host'); 56 | } 57 | 58 | if (!$manager instanceof ConnectorInterface) { 59 | throw new InvalidArgumentException('Entry "' . $filter . '" is not a valid connector'); 60 | } 61 | } 62 | 63 | $this->managers = $managers; 64 | } 65 | 66 | public function connect($uri) 67 | { 68 | $parts = parse_url((strpos($uri, '://') === false ? 'tcp://' : '') . $uri); 69 | if (!isset($parts) || !isset($parts['scheme'], $parts['host'], $parts['port'])) { 70 | return Promise\reject(new InvalidArgumentException('Invalid URI')); 71 | } 72 | 73 | $connector = $this->getConnectorForTarget( 74 | trim($parts['host'], '[]'), 75 | $parts['port'] 76 | ); 77 | 78 | if ($connector === null) { 79 | return Promise\reject(new UnderflowException('No connector for given target found')); 80 | } 81 | 82 | return $connector->connect($uri); 83 | } 84 | 85 | private function getConnectorForTarget($targetHost, $targetPort) 86 | { 87 | foreach ($this->managers as $host => $connector) { 88 | $portMin = 0; 89 | $portMax = 65535; 90 | 91 | // search colon (either single one OR preceded by "]" due to IPv6) 92 | $colon = strrpos($host, ':'); 93 | if ($colon !== false && (strpos($host, ':') === $colon || substr($host, $colon - 1, 1) === ']' )) { 94 | $minus = strpos($host, '-', $colon); 95 | if ($minus === false) { 96 | $portMin = $portMax = (int)substr($host, $colon + 1); 97 | } else { 98 | $portMin = (int)substr($host, $colon + 1, ($minus - $colon)); 99 | $portMax = (int)substr($host, $minus + 1); 100 | } 101 | $host = trim(substr($host, 0, $colon), '[]'); 102 | } 103 | 104 | if ($targetPort >= $portMin && $targetPort <= $portMax && fnmatch($host, $targetHost)) { 105 | return $connector; 106 | } 107 | } 108 | 109 | return null; 110 | } 111 | } 112 | --------------------------------------------------------------------------------