diff --git a/README.md b/README.md index b967d8d..ebdb165 100644 --- a/README.md +++ b/README.md @@ -49,12 +49,6 @@ Its constructor simply requires the URL to the remote Server-Sent Events (SSE) e $es = new Clue\React\EventSource\EventSource('https://example.com/stream.php'); ``` -This class takes an optional `LoopInterface|null $loop` parameter that can be used to -pass the event loop instance to use for this object. You can use a `null` value -here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). -This value SHOULD NOT be given unless you're sure you want to explicitly use a -given event loop instance. - If you need custom connector settings (DNS resolution, TLS parameters, timeouts, proxy servers etc.), you can explicitly pass a custom instance of the [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface) @@ -74,9 +68,15 @@ $connector = new React\Socket\Connector([ ]); $browser = new React\Http\Browser($connector); -$es = new Clue\React\EventSource\EventSource('https://example.com/stream.php', null, $browser); +$es = new Clue\React\EventSource\EventSource('https://example.com/stream.php', $browser); ``` +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + ## Install The recommended way to install this library is [through Composer](https://getcomposer.org). diff --git a/src/EventSource.php b/src/EventSource.php index d85d166..d6b03e8 100644 --- a/src/EventSource.php +++ b/src/EventSource.php @@ -82,12 +82,52 @@ class EventSource extends EventEmitter private $reconnectTime = 3.0; /** + * The `EventSource` class is responsible for communication with the remote Server-Sent Events (SSE) endpoint. + * + * The `EventSource` object works very similar to the one found in common + * web browsers. Unless otherwise noted, it follows the same semantics as defined + * under https://html.spec.whatwg.org/multipage/server-sent-events.html + * + * Its constructor simply requires the URL to the remote Server-Sent Events (SSE) endpoint: + * + * ```php + * $es = new Clue\React\EventSource\EventSource('https://example.com/stream.php'); + * ``` + * + * If you need custom connector settings (DNS resolution, TLS parameters, timeouts, + * proxy servers etc.), you can explicitly pass a custom instance of the + * [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface) + * to the [`Browser`](https://github.com/reactphp/http#browser) instance + * and pass it as an additional argument to the `EventSource` like this: + * + * ```php + * $connector = new React\Socket\Connector([ + * 'dns' => '127.0.0.1', + * 'tcp' => [ + * 'bindto' => '192.168.10.1:0' + * ], + * 'tls' => [ + * 'verify_peer' => false, + * 'verify_peer_name' => false + * ] + * ]); + * $browser = new React\Http\Browser($connector); + * + * $es = new Clue\React\EventSource\EventSource('https://example.com/stream.php', $browser); + * ``` + * + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * * @param string $url - * @param ?LoopInterface $loop * @param ?Browser $browser + * @param ?LoopInterface $loop * @throws \InvalidArgumentException for invalid URL */ - public function __construct($url, LoopInterface $loop = null, Browser $browser = null) + public function __construct($url, Browser $browser = null, LoopInterface $loop = null) { $parts = parse_url($url); if (!isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('http', 'https'))) { diff --git a/tests/EventSourceTest.php b/tests/EventSourceTest.php index 0cf440f..5c8b1bf 100644 --- a/tests/EventSourceTest.php +++ b/tests/EventSourceTest.php @@ -4,10 +4,9 @@ use Clue\React\EventSource\EventSource; use PHPUnit\Framework\TestCase; -use React\Promise\Promise; -use React\Promise\Deferred; -use React\Http\Browser; use React\Http\Io\ReadableBodyStream; +use React\Promise\Deferred; +use React\Promise\Promise; use React\Stream\ThroughStream; use RingCentral\Psr7\Response; @@ -15,28 +14,31 @@ class EventSourceTest extends TestCase { public function testConstructorThrowsIfFirstArgumentIsNotAnUri() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $this->setExpectedException('InvalidArgumentException'); - new EventSource('///', $loop); + new EventSource('///'); } public function testConstructorThrowsIfUriArgumentDoesNotIncludeScheme() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $this->setExpectedException('InvalidArgumentException'); - new EventSource('example.com', $loop); + new EventSource('example.com'); } public function testConstructorThrowsIfUriArgumentIncludesInvalidScheme() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $this->setExpectedException('InvalidArgumentException'); - new EventSource('ftp://example.com', $loop); + new EventSource('ftp://example.com'); } - public function testConstructWithoutLoopAssignsLoopAutomatically() + public function testConstructWithoutBrowserAndLoopAssignsBrowserAndLoopAutomatically() { - $es = new EventSource('http://example.invalid'); + $es = new EventSource('http://example.com'); + + $ref = new \ReflectionProperty($es, 'browser'); + $ref->setAccessible(true); + $browser = $ref->getValue($es); + + $this->assertInstanceOf('React\Http\Browser', $browser); $ref = new \ReflectionProperty($es, 'loop'); $ref->setAccessible(true); @@ -47,47 +49,28 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() $es->close(); } - public function testConstructorCanBeCalledWithoutBrowser() - { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - - $es = new EventSource('http://example.invalid', $loop); - - $ref = new \ReflectionProperty($es, 'browser'); - $ref->setAccessible(true); - $browser = $ref->getValue($es); - - $this->assertInstanceOf('React\Http\Browser', $browser); - } - public function testConstructorWillSendGetRequestThroughGivenBrowser() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $pending = new Promise(function () { }); $browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock(); $browser->expects($this->once())->method('withRejectErrorResponse')->with(false)->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->with('GET', 'http://example.com')->willReturn($pending); - $es = new EventSource('http://example.com', $loop, $browser); + new EventSource('http://example.com', $browser); } public function testConstructorWillSendGetRequestThroughGivenBrowserWithHttpsScheme() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $pending = new Promise(function () { }); $browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock(); $browser->expects($this->once())->method('withRejectErrorResponse')->with(false)->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->with('GET', 'https://example.com')->willReturn($pending); - $es = new EventSource('https://example.com', $loop, $browser); + new EventSource('https://example.com', $browser); } public function testCloseWillCancelPendingGetRequest() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $cancelled = null; $pending = new Promise(function () { }, function () use (&$cancelled) { ++$cancelled; @@ -96,7 +79,7 @@ public function testCloseWillCancelPendingGetRequest() $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($pending); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser); $es->close(); $this->assertEquals(1, $cancelled); @@ -104,8 +87,6 @@ public function testCloseWillCancelPendingGetRequest() public function testCloseWillNotEmitErrorEventWhenGetRequestCancellationHandlerRejects() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $pending = new Promise(function () { }, function () { throw new \RuntimeException(); }); @@ -113,7 +94,7 @@ public function testCloseWillNotEmitErrorEventWhenGetRequestCancellationHandlerR $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($pending); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser); $error = null; $es->on('error', function ($e) use (&$error) { @@ -138,7 +119,7 @@ public function testConstructorWillStartGetRequestThatWillStartRetryTimerWhenGet $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + new EventSource('http://example.com', $browser, $loop); $deferred->reject(new \RuntimeException()); } @@ -163,7 +144,7 @@ public function testConstructorWillStartGetRequestThatWillStartRetryTimerThatWil new Promise(function () { }) ); - $es = new EventSource('http://example.com', $loop, $browser); + new EventSource('http://example.com', $browser, $loop); $deferred->reject(new \RuntimeException()); @@ -184,7 +165,7 @@ public function testConstructorWillStartGetRequestThatWillEmitErrorWhenGetReques $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser, $loop); $caught = null; $es->on('error', function ($e) use (&$caught) { @@ -197,14 +178,12 @@ public function testConstructorWillStartGetRequestThatWillEmitErrorWhenGetReques public function testConstructorWillStartGetRequestThatWillNotStartRetryTimerWhenGetRequestRejectAndErrorHandlerClosesExplicitly() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $deferred = new Deferred(); $browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock(); $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser); $es->on('error', function () use ($es) { $es->close(); @@ -227,7 +206,7 @@ public function testCloseAfterGetRequestFromConstructorFailsWillCancelPendingRet $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser, $loop); $deferred->reject(new \RuntimeException()); @@ -236,14 +215,12 @@ public function testCloseAfterGetRequestFromConstructorFailsWillCancelPendingRet public function testConstructorWillReportFatalErrorWhenGetResponseResolvesWithInvalidStatusCode() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $deferred = new Deferred(); $browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock(); $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser); $readyState = null; $caught = null; @@ -261,14 +238,12 @@ public function testConstructorWillReportFatalErrorWhenGetResponseResolvesWithIn public function testConstructorWillReportFatalErrorWhenGetResponseResolvesWithInvalidContentType() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $deferred = new Deferred(); $browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock(); $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser); $readyState = null; $caught = null; @@ -286,14 +261,12 @@ public function testConstructorWillReportFatalErrorWhenGetResponseResolvesWithIn public function testConstructorWillReportOpenWhenGetResponseResolvesWithValidResponse() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $deferred = new Deferred(); $browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock(); $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser); $readyState = null; $es->on('open', function () use ($es, &$readyState) { @@ -309,14 +282,12 @@ public function testConstructorWillReportOpenWhenGetResponseResolvesWithValidRes public function testConstructorWillReportOpenWhenGetResponseResolvesWithValidResponseWithCaseInsensitiveContentType() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $deferred = new Deferred(); $browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock(); $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser); $readyState = null; $es->on('open', function () use ($es, &$readyState) { @@ -332,14 +303,12 @@ public function testConstructorWillReportOpenWhenGetResponseResolvesWithValidRes public function testConstructorWillReportOpenWhenGetResponseResolvesWithValidResponseAndSuperfluousParametersAfterTimer() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $deferred = new Deferred(); $browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock(); $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser); $readyState = null; $es->on('open', function () use ($es, &$readyState) { @@ -366,7 +335,7 @@ public function testCloseResponseStreamWillStartRetryTimerWithErrorEvent() $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser, $loop); $stream = new ThroughStream(); $response = new Response(200, array('Content-Type' => 'text/event-stream'), new ReadableBodyStream($stream)); @@ -394,7 +363,7 @@ public function testCloseResponseStreamWillNotStartRetryTimerWhenEventSourceIsCl $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser, $loop); $stream = new ThroughStream(); $response = new Response(200, array('Content-Type' => 'text/event-stream'), new ReadableBodyStream($stream)); @@ -411,14 +380,12 @@ public function testCloseResponseStreamWillNotStartRetryTimerWhenEventSourceIsCl public function testCloseFromOpenEventWillCloseResponseStreamAndCloseEventSource() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $deferred = new Deferred(); $browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock(); $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser); $es->on('open', function () use ($es) { $es->close(); @@ -434,14 +401,12 @@ public function testCloseFromOpenEventWillCloseResponseStreamAndCloseEventSource public function testEmitMessageWithParsedDataFromEventStream() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $deferred = new Deferred(); $browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock(); $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser); $stream = new ThroughStream(); $response = new Response(200, array('Content-Type' => 'text/event-stream'), new ReadableBodyStream($stream)); @@ -461,14 +426,12 @@ public function testEmitMessageWithParsedDataFromEventStream() public function testEmitMessageWithParsedIdAndDataOverMultipleRowsFromEventStream() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $deferred = new Deferred(); $browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock(); $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser); $stream = new ThroughStream(); $response = new Response(200, array('Content-Type' => 'text/event-stream'), new ReadableBodyStream($stream)); @@ -488,14 +451,12 @@ public function testEmitMessageWithParsedIdAndDataOverMultipleRowsFromEventStrea public function testEmitMessageWithParsedEventTypeAndDataWithTrailingWhitespaceFromEventStream() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $deferred = new Deferred(); $browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock(); $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser); $stream = new ThroughStream(); $response = new Response(200, array('Content-Type' => 'text/event-stream'), new ReadableBodyStream($stream)); @@ -514,14 +475,12 @@ public function testEmitMessageWithParsedEventTypeAndDataWithTrailingWhitespaceF public function testDoesNotEmitMessageWhenParsedEventStreamHasNoData() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $deferred = new Deferred(); $browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock(); $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser); $stream = new ThroughStream(); $response = new Response(200, array('Content-Type' => 'text/event-stream'), new ReadableBodyStream($stream)); @@ -539,14 +498,12 @@ public function testDoesNotEmitMessageWhenParsedEventStreamHasNoData() public function testEmitMessageWithParsedDataAndPreviousIdWhenNotGivenAgainFromEventStream() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $deferred = new Deferred(); $browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock(); $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser); $stream = new ThroughStream(); $response = new Response(200, array('Content-Type' => 'text/event-stream'), new ReadableBodyStream($stream)); @@ -566,14 +523,12 @@ public function testEmitMessageWithParsedDataAndPreviousIdWhenNotGivenAgainFromE public function testEmitMessageOnceWhenCallingCloseFromMessageHandlerFromEventStreamWithMultipleMessages() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $deferred = new Deferred(); $browser = $this->getMockBuilder('React\Http\Browser')->disableOriginalConstructor()->getMock(); $browser->expects($this->once())->method('withRejectErrorResponse')->willReturnSelf(); $browser->expects($this->once())->method('requestStreaming')->willReturn($deferred->promise()); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser); $stream = new ThroughStream(); $response = new Response(200, array('Content-Type' => 'text/event-stream'), new ReadableBodyStream($stream)); @@ -615,7 +570,7 @@ public function testReconnectAfterStreamClosesUsesLastEventIdFromParsedEventStre new Promise(function () { }) ); - $es = new EventSource('http://example.com', $loop, $browser); + $es = new EventSource('http://example.com', $browser, $loop); $stream = new ThroughStream(); $response = new Response(200, array('Content-Type' => 'text/event-stream'), new ReadableBodyStream($stream));