Skip to content

Commit 12d714f

Browse files
committed
Implement ExtEvLoop.
ExtEvLoop implements event loop based on PECL ev extension.
1 parent e295575 commit 12d714f

File tree

7 files changed

+312
-0
lines changed

7 files changed

+312
-0
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,16 @@ It supports the same backends as libevent.
201201

202202
This loop is known to work with PHP 5.4 through PHP 7+.
203203

204+
#### ExtEvLoop
205+
206+
An `ext-ev` based event loop.
207+
208+
This loop uses the [`ev` PECL extension](https://pecl.php.net/package/ev), that
209+
provides an interface to `libev` library.
210+
211+
This loop is known to work with PHP 5.4 through PHP 7+.
212+
213+
204214
#### ExtLibeventLoop
205215

206216
An `ext-libevent` based event loop.

src/ExtEvLoop.php

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
<?php
2+
3+
namespace React\EventLoop;
4+
5+
use Ev;
6+
use EvIo;
7+
use EvLoop;
8+
use React\EventLoop\Tick\FutureTickQueue;
9+
use React\EventLoop\Timer\Timer;
10+
use SplObjectStorage;
11+
12+
/**
13+
* An `ext-ev` based event loop.
14+
*
15+
* This loop uses the [`ev` PECL extension](https://pecl.php.net/package/ev),
16+
* that provides an interface to `libev` library.
17+
*
18+
* This loop is known to work with PHP 5.4 through PHP 7+.
19+
*
20+
* @see http://php.net/manual/en/book.ev.php
21+
* @see https://bitbucket.org/osmanov/pecl-ev/overview
22+
*/
23+
class ExtEvLoop implements LoopInterface
24+
{
25+
/**
26+
* @var EvLoop
27+
*/
28+
private $loop;
29+
30+
/**
31+
* @var FutureTickQueue
32+
*/
33+
private $futureTickQueue;
34+
35+
/**
36+
* @var SplObjectStorage
37+
*/
38+
private $timers;
39+
40+
/**
41+
* @var EvIo[]
42+
*/
43+
private $readStreams = [];
44+
45+
/**
46+
* @var EvIo[]
47+
*/
48+
private $writeStreams = [];
49+
50+
/**
51+
* @var bool
52+
*/
53+
private $running;
54+
55+
/**
56+
* @var SignalsHandler
57+
*/
58+
private $signals;
59+
60+
/**
61+
* @var \EvSignal[]
62+
*/
63+
private $signalEvents = [];
64+
65+
public function __construct()
66+
{
67+
$this->loop = new EvLoop();
68+
$this->futureTickQueue = new FutureTickQueue();
69+
$this->timers = new SplObjectStorage();
70+
$this->signals = new SignalsHandler(
71+
$this,
72+
function ($signal) {
73+
$this->signalEvents[$signal] = $this->loop->signal($signal, $f = function () use ($signal, &$f) {
74+
$this->signals->call($signal);
75+
// Ensure there are two copies of the callable around until it has been executed.
76+
// For more information see: https://bugs.php.net/bug.php?id=62452
77+
// Only an issue for PHP 5, this hack can be removed once PHP 5 support has been dropped.
78+
$g = $f;
79+
$f = $g;
80+
});
81+
},
82+
function ($signal) {
83+
if ($this->signals->count($signal) === 0) {
84+
$this->signalEvents[$signal]->stop();
85+
unset($this->signalEvents[$signal]);
86+
}
87+
}
88+
);
89+
}
90+
91+
public function addReadStream($stream, $listener)
92+
{
93+
$key = (int)$stream;
94+
95+
if (isset($this->readStreams[$key])) {
96+
return;
97+
}
98+
99+
$callback = $this->getStreamListenerClosure($stream, $listener);
100+
$event = $this->loop->io($stream, Ev::READ, $callback);
101+
$this->readStreams[$key] = $event;
102+
}
103+
104+
/**
105+
* @param resource $stream
106+
* @param callable $listener
107+
*
108+
* @return \Closure
109+
*/
110+
private function getStreamListenerClosure($stream, $listener)
111+
{
112+
return function () use ($stream, $listener) {
113+
call_user_func($listener, $stream);
114+
};
115+
}
116+
117+
public function addWriteStream($stream, $listener)
118+
{
119+
$key = (int)$stream;
120+
121+
if (isset($this->writeStreams[$key])) {
122+
return;
123+
}
124+
125+
$callback = $this->getStreamListenerClosure($stream, $listener);
126+
$event = $this->loop->io($stream, Ev::WRITE, $callback);
127+
$this->writeStreams[$key] = $event;
128+
}
129+
130+
public function removeReadStream($stream)
131+
{
132+
$key = (int)$stream;
133+
134+
if (!isset($this->readStreams[$key])) {
135+
return;
136+
}
137+
138+
$this->readStreams[$key]->stop();
139+
unset($this->readStreams[$key]);
140+
}
141+
142+
public function removeWriteStream($stream)
143+
{
144+
$key = (int)$stream;
145+
146+
if (!isset($this->writeStreams[$key])) {
147+
return;
148+
}
149+
150+
$this->writeStreams[$key]->stop();
151+
unset($this->writeStreams[$key]);
152+
}
153+
154+
public function addTimer($interval, $callback)
155+
{
156+
$timer = new Timer($interval, $callback, false);
157+
158+
$callback = function () use ($timer) {
159+
call_user_func($timer->getCallback(), $timer);
160+
161+
if ($this->isTimerActive($timer)) {
162+
$this->cancelTimer($timer);
163+
}
164+
};
165+
166+
$event = $this->loop->timer($timer->getInterval(), 0.0, $callback);
167+
$this->timers->attach($timer, $event);
168+
169+
return $timer;
170+
}
171+
172+
public function addPeriodicTimer($interval, $callback)
173+
{
174+
$timer = new Timer($interval, $callback, true);
175+
176+
$callback = function () use ($timer) {
177+
call_user_func($timer->getCallback(), $timer);
178+
};
179+
180+
$event = $this->loop->timer($interval, $interval, $callback);
181+
$this->timers->attach($timer, $event);
182+
183+
return $timer;
184+
}
185+
186+
public function cancelTimer(TimerInterface $timer)
187+
{
188+
if (!isset($this->timers[$timer])) {
189+
return;
190+
}
191+
192+
$event = $this->timers[$timer];
193+
$event->stop();
194+
$this->timers->detach($timer);
195+
}
196+
197+
public function isTimerActive(TimerInterface $timer)
198+
{
199+
return $this->timers->contains($timer);
200+
}
201+
202+
public function futureTick($listener)
203+
{
204+
$this->futureTickQueue->add($listener);
205+
}
206+
207+
public function run()
208+
{
209+
$this->running = true;
210+
211+
while ($this->running) {
212+
$this->futureTickQueue->tick();
213+
214+
$hasPendingCallbacks = !$this->futureTickQueue->isEmpty();
215+
$wasJustStopped = !$this->running;
216+
$nothingLeftToDo = !$this->readStreams && !$this->writeStreams && !$this->timers->count();
217+
218+
$flags = Ev::RUN_ONCE;
219+
if ($wasJustStopped || $hasPendingCallbacks) {
220+
$flags |= Ev::RUN_NOWAIT;
221+
} elseif ($nothingLeftToDo) {
222+
break;
223+
}
224+
225+
$this->loop->run($flags);
226+
}
227+
}
228+
229+
public function stop()
230+
{
231+
$this->running = false;
232+
}
233+
234+
public function __destruct()
235+
{
236+
/** @var TimerInterface $timer */
237+
foreach ($this->timers as $timer) {
238+
$this->cancelTimer($timer);
239+
}
240+
241+
foreach ($this->readStreams as $key => $stream) {
242+
$this->removeReadStream($key);
243+
}
244+
245+
foreach ($this->writeStreams as $key => $stream) {
246+
$this->removeWriteStream($key);
247+
}
248+
}
249+
250+
public function addSignal($signal, $listener)
251+
{
252+
$this->signals->add($signal, $listener);
253+
}
254+
255+
public function removeSignal($signal, $listener)
256+
{
257+
$this->signals->remove($signal, $listener);
258+
}
259+
}

src/Factory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public static function create()
2626
// @codeCoverageIgnoreStart
2727
if (class_exists('libev\EventLoop', false)) {
2828
return new ExtLibevLoop();
29+
} elseif (class_exists('EvLoop', false)) {
30+
return new ExtEvLoop();
2931
} elseif (class_exists('EventBase', false)) {
3032
return new ExtEventLoop();
3133
} elseif (function_exists('event_base_new') && PHP_VERSION_ID < 70000) {

tests/ExtEvLoopTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace React\Tests\EventLoop;
4+
5+
use React\EventLoop\ExtEvLoop;
6+
7+
class ExtEvLoopTest extends AbstractLoopTest
8+
{
9+
public function createLoop()
10+
{
11+
if (!class_exists('EvLoop')) {
12+
$this->markTestSkipped('ExtEvLoop tests skipped because ext-ev extension is not installed.');
13+
}
14+
15+
return new ExtEvLoop();
16+
}
17+
}

tests/Timer/AbstractTimerTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
namespace React\Tests\EventLoop\Timer;
44

5+
use React\EventLoop\LoopInterface;
56
use React\Tests\EventLoop\TestCase;
67

78
abstract class AbstractTimerTest extends TestCase
89
{
10+
/**
11+
* @return LoopInterface
12+
*/
913
abstract public function createLoop();
1014

1115
public function testAddTimerReturnsNonPeriodicTimerInstance()

tests/Timer/ExtEvTimerTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace React\Tests\EventLoop\Timer;
4+
5+
use React\EventLoop\ExtEvLoop;
6+
7+
class ExtEvTimerTest extends AbstractTimerTest
8+
{
9+
public function createLoop()
10+
{
11+
if (!class_exists('EvLoop')) {
12+
$this->markTestSkipped('ExtEvLoop tests skipped because ext-ev extension is not installed.');
13+
}
14+
15+
return new ExtEvLoop();
16+
}
17+
}

travis-init.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ if [[ "$TRAVIS_PHP_VERSION" != "hhvm" &&
1010
echo "yes" | pecl install event
1111
fi
1212

13+
# install 'ev' PHP extension
14+
echo "yes" | pecl install ev
15+
1316
# install 'libevent' PHP extension (does not support php 7)
1417
if [[ "$TRAVIS_PHP_VERSION" != "7.0" &&
1518
"$TRAVIS_PHP_VERSION" != "7.1" &&

0 commit comments

Comments
 (0)