diff --git a/README.md b/README.md index ef5c5ce6..6b345265 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ single [`run()`](#run) call that is controlled by the user. * [Usage](#usage) * [Loop](#loop) * [Loop methods](#loop-methods) + * [Loop autorun](#loop-autorun) * [get()](#get) * [~~Factory~~](#factory) * [~~create()~~](#create) @@ -76,8 +77,6 @@ Loop::addPeriodicTimer(5, function () { $formatted = number_format($memory, 3).'K'; echo "Current memory usage: {$formatted}\n"; }); - -Loop::run(); ``` See also the [examples](examples). @@ -98,8 +97,6 @@ Loop::addTimer(1.0, function () use ($timer) { Loop::cancelTimer($timer); echo 'Done' . PHP_EOL; }); - -Loop::run(); ``` As an alternative, you can also explicitly create an event loop instance at the @@ -127,12 +124,13 @@ In both cases, the program would perform the exact same steps. 1. The event loop instance is created at the beginning of the program. This is implicitly done the first time you call the [`Loop` class](#loop) or explicitly when using the deprecated [`Factory::create() method`](#create) - (or manually instantiating any of the [loop implementation](#loop-implementations)). + (or manually instantiating any of the [loop implementations](#loop-implementations)). 2. The event loop is used directly or passed as an instance to library and application code. In this example, a periodic timer is registered with the event loop which simply outputs `Tick` every fraction of a second until another timer stops the periodic timer after a second. -3. The event loop is run at the end of the program with a single [`run()`](#run) +3. The event loop is run at the end of the program. This is automatically done + when using [`Loop` class](#loop) or explicitly with a single [`run()`](#run) call at the end of the program. As of `v1.2.0`, we highly recommend using the [`Loop` class](#loop). @@ -176,8 +174,6 @@ Loop::addTimer(1.0, function () use ($timer) { Loop::cancelTimer($timer); echo 'Done' . PHP_EOL; }); - -Loop::run(); ``` On the other hand, if you're familiar with object-oriented programming (OOP) and @@ -208,14 +204,50 @@ class Greeter $greeter = new Greeter(Loop::get()); $greeter->greet('Alice'); $greeter->greet('Bob'); - -Loop::run(); ``` Each static method call will be forwarded as-is to the underlying event loop instance by using the [`Loop::get()`](#get) call internally. See [`LoopInterface`](#loopinterface) for more details about available methods. +#### Loop autorun + +When using the `Loop` class, it will automatically execute the loop at the end of +the program. This means the following example will schedule a timer and will +automatically execute the program until the timer event fires: + +```php +use React\EventLoop\Loop; + +Loop::addTimer(1.0, function () { + echo 'Hello' . PHP_EOL; +}); +``` + +As of `v1.2.0`, we highly recommend using the `Loop` class this way and omitting any +explicit [`run()`](#run) calls. For BC reasons, the explicit [`run()`](#run) +method is still valid and may still be useful in some applications, especially +for a transition period towards the more concise style. + +If you don't want the `Loop` to run automatically, you can either explicitly +[`run()`](#run) or [`stop()`](#stop) it. This can be useful if you're using +a global exception handler like this: + +```php +use React\EventLoop\Loop; + +Loop::addTimer(10.0, function () { + echo 'Never happens'; +}); + +set_exception_handler(function (Throwable $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; + Loop::stop(); +}); + +throw new RuntimeException('Demo'); +``` + #### get() The `get(): LoopInterface` method can be used to @@ -262,8 +294,6 @@ class Greeter $greeter = new Greeter(Loop::get()); $greeter->greet('Alice'); $greeter->greet('Bob'); - -Loop::run(); ``` See [`LoopInterface`](#loopinterface) for more details about available methods. diff --git a/examples/01-timers.php b/examples/01-timers.php index a7bf3945..5f263b3e 100644 --- a/examples/01-timers.php +++ b/examples/01-timers.php @@ -11,5 +11,3 @@ Loop::addTimer(0.3, function () { echo 'hello '; }); - -Loop::run(); diff --git a/examples/02-periodic.php b/examples/02-periodic.php index 4413870d..68533bd3 100644 --- a/examples/02-periodic.php +++ b/examples/02-periodic.php @@ -12,5 +12,3 @@ Loop::cancelTimer($timer); echo 'Done' . PHP_EOL; }); - -Loop::run(); diff --git a/examples/03-ticks.php b/examples/03-ticks.php index 4b2077da..e32e67af 100644 --- a/examples/03-ticks.php +++ b/examples/03-ticks.php @@ -11,5 +11,3 @@ echo 'c'; }); echo 'a'; - -Loop::run(); diff --git a/examples/04-signals.php b/examples/04-signals.php index ceca3521..e841311b 100644 --- a/examples/04-signals.php +++ b/examples/04-signals.php @@ -15,5 +15,3 @@ }); echo 'Listening for SIGINT. Use "kill -SIGINT ' . getmypid() . '" or CTRL+C' . PHP_EOL; - -Loop::run(); diff --git a/examples/11-consume-stdin.php b/examples/11-consume-stdin.php index f567d84a..dfcb220d 100644 --- a/examples/11-consume-stdin.php +++ b/examples/11-consume-stdin.php @@ -24,5 +24,3 @@ echo strlen($chunk) . ' bytes' . PHP_EOL; }); - -Loop::run(); diff --git a/examples/12-generate-yes.php b/examples/12-generate-yes.php index 4424b8ec..a57e8d6e 100644 --- a/examples/12-generate-yes.php +++ b/examples/12-generate-yes.php @@ -37,5 +37,3 @@ $data = substr($data, $r) . substr($data, 0, $r); } }); - -Loop::run(); diff --git a/examples/13-http-client-blocking.php b/examples/13-http-client-blocking.php index efd8cc86..f0562c90 100644 --- a/examples/13-http-client-blocking.php +++ b/examples/13-http-client-blocking.php @@ -29,5 +29,3 @@ echo $chunk; }); - -Loop::run(); diff --git a/examples/14-http-client-async.php b/examples/14-http-client-async.php index ceed3ec7..074a0eac 100644 --- a/examples/14-http-client-async.php +++ b/examples/14-http-client-async.php @@ -58,5 +58,3 @@ echo $chunk; }); }); - -Loop::run(); diff --git a/examples/21-http-server.php b/examples/21-http-server.php index e000eb51..61529240 100644 --- a/examples/21-http-server.php +++ b/examples/21-http-server.php @@ -32,5 +32,3 @@ $formatted = number_format($memory, 3).'K'; echo "Current memory usage: {$formatted}\n"; }); - -Loop::run(); diff --git a/examples/91-benchmark-ticks.php b/examples/91-benchmark-ticks.php index 452abbac..e3dc2b1c 100644 --- a/examples/91-benchmark-ticks.php +++ b/examples/91-benchmark-ticks.php @@ -9,5 +9,3 @@ for ($i = 0; $i < $n; ++$i) { Loop::futureTick(function () { }); } - -Loop::run(); diff --git a/examples/92-benchmark-timers.php b/examples/92-benchmark-timers.php index da381f16..dd42ec77 100644 --- a/examples/92-benchmark-timers.php +++ b/examples/92-benchmark-timers.php @@ -9,5 +9,3 @@ for ($i = 0; $i < $n; ++$i) { Loop::addTimer(0, function () { }); } - -Loop::run(); diff --git a/examples/93-benchmark-ticks-delay.php b/examples/93-benchmark-ticks-delay.php index ac5094f3..1976124f 100644 --- a/examples/93-benchmark-ticks-delay.php +++ b/examples/93-benchmark-ticks-delay.php @@ -16,5 +16,3 @@ }; $tick(); - -Loop::run(); diff --git a/examples/94-benchmark-timers-delay.php b/examples/94-benchmark-timers-delay.php index eb4fc5cb..dfe6c8c0 100644 --- a/examples/94-benchmark-timers-delay.php +++ b/examples/94-benchmark-timers-delay.php @@ -16,5 +16,3 @@ }; $tick(); - -Loop::run(); diff --git a/src/Loop.php b/src/Loop.php index fed27cba..7f1d962c 100644 --- a/src/Loop.php +++ b/src/Loop.php @@ -12,6 +12,8 @@ final class Loop */ private static $instance; + /** @var bool */ + private static $stopped = false; /** * Returns the event loop. @@ -31,7 +33,29 @@ public static function get() return self::$instance; } - self::$instance = Factory::create(); + self::$instance = $loop = Factory::create(); + + // Automatically run loop at end of program, unless already started or stopped explicitly. + // This is tested using child processes, so coverage is actually 100%, see BinTest. + // @codeCoverageIgnoreStart + $hasRun = false; + $loop->futureTick(function () use (&$hasRun) { + $hasRun = true; + }); + + $stopped =& self::$stopped; + register_shutdown_function(function () use ($loop, &$hasRun, &$stopped) { + // Don't run if we're coming from a fatal error (uncaught exception). + $error = error_get_last(); + if ((isset($error['type']) ? $error['type'] : 0) & (E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR)) { + return; + } + + if (!$hasRun && !$stopped) { + $loop->run(); + } + }); + // @codeCoverageIgnoreEnd return self::$instance; } @@ -195,6 +219,7 @@ public static function run() */ public static function stop() { + self::$stopped = true; self::get()->stop(); } } diff --git a/tests/BinTest.php b/tests/BinTest.php new file mode 100644 index 00000000..6f8231b8 --- /dev/null +++ b/tests/BinTest.php @@ -0,0 +1,75 @@ +markTestSkipped('Tests not supported on legacy PHP 5.3 or HHVM'); + } + + chdir(__DIR__ . '/bin/'); + } + + public function testExecuteExampleWithoutLoopRunRunsLoopAndExecutesTicks() + { + $output = exec(escapeshellarg(PHP_BINARY) . ' 01-ticks-loop-class.php'); + + $this->assertEquals('abc', $output); + } + + public function testExecuteExampleWithExplicitLoopRunRunsLoopAndExecutesTicks() + { + $output = exec(escapeshellarg(PHP_BINARY) . ' 02-ticks-loop-instance.php'); + + $this->assertEquals('abc', $output); + } + + public function testExecuteExampleWithExplicitLoopRunAndStopRunsLoopAndExecutesTicksUntilStopped() + { + $output = exec(escapeshellarg(PHP_BINARY) . ' 03-ticks-loop-stop.php'); + + $this->assertEquals('abc', $output); + } + + public function testExecuteExampleWithUncaughtExceptionShouldNotRunLoop() + { + $time = microtime(true); + exec(escapeshellarg(PHP_BINARY) . ' 11-uncaught.php 2>/dev/null'); + $time = microtime(true) - $time; + + $this->assertLessThan(1.0, $time); + } + + public function testExecuteExampleWithUndefinedVariableShouldNotRunLoop() + { + $time = microtime(true); + exec(escapeshellarg(PHP_BINARY) . ' 12-undefined.php 2>/dev/null'); + $time = microtime(true) - $time; + + $this->assertLessThan(1.0, $time); + } + + public function testExecuteExampleWithExplicitStopShouldNotRunLoop() + { + $time = microtime(true); + exec(escapeshellarg(PHP_BINARY) . ' 21-stop.php 2>/dev/null'); + $time = microtime(true) - $time; + + $this->assertLessThan(1.0, $time); + } + + public function testExecuteExampleWithExplicitStopInExceptionHandlerShouldNotRunLoop() + { + $time = microtime(true); + exec(escapeshellarg(PHP_BINARY) . ' 22-uncaught-stop.php 2>/dev/null'); + $time = microtime(true) - $time; + + $this->assertLessThan(1.0, $time); + } +} diff --git a/tests/bin/01-ticks-loop-class.php b/tests/bin/01-ticks-loop-class.php new file mode 100644 index 00000000..f4fcedf1 --- /dev/null +++ b/tests/bin/01-ticks-loop-class.php @@ -0,0 +1,13 @@ +futureTick(function () { + echo 'b'; +}); + +$loop->futureTick(function () { + echo 'c'; +}); + +echo 'a'; + +$loop->run(); diff --git a/tests/bin/03-ticks-loop-stop.php b/tests/bin/03-ticks-loop-stop.php new file mode 100644 index 00000000..d8b65946 --- /dev/null +++ b/tests/bin/03-ticks-loop-stop.php @@ -0,0 +1,23 @@ +futureTick(function () use ($loop) { + echo 'b'; + + $loop->stop(); + + $loop->futureTick(function () { + echo 'never'; + }); +}); + +echo 'a'; + +$loop->run(); + +echo 'c'; diff --git a/tests/bin/11-uncaught.php b/tests/bin/11-uncaught.php new file mode 100644 index 00000000..0655698b --- /dev/null +++ b/tests/bin/11-uncaught.php @@ -0,0 +1,11 @@ +addTimer(10.0, function () { + echo 'never'; +}); + +$undefined->foo('bar'); diff --git a/tests/bin/21-stop.php b/tests/bin/21-stop.php new file mode 100644 index 00000000..038d9223 --- /dev/null +++ b/tests/bin/21-stop.php @@ -0,0 +1,11 @@ +