Skip to content

Commit 913b2fa

Browse files
committed
Reintroduce the ext-eio adapter
During the initial rewrite this adapter was removed to make the rewrite easier. Since that is now in adding the ext-eio adapter will provide several high performance adapters.
1 parent 57d8f3a commit 913b2fa

File tree

13 files changed

+540
-10
lines changed

13 files changed

+540
-10
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,16 @@ jobs:
2222
extensions:
2323
- ""
2424
- "uv-amphp/ext-uv@master"
25+
- "uv-amphp/ext-uv@master, eio"
26+
- "eio"
2527
steps:
2628
- uses: actions/checkout@v2
2729
- name: Install libuv
2830
if: matrix.os == 'ubuntu-latest'
2931
run: sudo apt-get install libuv1-dev
32+
- name: Install ext-eio
33+
if: matrix.os == 'ubuntu-latest' && (matrix.extensions == 'eio' || matrix.extensions == 'uv-amphp/ext-uv@master, eio')
34+
run: sudo pecl install eio || sudo pecl install eio-beta
3035
- uses: shivammathur/setup-php@v2
3136
with:
3237
php-version: ${{ matrix.php }}

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* [Factory](#factory)
1717
* [create()](#create)
1818
* [Filesystem implementations](#filesystem-implementations)
19+
* [Eio](#eio)
1920
* [Uv](#uv)
2021
* [AdapterInterface](#adapterinterface)
2122
* [detect()](#detect)
@@ -110,6 +111,15 @@ manually instantiate one of the following classes.
110111
Note that you may have to install the required PHP extensions for the respective
111112
event loop implementation first or they will throw a `BadMethodCallException` on creation.
112113

114+
#### Eio
115+
116+
An `ext-eio` based filesystem.
117+
118+
This filesystem uses the [`eio` PECL extension](https://pecl.php.net/package/eio), that
119+
provides an interface to `libeio` library.
120+
121+
This filesystem is known to work with PHP 7+.
122+
113123
#### Uv
114124

115125
An `ext-uv` based filesystem.

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
}
3535
},
3636
"suggest": {
37+
"ext-eio": "* for great I/O performance",
3738
"ext-uv": "* for better I/O performance"
3839
},
3940
"config": {

src/Eio/Adapter.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace React\Filesystem\Eio;
4+
5+
use React\EventLoop\Loop;
6+
use React\EventLoop\LoopInterface;
7+
use React\Filesystem\AdapterInterface;
8+
use React\Filesystem\ModeTypeDetector;
9+
use React\Filesystem\Node;
10+
use React\Filesystem\PollInterface;
11+
use React\Filesystem\Stat;
12+
use React\Promise\PromiseInterface;
13+
14+
final class Adapter implements AdapterInterface
15+
{
16+
use StatTrait;
17+
18+
private LoopInterface $loop;
19+
private PollInterface $poll;
20+
21+
public function __construct()
22+
{
23+
$this->loop = Loop::get();
24+
$this->poll = new Poll($this->loop);
25+
}
26+
27+
public function detect(string $path): PromiseInterface
28+
{
29+
return $this->internalStat($path)->then(function (?Stat $stat) use ($path) {
30+
if ($stat === null) {
31+
return new NotExist($this->poll, $this, $this->loop, dirname($path) . DIRECTORY_SEPARATOR, basename($path));
32+
}
33+
34+
switch (ModeTypeDetector::detect($stat->mode())) {
35+
case Node\FileInterface::class:
36+
return $this->file($stat->path());
37+
break;
38+
case Node\DirectoryInterface::class:
39+
return $this->directory($stat->path());
40+
break;
41+
default:
42+
return new Node\Unknown($stat->path(), $stat->path());
43+
break;
44+
}
45+
});
46+
}
47+
48+
public function directory(string $path): Node\DirectoryInterface
49+
{
50+
return new Directory($this->poll, $this, dirname($path) . DIRECTORY_SEPARATOR, basename($path));
51+
}
52+
53+
public function file(string $path): Node\FileInterface
54+
{
55+
return new File($this->poll, dirname($path) . DIRECTORY_SEPARATOR, basename($path));
56+
}
57+
58+
protected function activate(): void
59+
{
60+
$this->poll->activate();
61+
}
62+
63+
protected function deactivate(): void
64+
{
65+
$this->poll->deactivate();
66+
}
67+
}

src/Eio/Directory.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
namespace React\Filesystem\Eio;
4+
5+
use React\Filesystem\AdapterInterface;
6+
use React\Filesystem\Node;
7+
use React\Filesystem\PollInterface;
8+
use React\Promise\Promise;
9+
use React\Promise\PromiseInterface;
10+
use function React\Promise\all;
11+
12+
final class Directory implements Node\DirectoryInterface
13+
{
14+
use StatTrait;
15+
16+
private PollInterface $poll;
17+
private AdapterInterface $filesystem;
18+
private string $path;
19+
private string $name;
20+
private int $workInProgress = 0;
21+
22+
public function __construct(PollInterface $poll, AdapterInterface $filesystem, string $path, string $name)
23+
{
24+
$this->poll = $poll;
25+
$this->filesystem = $filesystem;
26+
$this->path = $path;
27+
$this->name = $name;
28+
}
29+
30+
public function stat(): PromiseInterface
31+
{
32+
return $this->internalStat($this->path . $this->name);
33+
}
34+
35+
public function ls(): PromiseInterface
36+
{
37+
$this->activate();
38+
return new Promise(function (callable $resolve): void {
39+
\eio_readdir($this->path . $this->name . DIRECTORY_SEPARATOR, \EIO_READDIR_STAT_ORDER | \EIO_READDIR_DIRS_FIRST, \EIO_PRI_DEFAULT, function ($_, $contents) use ($resolve): void {
40+
$this->deactivate();
41+
$list = [];
42+
foreach ($contents['dents'] as $node) {
43+
$fullPath = $this->path . $this->name . DIRECTORY_SEPARATOR . $node['name'];
44+
switch ($node['type'] ?? null) {
45+
case EIO_DT_DIR:
46+
$list[] = $this->filesystem->directory($fullPath);
47+
break;
48+
case EIO_DT_REG :
49+
$list[] = $this->filesystem->file($fullPath);
50+
break;
51+
default:
52+
$list[] = $this->filesystem->detect($this->path . $this->name . DIRECTORY_SEPARATOR . $node['name']);
53+
break;
54+
}
55+
}
56+
57+
$resolve(all($list));
58+
});
59+
});
60+
}
61+
62+
public function unlink(): PromiseInterface
63+
{
64+
$this->activate();
65+
return new Promise(function (callable $resolve): void {
66+
\eio_readdir($this->path . $this->name . DIRECTORY_SEPARATOR, \EIO_READDIR_STAT_ORDER | \EIO_READDIR_DIRS_FIRST, \EIO_PRI_DEFAULT, function ($_, $contents) use ($resolve): void {
67+
$this->deactivate();
68+
if (count($contents['dents']) > 0) {
69+
$resolve(false);
70+
71+
return;
72+
}
73+
74+
$this->activate();
75+
\eio_rmdir($this->path . $this->name, function () use ($resolve): void {
76+
$this->deactivate();
77+
$resolve(true);
78+
});
79+
});
80+
});
81+
}
82+
83+
public function path(): string
84+
{
85+
return $this->path;
86+
}
87+
88+
public function name(): string
89+
{
90+
return $this->name;
91+
}
92+
93+
protected function activate(): void
94+
{
95+
$this->poll->activate();
96+
}
97+
98+
protected function deactivate(): void
99+
{
100+
$this->poll->deactivate();
101+
}
102+
}

src/Eio/EventStream.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace React\Filesystem\Eio;
4+
5+
/**
6+
* Singleton to make sure we always only have one file descriptor for the ext-eio event stream.
7+
* Creating more than one will invalidate the previous ones and make anything still using those fail.
8+
*
9+
* @internal
10+
*/
11+
final class EventStream
12+
{
13+
private static $fd = null;
14+
15+
public static function get()
16+
{
17+
if (self::$fd !== null) {
18+
return self::$fd;
19+
}
20+
21+
self::$fd = eio_get_event_stream();
22+
23+
return self::$fd;
24+
}
25+
}

src/Eio/File.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
namespace React\Filesystem\Eio;
4+
5+
use React\Filesystem\Node\FileInterface;
6+
use React\Filesystem\PollInterface;
7+
use React\Promise\Promise;
8+
use React\Promise\PromiseInterface;
9+
10+
final class File implements FileInterface
11+
{
12+
use StatTrait;
13+
14+
private PollInterface $poll;
15+
private string $path;
16+
private string $name;
17+
18+
public function __construct(PollInterface $poll, string $path, string $name)
19+
{
20+
$this->poll = $poll;
21+
$this->path = $path;
22+
$this->name = $name;
23+
}
24+
25+
public function stat(): PromiseInterface
26+
{
27+
return $this->internalStat($this->path . $this->name);
28+
}
29+
30+
public function getContents(int $offset = 0 , ?int $maxlen = null): PromiseInterface
31+
{
32+
$this->activate();
33+
return new Promise(function (callable $resolve, callable $reject) use ($offset, $maxlen): void {
34+
\eio_open(
35+
$this->path . DIRECTORY_SEPARATOR . $this->name,
36+
\EIO_O_RDONLY,
37+
0,
38+
\EIO_PRI_DEFAULT,
39+
function ($_, $fileDescriptor) use ($resolve, $reject, $offset, $maxlen): void {
40+
try {
41+
\eio_fstat($fileDescriptor, \EIO_PRI_DEFAULT, function ($fileDescriptor, $stat) use ($resolve, $reject, $offset, $maxlen): void {
42+
try {
43+
\eio_read($fileDescriptor, $maxlen ?? (int)$stat['size'], $offset, \EIO_PRI_DEFAULT, function ($fileDescriptor, string $buffer) use ($resolve): void {
44+
\eio_close($fileDescriptor, \EIO_PRI_DEFAULT, function () use ($resolve, $buffer) {
45+
$this->deactivate();
46+
$resolve($buffer);
47+
});
48+
}, $fileDescriptor);
49+
} catch (\Throwable $error) {
50+
$this->deactivate();
51+
$reject($error);
52+
}
53+
}, $fileDescriptor);
54+
} catch (\Throwable $error) {
55+
$this->deactivate();
56+
$reject($error);
57+
}
58+
}
59+
);
60+
});
61+
}
62+
63+
public function putContents(string $contents, int $flags = 0)
64+
{
65+
$this->activate();
66+
return new Promise(function (callable $resolve, callable $reject) use ($contents, $flags): void {
67+
\eio_open(
68+
$this->path . DIRECTORY_SEPARATOR . $this->name,
69+
(($flags & \FILE_APPEND) == \FILE_APPEND) ? \EIO_O_RDWR | \EIO_O_APPEND : \EIO_O_RDWR | \EIO_O_CREAT,
70+
0644,
71+
\EIO_PRI_DEFAULT,
72+
function ($_, $fileDescriptor) use ($resolve, $reject, $contents, $flags): void {
73+
try {
74+
\eio_write($fileDescriptor, $contents, strlen($contents), 0, \EIO_PRI_DEFAULT, function ($fileDescriptor, int $bytesWritten) use ($resolve, $reject): void {
75+
try {
76+
\eio_close($fileDescriptor, \EIO_PRI_DEFAULT, function () use ($resolve, $bytesWritten): void {
77+
$this->deactivate();
78+
$resolve($bytesWritten);
79+
});
80+
} catch (\Throwable $error) {
81+
$this->deactivate();
82+
$reject($error);
83+
}
84+
}, $fileDescriptor);
85+
} catch (\Throwable $error) {
86+
$this->deactivate();
87+
$reject($error);
88+
}
89+
}
90+
);
91+
});
92+
}
93+
94+
public function unlink(): PromiseInterface
95+
{
96+
$this->activate();
97+
return new Promise(function (callable $resolve): void {
98+
\eio_unlink($this->path . DIRECTORY_SEPARATOR . $this->name, \EIO_PRI_DEFAULT, function () use ($resolve): void {
99+
$this->deactivate();
100+
$resolve(true);
101+
});
102+
});
103+
}
104+
105+
public function path(): string
106+
{
107+
return $this->path;
108+
}
109+
110+
public function name(): string
111+
{
112+
return $this->name;
113+
}
114+
115+
protected function activate(): void
116+
{
117+
$this->poll->activate();
118+
}
119+
120+
protected function deactivate(): void
121+
{
122+
$this->poll->deactivate();
123+
}
124+
}

0 commit comments

Comments
 (0)