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()); + } +}
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: