diff --git a/README.md b/README.md index 4e4ee3d0..f9fb9e76 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,10 @@ In addition to the interface there are the following implementations provided: ([github](https://github.com/m4rw3r/php-libev)). It supports the same backends as libevent. +* `PeclEvLoop`: This uses the `libev` pecl extension that is documented on + ([php.net](http://php.net/manual/en/book.ev.php)). See + ([bitbucket](https://bitbucket.org/osmanov/pecl-ev/overview)) for source. + * `ExtEventLoop`: This uses the `event` pecl extension. It supports the same backends as libevent. diff --git a/src/Factory.php b/src/Factory.php index 9a481e35..5e49f832 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -11,6 +11,8 @@ public static function create() return new LibEventLoop(); } elseif (class_exists('libev\EventLoop', false)) { return new LibEvLoop; + } elseif (class_exists('EvLoop', false)) { + return new PeclEvLoop; } elseif (class_exists('EventBase', false)) { return new ExtEventLoop; } diff --git a/src/PeclEvLoop.php b/src/PeclEvLoop.php new file mode 100644 index 00000000..d1d57102 --- /dev/null +++ b/src/PeclEvLoop.php @@ -0,0 +1,195 @@ +loop = new EvLoop(); + $this->futureTickQueue = new FutureTickQueue($this); + $this->timers = new SplObjectStorage(); + } + + public function addReadStream($stream, callable $listener) + { + $key = (int) $stream; + + if (isset($this->readStreams[$key])) { + return; + } + + $callback = $this->getStreamListenerClosure($stream, $listener); + $event = $this->loop->io($stream, Ev::READ, $callback); + $this->readStreams[$key] = $event; + } + + /** + * @param resource $stream + * @param callable $listener + * + * @return \Closure + */ + private function getStreamListenerClosure($stream, callable $listener) { + return function () use ($stream, $listener) { + call_user_func($listener, $stream, $this); + }; + } + + public function addWriteStream($stream, callable $listener) + { + $key = (int) $stream; + + if (isset($this->writeStreams[$key])) { + return; + } + + $callback = $this->getStreamListenerClosure($stream, $listener); + $event = $this->loop->io($stream, Ev::WRITE, $callback); + $this->writeStreams[$key] = $event; + } + + public function removeReadStream($stream) + { + $key = (int) $stream; + + if (!isset($this->readStreams[$key])) { + return; + } + + $this->readStreams[$key]->stop(); + unset($this->readStreams[$key]); + } + + public function removeWriteStream($stream) + { + $key = (int) $stream; + + if (!isset($this->writeStreams[$key])) { + return; + } + + $this->writeStreams[$key]->stop(); + unset($this->writeStreams[$key]); + } + + public function removeStream($stream) + { + $this->removeReadStream($stream); + $this->removeWriteStream($stream); + } + + public function addTimer($interval, callable $callback) + { + $timer = new Timer($this, $interval, $callback, false); + + $callback = function () use ($timer) { + call_user_func($timer->getCallback(), $timer); + + if ($this->isTimerActive($timer)) { + $this->cancelTimer($timer); + } + }; + + $event = $this->loop->timer($timer->getInterval(), 0.0, $callback); + $this->timers->attach($timer, $event); + + return $timer; + } + + public function addPeriodicTimer($interval, callable $callback) + { + $timer = new Timer($this, $interval, $callback, true); + + $callback = function () use ($timer) { + call_user_func($timer->getCallback(), $timer); + }; + + //reschedule callback should be NULL to utilize $offset and $interval params + $event = $this->loop->periodic($interval, $interval, NULL, $callback); + $this->timers->attach($timer, $event); + + return $timer; + } + + public function cancelTimer(TimerInterface $timer) + { + if (!isset($this->timers[$timer])) { + return; + } + + $event = $this->timers[$timer]; + $event->stop(); + $this->timers->detach($timer); + } + + public function isTimerActive(TimerInterface $timer) + { + return $this->timers->contains($timer); + } + + public function futureTick(callable $listener) + { + $this->futureTickQueue->add($listener); + } + + public function run() + { + $this->running = true; + + while ($this->running) { + $this->futureTickQueue->tick(); + + $hasPendingCallbacks = !$this->futureTickQueue->isEmpty(); + $wasJustStopped = !$this->running; + $nothingLeftToDo = !$this->readStreams && !$this->writeStreams && !$this->timers->count(); + + $flags = Ev::RUN_ONCE; + if ($wasJustStopped || $hasPendingCallbacks) { + $flags |= Ev::RUN_NOWAIT; + } elseif ($nothingLeftToDo) { + break; + } + + $this->loop->run($flags); + } + } + + public function stop() + { + $this->running = false; + } + + public function __destruct() + { + /** @var TimerInterface $timer */ + foreach($this->timers as $timer) { + $this->cancelTimer($timer); + } + + foreach($this->readStreams as $key => $stream) { + $this->removeReadStream($key); + } + + foreach($this->writeStreams as $key => $stream) { + $this->removeWriteStream($key); + } + } +} diff --git a/tests/PeclEvLoopTest.php b/tests/PeclEvLoopTest.php new file mode 100644 index 00000000..c00deb65 --- /dev/null +++ b/tests/PeclEvLoopTest.php @@ -0,0 +1,17 @@ +markTestSkipped('pecl-ev tests skipped because ext-ev is not installed.'); + } + + return new PeclEvLoop(); + } +} \ No newline at end of file diff --git a/tests/Timer/AbstractTimerTest.php b/tests/Timer/AbstractTimerTest.php index e930ad37..389f0fb8 100644 --- a/tests/Timer/AbstractTimerTest.php +++ b/tests/Timer/AbstractTimerTest.php @@ -2,10 +2,15 @@ namespace React\Tests\EventLoop\Timer; +use React\EventLoop\LoopInterface; +use React\EventLoop\Timer\Timer; use React\Tests\EventLoop\TestCase; abstract class AbstractTimerTest extends TestCase { + /** + * @return LoopInterface + */ abstract public function createLoop(); public function testAddTimer() @@ -94,4 +99,13 @@ public function testMinimumIntervalOneMicrosecond() $this->assertEquals(0.000001, $timer->getInterval()); } + + public function testCancelNonexistentTimer() + { + $loop = $this->createLoop(); + + $timer = new Timer($loop, 1, function(){}); + + $loop->cancelTimer($timer); + } } diff --git a/tests/Timer/PeclEvLoopTimerTest.php b/tests/Timer/PeclEvLoopTimerTest.php new file mode 100644 index 00000000..0a4b3e32 --- /dev/null +++ b/tests/Timer/PeclEvLoopTimerTest.php @@ -0,0 +1,17 @@ +markTestSkipped('pecl-ev tests skipped because ext-ev is not installed.'); + } + + return new PeclEvLoop(); + } +} \ No newline at end of file diff --git a/travis-init.sh b/travis-init.sh index 87456016..955e8847 100755 --- a/travis-init.sh +++ b/travis-init.sh @@ -8,6 +8,9 @@ if [[ "$TRAVIS_PHP_VERSION" != "hhvm" && # install 'event' PHP extension echo "yes" | pecl install event + # install 'ev' PHP extension + echo "yes" | pecl install ev + # install 'libevent' PHP extension (does not support php 7) if [[ "$TRAVIS_PHP_VERSION" != "7.0" && "$TRAVIS_PHP_VERSION" != "7.1" ]]; then