├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── examples └── http │ ├── advanced_get.php │ ├── multiple_gets.php │ ├── simple_get.php │ ├── simple_post_json.php │ ├── stream_body.php │ └── stream_twitter.php ├── phpunit.xml ├── src ├── Client.php ├── Http.php ├── HttpObservable.php └── HttpResponseException.php └── tests ├── Functional ├── HttpClientStub.php ├── Observable │ └── HttpObservableTest.php └── TestCase.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor 3 | /.hhconfig 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: required 3 | 4 | php: 5 | - 7 6 | - 7.1 7 | - hhvm 8 | 9 | matrix: 10 | allow_failures: 11 | - php: hhvm 12 | 13 | install: 14 | - composer install 15 | 16 | script: 17 | - vendor/bin/phpunit -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Voryx LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Http Client for RxPHP 2 | 3 | 4 | This library is a RxPHP wrapper for the [ReactPHP's Http-client](https://github.com/reactphp/http-client) library. It allows you to make asynchronous http calls and emit the results through an RxPHP observable. 5 | 6 | It uses the [Voryx event-loop](https://github.com/voryx/event-loop) which behaves like the Javascript event-loop. ie. You don't need to start it. 7 | 8 | 9 | ##Installation 10 | 11 | Install dependencies using [composer](https://getcomposer.org/doc/00-intro.md#downloading-the-composer-executable) 12 | 13 | $ php composer.phar require "rx/http" 14 | 15 | ## Usage 16 | 17 | ### Get 18 | ```php 19 | 20 | $source = \Rx\React\Http::get('https://www.example.com/'); 21 | 22 | $source->subscribe( 23 | function ($data) { 24 | echo $data, PHP_EOL; 25 | }, 26 | function (\Exception $e) { 27 | echo $e->getMessage(), PHP_EOL; 28 | }, 29 | function () { 30 | echo "completed", PHP_EOL; 31 | } 32 | ); 33 | 34 | ``` 35 | 36 | ### Post 37 | ```php 38 | 39 | $postData = json_encode(["test" => "data"]); 40 | $headers = ['Content-Type' => 'application/json']; 41 | 42 | $source = \Rx\React\Http::post('https://www.example.com/', $postData, $headers); 43 | 44 | $source->subscribe( 45 | function ($data) { 46 | echo $data, PHP_EOL; 47 | }, 48 | function (\Exception $e) { 49 | echo $e->getMessage(), PHP_EOL; 50 | }, 51 | function () { 52 | echo "completed", PHP_EOL; 53 | } 54 | ); 55 | 56 | ``` 57 | 58 | ### Multiple Asynchronous Requests 59 | 60 | ```php 61 | 62 | $imageTypes = ["png", "jpeg", "webp"]; 63 | 64 | $images = \Rx\Observable::fromArray($imageTypes) 65 | ->flatMap(function ($type) { 66 | return \Rx\React\Http::get("http://httpbin.org/image/{$type}")->map(function ($image) use ($type) { 67 | return [$type => $image]; 68 | }); 69 | }); 70 | 71 | $images->subscribe( 72 | function ($data) { 73 | echo "Got Image: ", array_keys($data)[0], PHP_EOL; 74 | }, 75 | function (\Exception $e) { 76 | echo $e->getMessage(), PHP_EOL; 77 | }, 78 | function () { 79 | echo "completed", PHP_EOL; 80 | } 81 | ); 82 | 83 | 84 | ``` 85 | 86 | 87 | For more information, see the [examples](examples). -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rx/http", 3 | "type": "library", 4 | "description": "Http Client for RxPHP", 5 | "keywords": [ 6 | "rxphp", 7 | "reactivex", 8 | "react", 9 | "reactphp", 10 | "http", 11 | "rx.php" 12 | ], 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "David Dan", 17 | "email": "davidwdan@gmail.com", 18 | "role": "Developer" 19 | }, 20 | { 21 | "name": "Matt Bonneau", 22 | "email": "matt@bonneau.net", 23 | "role": "Developer" 24 | } 25 | ], 26 | "autoload": { 27 | "psr-4": { 28 | "Rx\\React\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Rx\\React\\Tests\\": "tests/" 34 | } 35 | }, 36 | "require": { 37 | "PHP": ">=7.0", 38 | "voryx/event-loop": "^3.0 || ^2.0", 39 | "reactivex/rxphp": "^2.0", 40 | "react/http-client": "^0.5.0" 41 | }, 42 | "require-dev": { 43 | "phpunit/phpunit": "^9.5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/http/advanced_get.php: -------------------------------------------------------------------------------- 1 | (new React\Dns\Resolver\Factory())->create('4.2.2.2', $loop)]); 12 | 13 | $source = (new \Rx\React\Client($loop, $connector))->request('GET', 'https://www.example.com/'); 14 | 15 | $source->subscribe( 16 | function ($data) { 17 | echo $data, PHP_EOL; 18 | }, 19 | function (\Throwable $e) { 20 | echo $e->getMessage(), PHP_EOL; 21 | }, 22 | function () { 23 | echo 'completed', PHP_EOL; 24 | } 25 | ); 26 | 27 | $loop->run(); 28 | -------------------------------------------------------------------------------- /examples/http/multiple_gets.php: -------------------------------------------------------------------------------- 1 | flatMap(function ($type) { 10 | return \Rx\React\Http::get("http://httpbin.org/image/{$type}")->map(function ($image) use ($type) { 11 | return [$type => $image]; 12 | }); 13 | }); 14 | 15 | $images->subscribe( 16 | function ($data) { 17 | echo 'Got Image: ', array_keys($data)[0], PHP_EOL; 18 | }, 19 | function (\Throwable $e) { 20 | echo $e->getMessage(), PHP_EOL; 21 | }, 22 | function () { 23 | echo 'completed', PHP_EOL; 24 | } 25 | ); 26 | -------------------------------------------------------------------------------- /examples/http/simple_get.php: -------------------------------------------------------------------------------- 1 | subscribe( 8 | function ($data) { 9 | echo $data, PHP_EOL; 10 | }, 11 | function (\Throwable $e) { 12 | echo $e->getMessage(), PHP_EOL; 13 | }, 14 | function () { 15 | echo 'completed', PHP_EOL; 16 | } 17 | ); 18 | -------------------------------------------------------------------------------- /examples/http/simple_post_json.php: -------------------------------------------------------------------------------- 1 | 'data']); 6 | $headers = ['Content-Type' => 'application/json']; 7 | 8 | $source = \Rx\React\Http::post('https://www.example.com/', $postData, $headers); 9 | 10 | $source->subscribe( 11 | function ($data) { 12 | echo $data, PHP_EOL; 13 | }, 14 | function (\Throwable $e) { 15 | echo $e->getMessage(), PHP_EOL; 16 | }, 17 | function () { 18 | echo 'completed', PHP_EOL; 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /examples/http/stream_body.php: -------------------------------------------------------------------------------- 1 | streamResults(); 6 | $start = time(); 7 | $size = 0; 8 | 9 | $source->subscribe( 10 | function ($data) use (&$size) { 11 | $size += strlen($data); 12 | echo "\033[1A", 'Downloaded size: ', number_format($size / 1024 / 1024, 2, '.', ''), 'MB', PHP_EOL; 13 | }, 14 | function (\Throwable $e) { 15 | echo $e->getMessage(); 16 | }, 17 | function () use (&$size, $start) { 18 | $end = time(); 19 | $duration = $end - $start; 20 | 21 | echo round($size / 1024 / 1024, 2), 'MB downloaded in ', $duration, ' seconds at ', round(($size / $duration) / 1024 / 1024, 2), 'MB/s', PHP_EOL; 22 | } 23 | ); 24 | -------------------------------------------------------------------------------- /examples/http/stream_twitter.php: -------------------------------------------------------------------------------- 1 | signRequest(new JacobKiers\OAuth\SignatureMethod\HmacSha1(), $consumer, $token); 23 | return trim(substr($oauthRequest->toHeader(), 15)); 24 | } 25 | 26 | $postData = 'follow=' . TWITTER_USER_ID; 27 | $method = 'POST'; 28 | $url = 'https://stream.twitter.com/1.1/statuses/filter.json'; 29 | $headers = [ 30 | 'Authorization' => generateHeader($method, $url, ['follow' => TWITTER_USER_ID]), 31 | 'Content-Type' => 'application/x-www-form-urlencoded', 32 | 'Content-Length' => strlen($postData), 33 | ]; 34 | 35 | $source = Http::post($url, $postData, $headers) 36 | ->streamResults() 37 | ->share(); 38 | 39 | $connected = $source 40 | ->take(1) 41 | ->do(function () { 42 | echo 'Connected to twitter, listening in on stream:', PHP_EOL; 43 | }); 44 | 45 | /** @var Observable $allTweets */ 46 | $allTweets = $connected 47 | ->merge($source) 48 | ->cut(PHP_EOL) 49 | ->filter(function ($tweet) { 50 | return strlen(trim($tweet)) > 0; 51 | }) 52 | ->map('json_decode'); 53 | 54 | $endTwitterStream = $allTweets 55 | ->filter('is_object') 56 | ->filter(function ($tweet) { 57 | return trim($tweet->text) === 'exit();'; 58 | }) 59 | ->do(function ($twitter) { 60 | echo 'exit(); found, stopping...', PHP_EOL; 61 | }); 62 | 63 | $usersTweets = $allTweets->filter(function ($tweet) { 64 | return isset($tweet->user->screen_name); 65 | }); 66 | 67 | $tweets = $usersTweets->takeUntil($endTwitterStream); 68 | 69 | $urls = $tweets->flatMap(function ($tweet) { 70 | return Observable::fromArray($tweet->entities->urls); 71 | }); 72 | 73 | $measurementsSubscription = $urls 74 | ->filter(function ($url) { 75 | return 0 === strpos($url->expanded_url, 'https://atlas.ripe.net/measurements/'); 76 | }) 77 | ->map(function ($url) { 78 | return trim(substr($url->expanded_url, 36), '/'); 79 | }) 80 | ->flatMap(function ($id) { 81 | return Http::get("https://atlas.ripe.net/api/v1/measurement/{$id}/"); 82 | }) 83 | ->map('json_decode') 84 | ->subscribe( 85 | function ($json) { 86 | echo 'Measurement #', $json->msm_id, ' "', $json->description, '" had ', $json->participant_count, ' nodes involved', PHP_EOL; 87 | }, 88 | function (HttpResponseException $e) { 89 | echo 'Error: ', $e->getMessage(), PHP_EOL; 90 | }, 91 | function () { 92 | echo 'complete', PHP_EOL; 93 | } 94 | ); 95 | 96 | $probesSubscription = $urls 97 | ->filter(function ($url) { 98 | return 0 === strpos($url->expanded_url, 'https://atlas.ripe.net/probes/'); 99 | }) 100 | ->map(function ($url) { 101 | return trim(substr($url->expanded_url, 30), '/'); 102 | }) 103 | ->flatMap(function ($id) { 104 | return Http::get("https://atlas.ripe.net/api/v1/probe/{$id}/"); 105 | }) 106 | ->map('json_decode') 107 | ->subscribe( 108 | function ($json) { 109 | echo 'Probe #', $json->id, ' connected since ' . date('r', $json->status_since), PHP_EOL; 110 | }, 111 | function (\Throwable $e) { 112 | echo 'Error: ', $e->getMessage(), PHP_EOL; 113 | }, 114 | function () { 115 | echo 'complete', PHP_EOL; 116 | } 117 | ); 118 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests/ 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | client = new HttpClient($loop, $connector); 25 | } 26 | 27 | public function request(string $method, string $url, string $body = null, array $headers = [], string $protocolVersion = '1.1'): HttpObservable 28 | { 29 | return new HttpObservable($method, $url, $body, $headers, $protocolVersion, $this->client); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Http.php: -------------------------------------------------------------------------------- 1 | getMethod(); 12 | $url = $request->getUri(); 13 | $body = $request->getBody()->getContents(); 14 | $headers = $request->getHeaders(); 15 | $protocolVersion = $request->getProtocolVersion(); 16 | 17 | return (new Client)->request($method, $url, $body, $headers, $protocolVersion); 18 | } 19 | 20 | public static function get(string $url, array $headers = [], string $protocolVersion = '1.1'): HttpObservable 21 | { 22 | return (new Client)->request('GET', $url, null, $headers, $protocolVersion); 23 | } 24 | 25 | public static function post(string $url, string $body = null, array $headers = [], string $protocolVersion = '1.1'): HttpObservable 26 | { 27 | return (new Client)->request('POST', $url, $body, $headers, $protocolVersion); 28 | } 29 | 30 | public static function put(string $url, string $body = null, array $headers = [], string $protocolVersion = '1.1'): HttpObservable 31 | { 32 | return (new Client)->request('PUT', $url, $body, $headers, $protocolVersion); 33 | } 34 | 35 | public static function delete(string $url, array $headers = [], string $protocolVersion = '1.1'): HttpObservable 36 | { 37 | return (new Client)->request('DELETE', $url, null, $headers, $protocolVersion); 38 | } 39 | 40 | public static function patch(string $url, string $body = null, array $headers = [], string $protocolVersion = '1.1'): HttpObservable 41 | { 42 | return (new Client)->request('PATCH', $url, $body, $headers, $protocolVersion); 43 | } 44 | 45 | public static function head(string $url, array $headers = [], string $protocolVersion = '1.1'): HttpObservable 46 | { 47 | return (new Client)->request('HEAD', $url, null, $headers, $protocolVersion); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/HttpObservable.php: -------------------------------------------------------------------------------- 1 | method = $method; 28 | $this->url = $url; 29 | $this->body = $body; 30 | $this->headers = $headers; 31 | $this->protocolVersion = $protocolVersion; 32 | $this->scheduler = Scheduler::getDefault(); 33 | $this->client = $client; 34 | } 35 | 36 | protected function _subscribe(ObserverInterface $observer): DisposableInterface 37 | { 38 | $this->setContentLength(); 39 | 40 | $scheduler = $this->scheduler; 41 | $buffer = ''; 42 | $request = $this->client->request($this->method, $this->url, $this->headers, $this->protocolVersion); 43 | 44 | $request->on('response', function (Response $response) use (&$buffer, $observer, $request, $scheduler) { 45 | $response->on('data', function ($data) use (&$buffer, $observer, $request, $scheduler, $response) { 46 | 47 | try { 48 | //Buffer the data if we get a http error 49 | $code = $response->getCode(); 50 | if ($code < 200 || $code >= 400) { 51 | $buffer .= $data; 52 | return; 53 | } 54 | 55 | if ($this->bufferResults) { 56 | $buffer .= $data; 57 | } else { 58 | $data = $this->includeResponse ? [$data, $response, $request] : $data; 59 | $scheduler->schedule(function () use ($observer, $data) { 60 | $observer->onNext($data); 61 | }); 62 | } 63 | 64 | } catch (\Exception $e) { 65 | $observer->onError($e); 66 | } 67 | }); 68 | 69 | $response->on('error', function ($e) use ($observer) { 70 | $error = new \Exception($e); 71 | $observer->onError($error); 72 | }); 73 | 74 | $response->on('end', function ($end = null) use (&$buffer, $observer, $request, $response, $scheduler) { 75 | 76 | $code = $response->getCode(); 77 | if ($code < 200 || $code >= 400) { 78 | $error = new HttpResponseException($request, $response, $response->getReasonPhrase(), $response->getCode(), $buffer); 79 | $observer->onError($error); 80 | return; 81 | } 82 | 83 | if ($this->bufferResults) { 84 | $data = $this->includeResponse ? [$buffer, $response, $request] : $buffer; 85 | $scheduler->schedule(function () use ($observer, $data, $scheduler) { 86 | $observer->onNext($data); 87 | $scheduler->schedule(function () use ($observer) { 88 | $observer->onCompleted(); 89 | }); 90 | }); 91 | 92 | return; 93 | } 94 | 95 | $scheduler->schedule(function () use ($observer) { 96 | $observer->onCompleted(); 97 | }); 98 | }); 99 | }); 100 | $request->end($this->body); 101 | 102 | $request->on('error', function ($e) use ($observer) { 103 | $error = new \Exception($e); 104 | $observer->onError($error); 105 | }); 106 | 107 | return new CallbackDisposable(function () use ($request) { 108 | $request->close(); 109 | }); 110 | } 111 | 112 | /** 113 | * Will not buffer the result. 114 | * 115 | * @return $this 116 | */ 117 | public function streamResults(): self 118 | { 119 | $this->bufferResults = false; 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * The observable will emit an array that includes the data, request, and response. 126 | * 127 | * @return $this 128 | */ 129 | public function includeResponse(): self 130 | { 131 | $this->includeResponse = true; 132 | 133 | return $this; 134 | } 135 | 136 | /** 137 | * Adds if needed a `Content-Length` header field to the request. 138 | */ 139 | private function setContentLength() 140 | { 141 | if (!is_string($this->body)) { 142 | return; 143 | } 144 | 145 | $headers = array_map('strtolower', array_keys($this->headers)); 146 | 147 | if (!in_array('content-length', $headers, true)) { 148 | $this->headers['Content-Length'] = strlen($this->body); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/HttpResponseException.php: -------------------------------------------------------------------------------- 1 | request = $request; 17 | $this->response = $response; 18 | $this->body = $body; 19 | parent::__construct($message, $code, $previous); 20 | } 21 | 22 | public function getResponse(): Response 23 | { 24 | return $this->response; 25 | } 26 | 27 | public function getRequest(): Request 28 | { 29 | return $this->request; 30 | } 31 | 32 | public function getBody(): string 33 | { 34 | return $this->body; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Functional/HttpClientStub.php: -------------------------------------------------------------------------------- 1 | request = $request; 16 | } 17 | 18 | public function request($method, $url, array $headers = [], $protocolVersion = '1.0') 19 | { 20 | return $this->request; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Functional/Observable/HttpObservableTest.php: -------------------------------------------------------------------------------- 1 | connector, $requestData); 30 | $response = new Response($this->stream, 'HTTP', '1.0', '200', 'OK', ['Content-Type' => 'text/plain']); 31 | $source = $this->createHttpObservable($request, $method, $url); 32 | 33 | $source->subscribe(new CallbackObserver( 34 | function ($value) use (&$result) { 35 | $result = $value; 36 | }, 37 | function ($e) use (&$error) { 38 | $error = true; 39 | }, 40 | function () use (&$complete) { 41 | $complete = true; 42 | } 43 | )); 44 | 45 | $request->emit("response", [$response]); 46 | $response->emit("data", [$testData1, $response]); 47 | $response->emit("data", [$testData2, $response]); 48 | $response->emit("data", [$testData3, $response]); 49 | $response->emit("end"); 50 | 51 | $this->assertEquals($result, $testData1 . $testData2 . $testData3); 52 | $this->assertTrue($complete); 53 | $this->assertFalse($error); 54 | 55 | } 56 | 57 | 58 | /** 59 | * @test 60 | */ 61 | public function http_without_buffer() 62 | { 63 | $testData = str_repeat("1", 69536); //1k, so it does not use the buffer 64 | $error = false; 65 | 66 | $method = "GET"; 67 | $url = "https://www.example.com"; 68 | $requestData = new RequestData($method, $url); 69 | $request = new Request($this->connector, $requestData); 70 | $response = new Response($this->stream, 'HTTP', '1.0', '200', 'OK', ['Content-Type' => 'text/plain']); 71 | $source = $this->createHttpObservable($request, $method, $url); 72 | 73 | $source->subscribe(new CallbackObserver( 74 | function ($value) use (&$result) { 75 | $result = $value; 76 | }, 77 | function ($e) use (&$error) { 78 | $error = true; 79 | }, 80 | function () use (&$complete) { 81 | $complete = true; 82 | } 83 | )); 84 | 85 | $request->emit("response", [$response]); 86 | $response->emit("data", [$testData, $response]); 87 | $response->emit("end"); 88 | 89 | $this->assertEquals($result, $testData); 90 | $this->assertTrue($complete); 91 | $this->assertFalse($error); 92 | 93 | } 94 | 95 | /** 96 | * @test 97 | */ 98 | public function http_with_stream() 99 | { 100 | $testData1 = str_repeat("1", 69536); 101 | $testData2 = str_repeat("1", 69536); 102 | $testData3 = str_repeat("1", 69536); 103 | 104 | $error = false; 105 | $result = false; 106 | $invoked = 0; 107 | 108 | $method = "GET"; 109 | $url = "https://www.example.com"; 110 | $requestData = new RequestData($method, $url); 111 | $request = new Request($this->connector, $requestData); 112 | $response = new Response($this->stream, 'HTTP', '1.0', '200', 'OK', ['Content-Type' => 'text/plain']); 113 | $source = $this->createHttpObservable($request, $method, $url) 114 | ->streamResults(); 115 | 116 | $source->subscribe(new CallbackObserver( 117 | function ($v) use (&$result, &$invoked, &$value) { 118 | $result = true; 119 | $invoked++; 120 | $value .= $v; 121 | }, 122 | function ($e) use (&$error) { 123 | $error = true; 124 | }, 125 | function () use (&$complete) { 126 | $complete = true; 127 | } 128 | )); 129 | 130 | $request->emit("response", [$response]); 131 | $response->emit("data", [$testData1, $response]); 132 | $response->emit("data", [$testData2, $response]); 133 | $response->emit("data", [$testData3, $response]); 134 | $response->emit("end"); 135 | 136 | $this->assertTrue($result); 137 | $this->assertTrue($complete); 138 | $this->assertFalse($error); 139 | 140 | $this->assertEquals($invoked, 3); 141 | $this->assertEquals($value, $testData1 . $testData2 . $testData3); 142 | 143 | } 144 | 145 | /** 146 | * @test 147 | */ 148 | public function http_with_error() 149 | { 150 | $testData = str_repeat("1", 69536); //1k, so it does not use the buffer 151 | $error = false; 152 | $complete = false; 153 | 154 | $method = "GET"; 155 | $url = "https://www.example.com"; 156 | $requestData = new RequestData($method, $url); 157 | $request = new Request($this->connector, $requestData); 158 | $response = new Response($this->stream, 'HTTP', '1.0', '500', 'OK', ['Content-Type' => 'text/plain']); 159 | $source = $this->createHttpObservable($request, $method, $url); 160 | 161 | $source->subscribe(new CallbackObserver( 162 | function ($value) use (&$result) { 163 | $result = $value; 164 | }, 165 | function ($e) use (&$error) { 166 | $error = true; 167 | }, 168 | function () use (&$complete) { 169 | $complete = true; 170 | } 171 | )); 172 | 173 | $request->emit("response", [$response]); 174 | $response->emit("data", [$testData, $response]); 175 | $response->emit("end"); 176 | 177 | $this->assertNotEquals($result, $testData); 178 | $this->assertFalse($complete); 179 | $this->assertTrue($error); 180 | 181 | } 182 | 183 | /** 184 | * @test 185 | */ 186 | public function http_with_error_with_body() 187 | { 188 | $testData = str_repeat('1', 69536); //1k, so it does not use the buffer 189 | $error = null; 190 | $complete = false; 191 | 192 | $method = 'GET'; 193 | $url = 'https://www.example.com'; 194 | $requestData = new RequestData($method, $url); 195 | $request = new Request($this->connector, $requestData); 196 | $response = new Response($this->stream, 'HTTP', '1.0', '500', 'OK', ['Content-Type' => 'text/plain']); 197 | $source = $this->createHttpObservable($request, $method, $url); 198 | 199 | $source->subscribe(new CallbackObserver( 200 | function ($value) use (&$result) { 201 | $result = $value; 202 | }, 203 | function (HttpResponseException $e) use (&$error) { 204 | $error = $e; 205 | }, 206 | function () use (&$complete) { 207 | $complete = true; 208 | } 209 | )); 210 | 211 | $request->emit('response', [$response]); 212 | $response->emit('data', [$testData, $response]); 213 | $response->emit('end'); 214 | 215 | $this->assertEquals($error->getBody(), $testData); 216 | $this->assertFalse($complete); 217 | } 218 | 219 | /** 220 | * @test 221 | */ 222 | public function http_with_error_with_body_buffered() 223 | { 224 | $testData1 = str_repeat('1', 69536); 225 | $testData2 = str_repeat('1', 69536); 226 | $testData3 = str_repeat('1', 69536); 227 | 228 | $error = null; 229 | $complete = false; 230 | 231 | $method = 'GET'; 232 | $url = 'https://www.example.com'; 233 | $requestData = new RequestData($method, $url); 234 | $request = new Request($this->connector, $requestData); 235 | $response = new Response($this->stream, 'HTTP', '1.0', '500', 'OK', ['Content-Type' => 'text/plain']); 236 | $source = $this->createHttpObservable($request, $method, $url); 237 | 238 | $source->subscribe(new CallbackObserver( 239 | function ($value) use (&$result) { 240 | $result = $value; 241 | }, 242 | function (HttpResponseException $e) use (&$error) { 243 | $error = $e; 244 | }, 245 | function () use (&$complete) { 246 | $complete = true; 247 | } 248 | )); 249 | 250 | $request->emit('response', [$response]); 251 | $response->emit('data', [$testData1, $response]); 252 | $response->emit('data', [$testData2, $response]); 253 | $response->emit('data', [$testData3, $response]); 254 | $response->emit('end'); 255 | 256 | $this->assertEquals($error->getBody(), $testData1 . $testData2 . $testData3); 257 | $this->assertFalse($complete); 258 | } 259 | 260 | /** 261 | * @test 262 | */ 263 | public function http_with_includeResponse() 264 | { 265 | $testData = str_repeat("1", 69536); //1k, so it does not use the buffer 266 | $error = false; 267 | $complete = false; 268 | 269 | $method = "GET"; 270 | $url = "https://www.example.com"; 271 | $requestData = new RequestData($method, $url); 272 | $request = new Request($this->connector, $requestData); 273 | $response = new Response($this->stream, 'HTTP', '1.0', '200', 'OK', ['Content-Type' => 'text/plain']); 274 | $source = $this->createHttpObservable($request, $method, $url) 275 | ->includeResponse(); 276 | 277 | $source->subscribe(new CallbackObserver( 278 | function ($value) use (&$result) { 279 | $result = $value; 280 | }, 281 | function ($e) use (&$error) { 282 | $error = true; 283 | }, 284 | function () use (&$complete) { 285 | $complete = true; 286 | } 287 | )); 288 | 289 | $request->emit("response", [$response]); 290 | $response->emit("data", [$testData, $response]); 291 | $response->emit("end"); 292 | 293 | $this->assertEquals($result[0], $testData); 294 | $this->assertInstanceOf(Response::class, $result[1]); 295 | $this->assertInstanceOf(Request::class, $result[2]); 296 | $this->assertTrue($complete); 297 | $this->assertFalse($error); 298 | 299 | } 300 | 301 | /** 302 | * @test 303 | */ 304 | public function http_with_includeResponse_with_buffer() 305 | { 306 | $testData1 = str_repeat("1", 69536); 307 | $testData2 = str_repeat("1", 69536); 308 | $testData3 = str_repeat("1", 69536); 309 | $complete = false; 310 | $error = false; 311 | 312 | $method = "GET"; 313 | $url = "https://www.example.com"; 314 | $requestData = new RequestData($method, $url); 315 | $request = new Request($this->connector, $requestData); 316 | $response = new Response($this->stream, 'HTTP', '1.0', '200', 'OK', ['Content-Type' => 'text/plain']); 317 | $source = $this->createHttpObservable($request, $method, $url) 318 | ->includeResponse(); 319 | 320 | $source->subscribe(new CallbackObserver( 321 | function ($value) use (&$result) { 322 | $result = $value; 323 | }, 324 | function ($e) use (&$error) { 325 | $error = true; 326 | }, 327 | function () use (&$complete) { 328 | $complete = true; 329 | } 330 | )); 331 | 332 | $request->emit("response", [$response]); 333 | $response->emit("data", [$testData1, $response]); 334 | $response->emit("data", [$testData2, $response]); 335 | $response->emit("data", [$testData3, $response]); 336 | $response->emit("end"); 337 | 338 | $this->assertEquals($result[0], $testData1 . $testData2 . $testData3); 339 | $this->assertInstanceOf(Response::class, $result[1]); 340 | $this->assertInstanceOf(Request::class, $result[2]); 341 | $this->assertTrue($complete); 342 | $this->assertFalse($error); 343 | 344 | } 345 | 346 | /** 347 | * @test 348 | */ 349 | public function http_post_with_content_length() 350 | { 351 | $testData = str_repeat('1', 69536); //1k, so it does not use the buffer 352 | $error = null; 353 | $complete = false; 354 | 355 | $method = 'POST'; 356 | $url = 'https://www.example.com'; 357 | $requestData = new RequestData($method, $url); 358 | $request = new Request($this->connector, $requestData); 359 | $response = new Response($this->stream, 'HTTP', '1.0', '500', 'OK', ['Content-Type' => 'text/plain']); 360 | $source = $this->createHttpObservable($request, $method, $url); 361 | 362 | $source->subscribe(new CallbackObserver( 363 | function ($value) use (&$result) { 364 | $result = $value; 365 | }, 366 | function (HttpResponseException $e) use (&$error) { 367 | $error = $e; 368 | }, 369 | function () use (&$complete) { 370 | $complete = true; 371 | } 372 | )); 373 | 374 | $request->emit('response', [$response]); 375 | $response->emit('data', [$testData, $response]); 376 | $response->emit('end'); 377 | 378 | $this->assertEquals($error->getBody(), $testData); 379 | $this->assertFalse($complete); 380 | } 381 | 382 | } 383 | -------------------------------------------------------------------------------- /tests/Functional/TestCase.php: -------------------------------------------------------------------------------- 1 | stream = $this->getMockBuilder(ReadableStreamInterface::class) 20 | ->disableOriginalConstructor() 21 | ->getMock(); 22 | 23 | $this->connector = $this->createMock(ConnectorInterface::class); 24 | 25 | $this->connector->expects($this->once()) 26 | ->method('connect') 27 | ->willReturnCallback(function () { 28 | 29 | return new Promise(function () { 30 | }); 31 | }); 32 | } 33 | 34 | protected function createHttpObservable( 35 | Request $request, 36 | string $method, 37 | string $url, 38 | string $body = null, 39 | array $headers = [], 40 | string $protocolVersion = '1.1' 41 | ): HttpObservable 42 | { 43 | $reflection = new \ReflectionClass(HttpObservable::class); 44 | $client_property = $reflection->getProperty('client'); 45 | $client_property->setAccessible(true); 46 | 47 | $properties = [ 48 | 'client' => new HttpClientStub($request), 49 | 'method' => $method, 50 | 'url' => $url, 51 | 'body' => $body, 52 | 'headers' => $headers, 53 | 'protocolVersion' => $protocolVersion, 54 | 'scheduler' => Scheduler::getImmediate() 55 | ]; 56 | 57 | $httpObservable = $reflection->newInstanceWithoutConstructor(); 58 | 59 | foreach ($properties as $key => $property) { 60 | $p = $reflection->getProperty($key); 61 | $p->setAccessible(true); 62 | $p->setValue($httpObservable, $property); 63 | } 64 | 65 | return $httpObservable; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |