diff --git a/README.md b/README.md index cb027228..210af1c9 100644 --- a/README.md +++ b/README.md @@ -40,33 +40,25 @@ All of the loops support these features: Here is an async HTTP server built with just the event loop. ```php - $loop = React\EventLoop\Factory::create(); - $server = stream_socket_server('tcp://127.0.0.1:8080'); stream_set_blocking($server, 0); - $loop->addReadStream($server, function ($server) use ($loop) { + React\EventLoop\addReadStream($server, function ($server) { $conn = stream_socket_accept($server); $data = "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nHi\n"; - $loop->addWriteStream($conn, function ($conn) use (&$data, $loop) { + React\EventLoop\addWriteStream($conn, function ($conn) use (&$data) { $written = fwrite($conn, $data); if ($written === strlen($data)) { fclose($conn); - $loop->removeStream($conn); + React\EventLoop\removeStream($conn); } else { $data = substr($data, $written); } }); }); - $loop->addPeriodicTimer(5, function () { + React\EventLoop\addPeriodicTimer(5, function () { $memory = memory_get_usage() / 1024; $formatted = number_format($memory, 3).'K'; echo "Current memory usage: {$formatted}\n"; }); - - $loop->run(); ``` -**Note:** The factory is just for convenience. It tries to pick the best -available implementation. Libraries `SHOULD` allow the user to inject an -instance of the loop. They `MAY` use the factory when the user did not supply -a loop. diff --git a/composer.json b/composer.json index 5001a9c8..beb5cb00 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "autoload": { "psr-4": { "React\\EventLoop\\": "src" - } + }, + "files": ["src/functions.php"] } } diff --git a/src/GlobalLoop.php b/src/GlobalLoop.php new file mode 100644 index 00000000..04c8c462 --- /dev/null +++ b/src/GlobalLoop.php @@ -0,0 +1,72 @@ +run(); + }); + + return self::$loop = self::create(); + } + + /** + * @return LoopInterface + */ + public static function create() + { + $loop = call_user_func(self::$factory); + + if (!$loop instanceof LoopInterface) { + throw new \LogicException( + sprintf( + 'The GlobalLoop factory must return an instance of LoopInterface but returned %s.', + is_object($loop) ? get_class($loop) : gettype($loop) + ) + ); + } + + return $loop; + } +} diff --git a/src/functions.php b/src/functions.php new file mode 100644 index 00000000..38f5c116 --- /dev/null +++ b/src/functions.php @@ -0,0 +1,123 @@ +addReadStream($stream, $listener); +} + +/** + * Register a listener to the global event loop to be notified when a stream is + * ready to write. + * + * @param resource $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + */ +function addWriteStream($stream, callable $listener) +{ + $loop = GlobalLoop::$loop ?: GlobalLoop::get(); + + $loop->addWriteStream($stream, $listener); +} + +/** + * Remove the read event listener from the global event loop for the given + * stream. + * + * @param resource $stream The PHP stream resource. + */ +function removeReadStream($stream) +{ + $loop = GlobalLoop::$loop ?: GlobalLoop::get(); + + $loop->removeReadStream($stream); +} + +/** + * Remove the write event listener from the global event loop for the given + * stream. + * + * @param resource $stream The PHP stream resource. + */ +function removeWriteStream($stream) +{ + $loop = GlobalLoop::$loop ?: GlobalLoop::get(); + + $loop->removeWriteStream($stream); +} + +/** + * Remove all listeners from the global event loop for the given stream. + * + * @param resource $stream The PHP stream resource. + */ +function removeStream($stream) +{ + $loop = GlobalLoop::$loop ?: GlobalLoop::get(); + + $loop->removeStream($stream); +} + +/** + * Enqueue a callback to the global event loop to be invoked once after the + * given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param int|float $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ +function addTimer($interval, callable $callback) +{ + $loop = GlobalLoop::$loop ?: GlobalLoop::get(); + + return $loop->addTimer($interval, $callback); +} + +/** + * Enqueue a callback to the global event loop to be invoked repeatedly after + * the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param int|float $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ +function addPeriodicTimer($interval, callable $callback) +{ + $loop = GlobalLoop::$loop ?: GlobalLoop::get(); + + return $loop->addPeriodicTimer($interval, $callback); +} + +/** + * Schedule a callback to be invoked on a future tick of the global event loop. + * + * Callbacks are guaranteed to be executed in the order they are enqueued. + * + * @param callable $listener The callback to invoke. + */ +function futureTick(callable $listener) +{ + $loop = GlobalLoop::$loop ?: GlobalLoop::get(); + + $loop->futureTick($listener); +} diff --git a/tests/FunctionTest.php b/tests/FunctionTest.php new file mode 100644 index 00000000..55c5f02d --- /dev/null +++ b/tests/FunctionTest.php @@ -0,0 +1,121 @@ +getMockBuilder('React\EventLoop\LoopInterface') + ->getMock(); + + self::$state = GlobalLoop::$loop; + GlobalLoop::$loop = $this->globalLoop = $globalLoop; + } + + public function tearDown() + { + $this->globalLoop = null; + + GlobalLoop::$loop = self::$state; + } + + public function createStream() + { + return fopen('php://temp', 'r+'); + } + + public function testAddReadStream() + { + $stream = $this->createStream(); + $listener = function() {}; + + $this->globalLoop + ->expects($this->once()) + ->method('addReadStream') + ->with($stream, $listener); + + EventLoop\addReadStream($stream, $listener); + } + + public function testAddWriteStream() + { + $stream = $this->createStream(); + $listener = function() {}; + + $this->globalLoop + ->expects($this->once()) + ->method('addWriteStream') + ->with($stream, $listener); + + EventLoop\addWriteStream($stream, $listener); + } + + public function testRemoveReadStream() + { + $stream = $this->createStream(); + + $this->globalLoop + ->expects($this->once()) + ->method('removeReadStream') + ->with($stream); + + EventLoop\removeReadStream($stream); + } + + public function testRemoveWriteStream() + { + $stream = $this->createStream(); + + $this->globalLoop + ->expects($this->once()) + ->method('removeWriteStream') + ->with($stream); + + EventLoop\removeWriteStream($stream); + } + + public function testRemoveStream() + { + $stream = $this->createStream(); + + $this->globalLoop + ->expects($this->once()) + ->method('removeStream') + ->with($stream); + + EventLoop\removeStream($stream); + } + + public function testAddTimer() + { + $interval = 1; + $listener = function() {}; + + $this->globalLoop + ->expects($this->once()) + ->method('addTimer') + ->with($interval, $listener); + + EventLoop\addTimer($interval, $listener); + } + + public function testAddPeriodicTimer() + { + $interval = 1; + $listener = function() {}; + + $this->globalLoop + ->expects($this->once()) + ->method('addPeriodicTimer') + ->with($interval, $listener); + + EventLoop\addPeriodicTimer($interval, $listener); + } +} diff --git a/tests/GlobalLoopTest.php b/tests/GlobalLoopTest.php new file mode 100644 index 00000000..3650d875 --- /dev/null +++ b/tests/GlobalLoopTest.php @@ -0,0 +1,80 @@ +assertNull(GlobalLoop::$loop); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', GlobalLoop::get()); + } + + public function testCreatesCustomLoopWithFactory() + { + $this->assertNull(GlobalLoop::$loop); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface') + ->getMock(); + + $factory = $this->createCallableMock(); + $factory + ->expects($this->once()) + ->method('__invoke') + ->will($this->returnValue($loop)); + + GlobalLoop::setFactory($factory); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', GlobalLoop::get()); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage The GlobalLoop factory must return an instance of LoopInterface but returned NULL. + */ + public function testThrowsExceptionWhenFactoryDoesNotReturnALoopInterface() + { + $this->assertNull(GlobalLoop::$loop); + + $factory = $this->createCallableMock(); + $factory + ->expects($this->once()) + ->method('__invoke'); + + GlobalLoop::setFactory($factory); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', GlobalLoop::get()); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage Setting a factory after the global loop has been created is not allowed. + */ + public function testThrowsExceptionWhenSettingAFactoryAfterLoopIsCreated() + { + $this->assertNull(GlobalLoop::$loop); + + GlobalLoop::get(); + + $this->assertNotNull(GlobalLoop::$loop); + + GlobalLoop::setFactory($this->expectCallableNever()); + } +}