Skip to content

Commit 78ff76a

Browse files
[Php86] Polyfill the Io\Poll API
Tracks https://wiki.php.net/rfc/poll_api for PHP 8.6. Requires PHP >= 8.1 (enums); only the Poll backend is available since the polyfill is backed by stream_select().
1 parent 21e3e69 commit 78ff76a

21 files changed

Lines changed: 1246 additions & 3 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ Polyfills are provided for:
9292
- the `ARRAY_FILTER_USE_VALUE` constant introduced in PHP 8.6;
9393
- the `SortDirection` enum introduced in PHP 8.6;
9494
- the `grapheme_strrev` function introduced in PHP 8.6;
95+
- the `Io\Poll` API and `StreamPollHandle` introduced in PHP 8.6 (requires PHP >= 8.1; only the `Poll` backend is available);
9596

9697
It is strongly recommended to upgrade your PHP version and/or install the missing
9798
extensions whenever possible. This polyfill should be used only when there is no

src/Php86/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ This component provides features added to PHP 8.6 core:
66
- [`clamp`](https://wiki.php.net/rfc/clamp_v2)
77
- `ARRAY_FILTER_USE_VALUE` constant
88
- [`SortDirection`](https://wiki.php.net/rfc/sort_direction_enum)
9+
- `grapheme_strrev`
10+
- [`Io\Poll` API and `StreamPollHandle`](https://wiki.php.net/rfc/poll_api) (requires PHP >= 8.1; backed by `stream_select()`, so only the `Poll` backend is available)
911

1012
More information can be found in the
1113
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Io;
13+
14+
if (\PHP_VERSION_ID < 80600 && \PHP_VERSION_ID >= 80100) {
15+
class IoException extends \Exception
16+
{
17+
}
18+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Io\Poll;
13+
14+
if (\PHP_VERSION_ID < 80600 && \PHP_VERSION_ID >= 80100) {
15+
enum Backend
16+
{
17+
case Auto;
18+
case Poll;
19+
case Epoll;
20+
case Kqueue;
21+
case EventPorts;
22+
case WSAPoll;
23+
24+
public function isAvailable(): bool
25+
{
26+
return match ($this) {
27+
self::Auto, self::Poll => true,
28+
default => false,
29+
};
30+
}
31+
32+
public function supportsEdgeTriggering(): bool
33+
{
34+
return false;
35+
}
36+
37+
public static function getAvailableBackends(): array
38+
{
39+
return [self::Poll];
40+
}
41+
}
42+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Io\Poll;
13+
14+
if (\PHP_VERSION_ID < 80600 && \PHP_VERSION_ID >= 80100) {
15+
class BackendUnavailableException extends PollException
16+
{
17+
}
18+
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Io\Poll;
13+
14+
if (\PHP_VERSION_ID < 80600 && \PHP_VERSION_ID >= 80100) {
15+
final class Context
16+
{
17+
private Backend $backend;
18+
19+
/**
20+
* @var \SplObjectStorage<Watcher, null>
21+
*/
22+
private \SplObjectStorage $watchers;
23+
24+
public function __construct(Backend $backend = Backend::Auto)
25+
{
26+
if (!$backend->isAvailable()) {
27+
throw new BackendUnavailableException(\sprintf('Backend %s not available', $backend->name));
28+
}
29+
30+
$this->backend = Backend::Auto === $backend ? Backend::Poll : $backend;
31+
$this->watchers = new \SplObjectStorage();
32+
}
33+
34+
public function getBackend(): Backend
35+
{
36+
return $this->backend;
37+
}
38+
39+
public function add(Handle $handle, array $events, mixed $data = null): Watcher
40+
{
41+
if (!$handle instanceof \StreamPollHandle) {
42+
throw new InvalidHandleException(\sprintf('Handle of type "%s" is not supported by the polyfill; only %s is.', \get_class($handle), \StreamPollHandle::class));
43+
}
44+
45+
if (!$handle->isValid()) {
46+
throw new InvalidHandleException('Handle is no longer valid.');
47+
}
48+
49+
foreach ($events as $event) {
50+
if (!$event instanceof Event) {
51+
throw new \TypeError(\sprintf('%s(): Argument #2 ($events) must be array of Event enums', __METHOD__));
52+
}
53+
}
54+
55+
if (!$events) {
56+
throw new \TypeError(\sprintf('%s(): Argument #2 ($events) must be array of Event enums', __METHOD__));
57+
}
58+
59+
$stream = $handle->getStream();
60+
foreach ($this->watchers as $existing) {
61+
if ($existing->getHandle() === $handle
62+
|| ($existing->getHandle() instanceof \StreamPollHandle && $existing->getHandle()->getStream() === $stream)
63+
) {
64+
throw new HandleAlreadyWatchedException('Handle already added');
65+
}
66+
}
67+
68+
static $create;
69+
$create ??= \Closure::bind(
70+
static fn (Context $ctx, Handle $h, array $e, mixed $d): Watcher => new Watcher($ctx, $h, $e, $d),
71+
null,
72+
Watcher::class,
73+
);
74+
$watcher = $create($this, $handle, $events, $data);
75+
$this->watchers[$watcher] = null;
76+
77+
return $watcher;
78+
}
79+
80+
public function wait(?int $timeoutSeconds = null, int $timeoutMicroseconds = 0, ?int $maxEvents = null): array
81+
{
82+
if (null !== $timeoutSeconds) {
83+
if ($timeoutSeconds < 0) {
84+
throw new \ValueError(\sprintf('%s(): Argument #1 ($timeoutSeconds) must be greater than or equal to 0', __METHOD__));
85+
}
86+
if ($timeoutMicroseconds < 0) {
87+
throw new \ValueError(\sprintf('%s(): Argument #2 ($timeoutMicroseconds) must be greater than or equal to 0', __METHOD__));
88+
}
89+
}
90+
91+
if (null !== $maxEvents && $maxEvents <= 0) {
92+
throw new \ValueError(\sprintf('%s(): Argument #3 ($maxEvents) must be greater than 0', __METHOD__));
93+
}
94+
95+
static $setTriggered, $clearContext;
96+
$setTriggered ??= \Closure::bind(
97+
static function (Watcher $w, array $events): void { $w->triggeredEvents = $events; },
98+
null,
99+
Watcher::class,
100+
);
101+
$clearContext ??= \Closure::bind(
102+
static function (Watcher $w): void { $w->context = null; },
103+
null,
104+
Watcher::class,
105+
);
106+
107+
$read = $write = $except = [];
108+
$byId = [];
109+
110+
foreach ($this->watchers as $watcher) {
111+
$handle = $watcher->getHandle();
112+
if (!$handle instanceof \StreamPollHandle || !$handle->isValid()) {
113+
continue;
114+
}
115+
116+
$stream = $handle->getStream();
117+
$id = get_resource_id($stream);
118+
$byId[$id] = $watcher;
119+
120+
$read[$id] = $stream;
121+
$except[$id] = $stream;
122+
123+
foreach ($watcher->getWatchedEvents() as $event) {
124+
if (Event::Write === $event) {
125+
$write[$id] = $stream;
126+
break;
127+
}
128+
}
129+
}
130+
131+
if (!$byId) {
132+
if (null !== $timeoutSeconds) {
133+
$micros = $timeoutSeconds * 1_000_000 + $timeoutMicroseconds;
134+
if ($micros > 0) {
135+
usleep($micros);
136+
}
137+
}
138+
139+
return [];
140+
}
141+
142+
$readCopy = $read;
143+
$writeCopy = $write;
144+
$exceptCopy = $except;
145+
146+
$result = @stream_select($readCopy, $writeCopy, $exceptCopy, $timeoutSeconds, $timeoutMicroseconds);
147+
148+
if (false === $result) {
149+
$error = error_get_last();
150+
if ($error && false !== stripos($error['message'], 'interrupt')) {
151+
return [];
152+
}
153+
throw new FailedPollWaitException($error['message'] ?? 'stream_select() failed');
154+
}
155+
156+
if (0 === $result) {
157+
return [];
158+
}
159+
160+
$triggered = [];
161+
foreach ($byId as $id => $watcher) {
162+
$isReadable = isset($readCopy[$id]);
163+
$isWritable = isset($writeCopy[$id]);
164+
$hasOob = isset($exceptCopy[$id]);
165+
166+
if (!$isReadable && !$isWritable && !$hasOob) {
167+
continue;
168+
}
169+
170+
$watched = $watcher->getWatchedEvents();
171+
$stream = $watcher->getHandle()->getStream();
172+
$events = [];
173+
174+
if ($isReadable) {
175+
$peek = @stream_socket_recvfrom($stream, 1, \STREAM_PEEK);
176+
if ('' === $peek) {
177+
$events[] = Event::HangUp;
178+
if (\in_array(Event::ReadHangUp, $watched, true)) {
179+
$events[] = Event::ReadHangUp;
180+
}
181+
} elseif (\in_array(Event::Read, $watched, true)) {
182+
$events[] = Event::Read;
183+
}
184+
}
185+
186+
if ($isWritable && \in_array(Event::Write, $watched, true)) {
187+
$events[] = Event::Write;
188+
}
189+
190+
if ($hasOob) {
191+
$events[] = Event::Error;
192+
}
193+
194+
if ($events) {
195+
$triggered[] = [$watcher, $events];
196+
}
197+
}
198+
199+
if (null !== $maxEvents && \count($triggered) > $maxEvents) {
200+
$triggered = \array_slice($triggered, 0, $maxEvents);
201+
}
202+
203+
$result = [];
204+
foreach ($triggered as [$watcher, $events]) {
205+
$setTriggered($watcher, $events);
206+
$result[] = $watcher;
207+
}
208+
209+
foreach ($result as $watcher) {
210+
foreach ($watcher->getWatchedEvents() as $event) {
211+
if (Event::OneShot === $event) {
212+
unset($this->watchers[$watcher]);
213+
$clearContext($watcher);
214+
break;
215+
}
216+
}
217+
}
218+
219+
return $result;
220+
}
221+
222+
public function __serialize(): array
223+
{
224+
throw new \Exception("Serialization of 'Io\\Poll\\Context' is not allowed");
225+
}
226+
227+
public function __unserialize(array $data): void
228+
{
229+
throw new \Exception("Unserialization of 'Io\\Poll\\Context' is not allowed");
230+
}
231+
}
232+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Io\Poll;
13+
14+
if (\PHP_VERSION_ID < 80600 && \PHP_VERSION_ID >= 80100) {
15+
enum Event
16+
{
17+
case Read;
18+
case Write;
19+
case Error;
20+
case HangUp;
21+
case ReadHangUp;
22+
case OneShot;
23+
case EdgeTriggered;
24+
}
25+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Io\Poll;
13+
14+
if (\PHP_VERSION_ID < 80600 && \PHP_VERSION_ID >= 80100) {
15+
class FailedContextInitializationException extends FailedPollOperationException
16+
{
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Io\Poll;
13+
14+
if (\PHP_VERSION_ID < 80600 && \PHP_VERSION_ID >= 80100) {
15+
class FailedHandleAddException extends FailedPollOperationException
16+
{
17+
}
18+
}

0 commit comments

Comments
 (0)