Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Polyfills are provided for:
- the `ARRAY_FILTER_USE_VALUE` constant introduced in PHP 8.6;
- the `SortDirection` enum introduced in PHP 8.6;
- the `grapheme_strrev` function introduced in PHP 8.6;
- the `Io\Poll` API and `StreamPollHandle` introduced in PHP 8.6 (requires PHP >= 8.1; only the `Poll` backend is available);

It is strongly recommended to upgrade your PHP version and/or install the missing
extensions whenever possible. This polyfill should be used only when there is no
Expand Down
2 changes: 2 additions & 0 deletions src/Php86/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ This component provides features added to PHP 8.6 core:
- [`clamp`](https://wiki.php.net/rfc/clamp_v2)
- `ARRAY_FILTER_USE_VALUE` constant
- [`SortDirection`](https://wiki.php.net/rfc/sort_direction_enum)
- `grapheme_strrev`
- [`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)

More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
Expand Down
18 changes: 18 additions & 0 deletions src/Php86/Resources/stubs/Io/IoException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Io;

if (\PHP_VERSION_ID < 80600 && \PHP_VERSION_ID >= 80100) {
class IoException extends \Exception
{
}
}
42 changes: 42 additions & 0 deletions src/Php86/Resources/stubs/Io/Poll/Backend.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Io\Poll;

if (\PHP_VERSION_ID < 80600 && \PHP_VERSION_ID >= 80100) {
enum Backend
{
case Auto;
case Poll;
case Epoll;
case Kqueue;
case EventPorts;
case WSAPoll;

public function isAvailable(): bool
{
return match ($this) {
self::Auto, self::Poll => true,
default => false,
};
}

public function supportsEdgeTriggering(): bool
{
return false;
}

public static function getAvailableBackends(): array
{
return [self::Auto, self::Poll];
}
}
}
18 changes: 18 additions & 0 deletions src/Php86/Resources/stubs/Io/Poll/BackendUnavailableException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Io\Poll;

if (\PHP_VERSION_ID < 80600 && \PHP_VERSION_ID >= 80100) {
class BackendUnavailableException extends PollException
{
}
}
217 changes: 217 additions & 0 deletions src/Php86/Resources/stubs/Io/Poll/Context.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Io\Poll;

if (\PHP_VERSION_ID < 80600 && \PHP_VERSION_ID >= 80100) {
final class Context
{
private Backend $backend;

/**
* @var \SplObjectStorage<Watcher, null>
*/
private \SplObjectStorage $watchers;

public function __construct(Backend $backend = Backend::Auto)
{
if (!$backend->isAvailable()) {
throw new BackendUnavailableException(\sprintf('Backend "%s" is not available.', $backend->name));
}

$this->backend = Backend::Auto === $backend ? Backend::Poll : $backend;
$this->watchers = new \SplObjectStorage();
}

public function getBackend(): Backend
{
return $this->backend;
}

public function add(Handle $handle, array $events, mixed $data = null): Watcher
{
if (!$handle instanceof \StreamPollHandle) {
throw new InvalidHandleException(\sprintf('Handle of type "%s" is not supported by the polyfill; only %s is.', \get_class($handle), \StreamPollHandle::class));
}

if (!$handle->isValid()) {
throw new InvalidHandleException('Handle is no longer valid.');
}

foreach ($events as $i => $event) {
if (!$event instanceof Event) {
throw new \TypeError(\sprintf('%s(): Argument #2 ($events)[%d] must be of type %s, %s given', __METHOD__, $i, Event::class, get_debug_type($event)));
}
}

$stream = $handle->getStream();
foreach ($this->watchers as $existing) {
if ($existing->getHandle() === $handle
|| ($existing->getHandle() instanceof \StreamPollHandle && $existing->getHandle()->getStream() === $stream)
) {
throw new HandleAlreadyWatchedException('Handle already added');
}
}

static $create;
$create ??= \Closure::bind(
static fn (Context $ctx, Handle $h, array $e, mixed $d): Watcher => new Watcher($ctx, $h, $e, $d),
null,
Watcher::class,
);
$watcher = $create($this, $handle, $events, $data);
$this->watchers[$watcher] = null;

return $watcher;
}

public function wait(?int $timeoutSeconds = null, int $timeoutMicroseconds = 0, ?int $maxEvents = null): array
{
static $setTriggered, $clearContext;
$setTriggered ??= \Closure::bind(
static function (Watcher $w, array $events): void { $w->triggeredEvents = $events; },
null,
Watcher::class,
);
$clearContext ??= \Closure::bind(
static function (Watcher $w): void { $w->context = null; },
null,
Watcher::class,
);

foreach ($this->watchers as $watcher) {
$setTriggered($watcher, []);
}

$read = $write = $except = [];
$byId = [];

foreach ($this->watchers as $watcher) {
$handle = $watcher->getHandle();
if (!$handle instanceof \StreamPollHandle || !$handle->isValid()) {
continue;
}

$stream = $handle->getStream();
$id = get_resource_id($stream);
$byId[$id] = $watcher;

$read[$id] = $stream;
$except[$id] = $stream;

foreach ($watcher->getWatchedEvents() as $event) {
if (Event::Write === $event) {
$write[$id] = $stream;
break;
}
}
}

if (!$byId) {
if (null !== $timeoutSeconds) {
$micros = $timeoutSeconds * 1_000_000 + $timeoutMicroseconds;
if ($micros > 0) {
usleep($micros);
}
}

return [];
}

$readCopy = $read;
$writeCopy = $write;
$exceptCopy = $except;

$result = @stream_select($readCopy, $writeCopy, $exceptCopy, $timeoutSeconds, $timeoutMicroseconds);

if (false === $result) {
$error = error_get_last();
if ($error && false !== stripos($error['message'], 'interrupt')) {
return [];
}
throw new FailedPollWaitException($error['message'] ?? 'stream_select() failed');
}

if (0 === $result) {
return [];
}

$triggered = [];
foreach ($byId as $id => $watcher) {
$isReadable = isset($readCopy[$id]);
$isWritable = isset($writeCopy[$id]);
$hasOob = isset($exceptCopy[$id]);

if (!$isReadable && !$isWritable && !$hasOob) {
continue;
}

$watched = $watcher->getWatchedEvents();
$stream = $watcher->getHandle()->getStream();
$events = [];

if ($isReadable) {
$peek = @stream_socket_recvfrom($stream, 1, \STREAM_PEEK);
if ('' === $peek) {
$events[] = Event::HangUp;
if (\in_array(Event::ReadHangUp, $watched, true)) {
$events[] = Event::ReadHangUp;
}
} elseif (\in_array(Event::Read, $watched, true)) {
$events[] = Event::Read;
}
}

if ($isWritable && \in_array(Event::Write, $watched, true)) {
$events[] = Event::Write;
}

if ($hasOob) {
$events[] = Event::Error;
}

if ($events) {
$setTriggered($watcher, $events);
$triggered[] = $watcher;
}
}

if (null !== $maxEvents && \count($triggered) > $maxEvents) {
for ($i = $maxEvents, $n = \count($triggered); $i < $n; ++$i) {
$setTriggered($triggered[$i], []);
}
$triggered = \array_slice($triggered, 0, $maxEvents);
}

foreach ($triggered as $watcher) {
foreach ($watcher->getWatchedEvents() as $event) {
if (Event::OneShot === $event) {
unset($this->watchers[$watcher]);
$clearContext($watcher);
break;
}
}
}

return $triggered;
}

public function __serialize(): array
{
throw new \Exception("Serialization of 'Io\\Poll\\Context' is not allowed");
}

public function __unserialize(array $data): void
{
throw new \Exception("Unserialization of 'Io\\Poll\\Context' is not allowed");
}
}
}
25 changes: 25 additions & 0 deletions src/Php86/Resources/stubs/Io/Poll/Event.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Io\Poll;

if (\PHP_VERSION_ID < 80600 && \PHP_VERSION_ID >= 80100) {
enum Event
{
case Read;
case Write;
case Error;
case HangUp;
case ReadHangUp;
case OneShot;
case EdgeTriggered;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Io\Poll;

if (\PHP_VERSION_ID < 80600 && \PHP_VERSION_ID >= 80100) {
class FailedContextInitializationException extends FailedPollOperationException
{
}
}
18 changes: 18 additions & 0 deletions src/Php86/Resources/stubs/Io/Poll/FailedHandleAddException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Io\Poll;

if (\PHP_VERSION_ID < 80600 && \PHP_VERSION_ID >= 80100) {
class FailedHandleAddException extends FailedPollOperationException
{
}
}
Loading
Loading