Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ of [ReactPHP](https://reactphp.org/)'s event-driven architecture.
* [EventSource::$url](#eventsourceurl)
* [close()](#close)
* [MessageEvent](#messageevent)
* [MessageEvent::__construct()](#messageevent__construct)
* [MessageEvent::$data](#messageeventdata)
* [MessageEvent::$lastEventId](#messageeventlasteventid)
* [MessageEvent::$type](#messageeventtype)
Expand Down Expand Up @@ -240,6 +241,19 @@ This will close any active connections or connection attempts and go into the

The `MessageEvent` class represents an incoming EventSource message.

#### MessageEvent::__construct()

The `new MessageEvent(string $data, string $lastEventId = '', string $type = 'message')` constructor can be used to
create a new `MessageEvent` instance.

This is mostly used internally to represent each incoming message event
(see also [`message` event](#message-event)). Likewise, you can also use
this class in test cases to test how your application reacts to incoming
messages.

The constructor validates and initializes all properties of this class.
It throws an `InvalidArgumentException` if any parameters are invalid.

#### MessageEvent::$data

The `readonly string $data` property can be used to
Expand Down
43 changes: 35 additions & 8 deletions src/MessageEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public static function parse($data, $lastEventId, &$retryTime = 0.0)
$data = substr($data, 0, -1);
}

/** @throws void because parameter values are validated above already */
return new self($data, $id, $type);
}

Expand All @@ -52,15 +53,41 @@ private static function utf8($string)
return \htmlspecialchars_decode(\htmlspecialchars($string, \ENT_NOQUOTES | \ENT_SUBSTITUTE, 'utf-8'));
}

/** @return bool */
private static function isUtf8($string)
{
return $string === self::utf8($string);
}

/**
* @internal
* @param string $data
* @param string $lastEventId
* @param string $type
* Create a new `MessageEvent` instance.
*
* This is mostly used internally to represent each incoming message event
* (see also [`message` event](#message-event)). Likewise, you can also use
* this class in test cases to test how your application reacts to incoming
* messages.
*
* The constructor validates and initializes all properties of this class.
* It throws an `InvalidArgumentException` if any parameters are invalid.
*
* @param string $data message data (requires valid UTF-8 data, possibly multi-line)
* @param string $lastEventId optional last event ID (defaults to empty string, requires valid UTF-8, no null bytes, single line)
* @param string $type optional event type (defaults to "message", requires valid UTF-8, single line)
* @throws \InvalidArgumentException if any parameters are invalid
*/
private function __construct($data, $lastEventId, $type)
final public function __construct($data, $lastEventId = '', $type = 'message')
{
$this->data = $data;
if (!self::isUtf8($data)) {
throw new \InvalidArgumentException('Invalid $data given, must be valid UTF-8 string');
}
if (!self::isUtf8($lastEventId) || \strpos($lastEventId, "\0") !== false || \strpos($lastEventId, "\r") !== false || \strpos($lastEventId, "\n") !== false) {
throw new \InvalidArgumentException('Invalid $lastEventId given, must be valid UTF-8 string with no null bytes or newline characters');
}
if (!self::isUtf8($type) || $type === '' || \strpos($type, "\r") !== false || \strpos($type, "\n")) {
throw new \InvalidArgumentException('Invalid $type given, must be valid UTF-8 string with no newline characters');
}

$this->data = \preg_replace("/\r\n?/", "\n", $data);
$this->lastEventId = $lastEventId;
$this->type = $type;
}
Expand All @@ -72,13 +99,13 @@ private function __construct($data, $lastEventId, $type)
public $data = '';

/**
* @var string
* @var string defaults to empty string
* @readonly
*/
public $lastEventId = '';

/**
* @var string
* @var string defaults to "message"
* @readonly
*/
public $type = 'message';
Expand Down
105 changes: 105 additions & 0 deletions tests/MessageEventTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,109 @@ public function testParseRetryTime($input, $expected)

$this->assertSame($expected, $retryTime);
}

public function testConstructWithDefaultLastEventIdAndType()
{
$message = new MessageEvent('hello');

$this->assertEquals('hello', $message->data);
$this->assertEquals('', $message->lastEventId);
$this->assertEquals('message', $message->type);
}

public function testConstructWithEmptyDataAndId()
{
$message = new MessageEvent('', '');

$this->assertEquals('', $message->data);
$this->assertEquals('', $message->lastEventId);
$this->assertEquals('message', $message->type);
}

public function testConstructWithNullBytesInDataAndType()
{
$message = new MessageEvent("h\x00llo!", '', "h\x00llo!");

$this->assertEquals("h\x00llo!", $message->data);
$this->assertEquals('', $message->lastEventId);
$this->assertEquals("h\x00llo!", $message->type);
}

public function testConstructWithCarriageReturnAndLineFeedsInDataReplacedWithSimpleLineFeeds()
{
$message = new MessageEvent("hello\rworld!\r\n");

$this->assertEquals("hello\nworld!\n", $message->data);
}

public function testConstructWithInvalidDataUtf8Throws()
{
$this->setExpectedException('InvalidArgumentException', 'Invalid $data given, must be valid UTF-8 string');
new MessageEvent("h\xFFllo!");
}

public function testConstructWithInvalidLastEventIdUtf8Throws()
{
$this->setExpectedException('InvalidArgumentException', 'Invalid $lastEventId given, must be valid UTF-8 string with no null bytes or newline characters');
new MessageEvent('hello', "h\xFFllo");
}

public function testConstructWithInvalidLastEventIdNullThrows()
{
$this->setExpectedException('InvalidArgumentException', 'Invalid $lastEventId given, must be valid UTF-8 string with no null bytes or newline characters');
new MessageEvent('hello', "h\x00llo");
}

public function testConstructWithInvalidLastEventIdCarriageReturnThrows()
{
$this->setExpectedException('InvalidArgumentException', 'Invalid $lastEventId given, must be valid UTF-8 string with no null bytes or newline characters');
new MessageEvent('hello', "hello\r");
}

public function testConstructWithInvalidLastEventIdLineFeedThrows()
{
$this->setExpectedException('InvalidArgumentException', 'Invalid $lastEventId given, must be valid UTF-8 string with no null bytes or newline characters');
new MessageEvent('hello', "hello\n");
}

public function testConstructWithInvalidTypeUtf8Throws()
{
$this->setExpectedException('InvalidArgumentException', 'Invalid $type given, must be valid UTF-8 string with no newline characters');
new MessageEvent('hello', '', "h\xFFllo");
}

public function testConstructWithInvalidTypeEmptyThrows()
{
$this->setExpectedException('InvalidArgumentException', 'Invalid $type given, must be valid UTF-8 string with no newline characters');
new MessageEvent('hello', '', '');
}

public function testConstructWithInvalidTypeCarriageReturnThrows()
{
$this->setExpectedException('InvalidArgumentException', 'Invalid $type given, must be valid UTF-8 string with no newline characters');
new MessageEvent('hello', '', "hello\r");
}

public function testConstructWithInvalidTypeLineFeedThrows()
{
$this->setExpectedException('InvalidArgumentException', 'Invalid $type given, must be valid UTF-8 string with no newline characters');
new MessageEvent('hello', '', "hello\r");
}

public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null)
{
if (method_exists($this, 'expectException')) {
// PHPUnit 5.2+
$this->expectException($exception);
if ($exceptionMessage !== '') {
$this->expectExceptionMessage($exceptionMessage);
}
if ($exceptionCode !== null) {
$this->expectExceptionCode($exceptionCode);
}
} else {
// legacy PHPUnit < 5.2
parent::setExpectedException($exception, $exceptionMessage, $exceptionCode);
}
}
}