├── 2-streams
├── file.txt
├── duplex.php
├── piping.php
├── writable-events-1.php
├── through.php
├── writable-events-2.php
├── reading-control.php
├── composite.php
├── read-file.php
├── writable.php
├── spooling.php
├── is-writable.php
├── writable-end.php
├── readable-events.php
└── is-readable.php
├── 6-filesystem
├── link
│ ├── test.txt
│ ├── test_link.txt
│ ├── read.php
│ ├── create.php
│ └── remove.php
├── file
│ ├── dest.txt
│ ├── new.txt
│ ├── generate_huge_test_file.php
│ ├── stat.php
│ ├── time.php
│ ├── chmod.php
│ ├── create.php
│ ├── size.php
│ ├── create_with_open.php
│ ├── remove.php
│ ├── create_with_touch.php
│ ├── exists.php
│ ├── write_with_put_contents.php
│ ├── chown.php
│ ├── read.php
│ ├── rename.php
│ ├── write_with_stream.php
│ └── open.php
├── adapter.php
└── directory
│ ├── stat.php
│ ├── create.php
│ ├── remove.php
│ ├── create_recursive.php
│ ├── size.php
│ └── list.php
├── .gitignore
├── 10-promise-stream
├── stream-to-promise
│ ├── file.txt
│ ├── buffer.php
│ ├── spooling_problem.php
│ ├── spooling_with_promise.php
│ ├── spooling_with_promise_chunks.php
│ └── spooling_with_promise_collect_error.php
└── promise-to-stream
│ ├── unwrap_writable.php
│ ├── unwrap_readable.php
│ ├── unwrap_readable_close.php
│ ├── unwrap_readable_error.php
│ └── unwrap_readable_no_error.php
├── cover.jpg
├── README.md
├── 1-timers
├── one-off.php
├── blocked_loop.php
├── periodic.php
├── controlling_by_timer.php
├── controlling_by_counter.php
└── check_active.php
├── 3-ticks
├── future.php
├── future-closure.php
├── all.php
└── future-resucrsively.php
├── 4-promises
├── promise-done-fatal.php
├── promise-done.php
├── promise-fail.php
├── promise-done-full.php
├── resolution-forwarding.php
├── mixed-forwarding.php
├── rejection-forwarding.php
└── rejection-forwarding-typehints.php
├── 7-restulf-api-with-mysql
├── src
│ ├── UserNotFoundError.php
│ ├── Controller
│ │ ├── ListUsers.php
│ │ ├── DeleteUser.php
│ │ ├── ViewUser.php
│ │ ├── CreateUser.php
│ │ └── UpdateUser.php
│ ├── Auth.php
│ ├── JsonResponse.php
│ ├── Router.php
│ └── Users.php
├── composer.json
├── requests.http
└── index.php
├── 5-socket
├── chat-server
│ ├── composer.json
│ ├── index.php
│ └── src
│ │ ├── Output.php
│ │ ├── Connections.php
│ │ ├── Message.php
│ │ └── Server.php
└── client.php
├── 4-managing-promises
├── reject.php
├── resolve.php
├── all.php
├── any.php
├── some.php
└── race.php
├── 8-child-process
├── pipe-io.php
├── input-hello-world.php
├── ping-with-timer.php
├── ping.php
├── process-is-already-started.php
├── pipe-commands.php
└── pid.php
├── 11-integration-with-sync-applications
├── await.php
├── request-with-sleep.php
├── awaitAny.php
├── await-reject.php
└── awaitAll.php
├── 9-cancelling-promises
├── simple-promise.php
├── cancel-simple-promise.php
├── cancel-and-stop-timer.php
└── timeout-promise-wrapper.php
├── phpunit.xml
├── composer.json
└── 12-promise-testing
├── Mocks
├── PromiseRejectsTest.php
├── PromiseFulfillsTest.php
├── PromiseFulfillsWithTest.php
└── PromiseRejectsWithTest.php
└── Waiting
├── PromiseRejectsTest.php
├── PromiseFulfillsTest.php
├── PromiseFulfillsWithTest.php
└── PromiseRejectsWithTest.php
/2-streams/file.txt:
--------------------------------------------------------------------------------
1 | Lorem ipsum
--------------------------------------------------------------------------------
/6-filesystem/link/test.txt:
--------------------------------------------------------------------------------
1 | Hello world
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | composer.lock
3 |
--------------------------------------------------------------------------------
/6-filesystem/file/dest.txt:
--------------------------------------------------------------------------------
1 | Hello world
2 |
--------------------------------------------------------------------------------
/6-filesystem/file/new.txt:
--------------------------------------------------------------------------------
1 | Hello world
2 |
--------------------------------------------------------------------------------
/6-filesystem/link/test_link.txt:
--------------------------------------------------------------------------------
1 | test.txt
--------------------------------------------------------------------------------
/10-promise-stream/stream-to-promise/file.txt:
--------------------------------------------------------------------------------
1 | Lorem ipsum
2 |
--------------------------------------------------------------------------------
/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seregazhuk/reactphp-book/HEAD/cover.jpg
--------------------------------------------------------------------------------
/6-filesystem/file/generate_huge_test_file.php:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/1-timers/one-off.php:
--------------------------------------------------------------------------------
1 | addTimer(2, fn () => print "Hello world\n");
8 | $loop->run();
9 |
10 | echo "finished\n";
11 |
--------------------------------------------------------------------------------
/3-ticks/future.php:
--------------------------------------------------------------------------------
1 | futureTick(fn() => print "Tick\n");
8 |
9 | echo "Loop starts\n";
10 |
11 | $eventLoop->run();
12 |
13 | echo "Loop stops\n";
14 |
--------------------------------------------------------------------------------
/4-promises/promise-done-fatal.php:
--------------------------------------------------------------------------------
1 | promise();
6 | $promise->done(
7 | function ($data) {
8 | throw new Exception('error');
9 | }
10 | );
11 |
12 | $deferred->resolve('no results');
13 |
--------------------------------------------------------------------------------
/4-promises/promise-done.php:
--------------------------------------------------------------------------------
1 | promise();
8 | $promise->done(
9 | fn ($data) => print 'Done: ' . $data . PHP_EOL
10 | );
11 |
12 | $deferred->resolve('hello world');
13 |
--------------------------------------------------------------------------------
/4-promises/promise-fail.php:
--------------------------------------------------------------------------------
1 | promise();
8 | $promise->otherwise(
9 | fn ($data) => print 'Fail: ' . $data . PHP_EOL
10 | );
11 |
12 | $deferred->reject('no results');
13 |
--------------------------------------------------------------------------------
/7-restulf-api-with-mysql/src/UserNotFoundError.php:
--------------------------------------------------------------------------------
1 | addPeriodicTimer(1, function() use (&$i) {
9 | echo ++$i, "\n";
10 | });
11 |
12 | $loop->addTimer(2, fn () => sleep(10));
13 |
14 | $loop->run();
15 |
--------------------------------------------------------------------------------
/3-ticks/future-closure.php:
--------------------------------------------------------------------------------
1 | futureTick(fn() => print $string);
9 |
10 | echo "Loop starts\n";
11 |
12 | $eventLoop->run();
13 |
14 | echo "Loop stops\n";
15 |
--------------------------------------------------------------------------------
/1-timers/periodic.php:
--------------------------------------------------------------------------------
1 | addPeriodicTimer(
8 | 2,
9 | function () use (&$counter) {
10 | $counter++;
11 | echo "$counter\n";
12 | }
13 | );
14 |
15 | $loop->run();
16 |
--------------------------------------------------------------------------------
/6-filesystem/adapter.php:
--------------------------------------------------------------------------------
1 | getAdapter()) . PHP_EOL;
11 |
12 | $loop->run();
13 |
--------------------------------------------------------------------------------
/5-socket/chat-server/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "event-driven-php/socket-chat",
3 | "minimum-stability": "stable",
4 | "require": {
5 | "react/socket": "^1.5",
6 | "clue/stdio-react": "^2.3"
7 | },
8 | "autoload": {
9 | "psr-4": {
10 | "Chat\\": "src/"
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/2-streams/duplex.php:
--------------------------------------------------------------------------------
1 | write('hello!');
11 | $stream->end();
12 |
13 | $loop->run();
14 |
--------------------------------------------------------------------------------
/4-managing-promises/reject.php:
--------------------------------------------------------------------------------
1 | resolve('my-value');
7 |
8 | $promise = React\Promise\reject($deferred->promise());
9 | $promise->then(
10 | null,
11 | fn($reason) => print "Promise was rejected with: $reason\n"
12 | );
13 |
--------------------------------------------------------------------------------
/2-streams/piping.php:
--------------------------------------------------------------------------------
1 | pipe($output);
11 |
12 | $loop->run();
13 |
--------------------------------------------------------------------------------
/4-promises/promise-done-full.php:
--------------------------------------------------------------------------------
1 | promise();
8 | $promise->done(
9 | fn ($data) => print 'Done: ' . $data . PHP_EOL,
10 | fn ($data) => print 'Reject: ' . $data . PHP_EOL
11 | );
12 |
13 | $deferred->reject('hello world');
14 |
--------------------------------------------------------------------------------
/2-streams/writable-events-1.php:
--------------------------------------------------------------------------------
1 | write("Hello world\n"));
9 |
10 | $writable->on(
11 | 'drain',
12 | fn () => print "The stream is drained\n"
13 | );
14 |
15 | $loop->run();
16 |
--------------------------------------------------------------------------------
/5-socket/chat-server/index.php:
--------------------------------------------------------------------------------
1 | run();
12 |
13 | echo "Listening on {$socket->getAddress()}\n";
14 |
15 | $loop->run();
16 |
--------------------------------------------------------------------------------
/6-filesystem/file/stat.php:
--------------------------------------------------------------------------------
1 | file('new.txt')->stat()->then(
12 | function ($stat) {
13 | print_r($stat);
14 | }
15 | );
16 |
17 | $loop->run();
18 |
--------------------------------------------------------------------------------
/6-filesystem/file/time.php:
--------------------------------------------------------------------------------
1 | file('new.txt')->time()->then(
12 | function ($time) {
13 | print_r($time);
14 | }
15 | );
16 |
17 | $loop->run();
18 |
--------------------------------------------------------------------------------
/6-filesystem/file/chmod.php:
--------------------------------------------------------------------------------
1 | file('test.txt')
12 | ->chmod(0755)
13 | ->then(
14 | fn() => print "Mode changed\n"
15 | );
16 |
17 | $loop->run();
18 |
--------------------------------------------------------------------------------
/6-filesystem/file/create.php:
--------------------------------------------------------------------------------
1 | file('new_created.txt');
12 | $file->create()->then(
13 | fn() => print 'File created' . PHP_EOL
14 | );
15 |
16 | $loop->run();
17 |
--------------------------------------------------------------------------------
/6-filesystem/file/size.php:
--------------------------------------------------------------------------------
1 | file('new.txt')
12 | ->size()
13 | ->then(
14 | fn($size) => print "Size is: $size bytes\n"
15 | );
16 |
17 | $loop->run();
18 |
--------------------------------------------------------------------------------
/3-ticks/all.php:
--------------------------------------------------------------------------------
1 | addTimer(0, fn() => print "Timer\n");
8 | $eventLoop->futureTick(fn() => print "Future tick\n");
9 |
10 | $writable = new \React\Stream\WritableResourceStream(fopen('php://stdout', 'w'), $eventLoop);
11 | $writable->write("I\O");
12 |
13 | $eventLoop->run();
14 |
--------------------------------------------------------------------------------
/6-filesystem/file/create_with_open.php:
--------------------------------------------------------------------------------
1 | file('new_3.txt');
12 | $file->open('c')->then(
13 | fn() => print 'File created' . PHP_EOL
14 | );
15 |
16 | $loop->run();
17 |
--------------------------------------------------------------------------------
/4-managing-promises/resolve.php:
--------------------------------------------------------------------------------
1 | promise());
7 | var_dump($deferred->promise() === $promise);
8 |
9 | $deferred->resolve('hello world');
10 |
11 | $promise = React\Promise\resolve($value = 'my-value');
12 | $promise->then(fn($value) => print $value . PHP_EOL);
13 |
--------------------------------------------------------------------------------
/6-filesystem/directory/stat.php:
--------------------------------------------------------------------------------
1 | dir(__DIR__);
11 |
12 | $dir->stat()->then(
13 | function ($stat) {
14 | print_r($stat);
15 | }
16 | );
17 |
18 | $loop->run();
19 |
--------------------------------------------------------------------------------
/6-filesystem/file/remove.php:
--------------------------------------------------------------------------------
1 | file('test.txt')
12 | ->remove()
13 | ->then(
14 | fn() => print 'File was removed' . PHP_EOL
15 | );
16 |
17 | $loop->run();
18 |
--------------------------------------------------------------------------------
/1-timers/controlling_by_timer.php:
--------------------------------------------------------------------------------
1 | addPeriodicTimer(
8 | 2,
9 | function () use (&$counter) {
10 | $counter++;
11 | echo "$counter\n";
12 | }
13 | );
14 |
15 | $loop->addTimer(5, fn() => $loop->cancelTimer($periodicTimer));
16 |
17 | $loop->run();
18 |
--------------------------------------------------------------------------------
/2-streams/through.php:
--------------------------------------------------------------------------------
1 | pipe($through)->pipe($output);
12 |
13 | $loop->run();
14 |
--------------------------------------------------------------------------------
/6-filesystem/file/create_with_touch.php:
--------------------------------------------------------------------------------
1 | file('test.txt');
12 | $file->touch()->then(
13 | function () {
14 | echo 'File created' . PHP_EOL;
15 | }
16 | );
17 |
18 | $loop->run();
19 |
--------------------------------------------------------------------------------
/7-restulf-api-with-mysql/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": {
3 | "react/mysql": "^0.5",
4 | "react/http": "^1.0",
5 | "nikic/fast-route": "^1.3",
6 | "for/http-middleware-psr15-adapter": "^1.0",
7 | "middlewares/http-authentication": "^2.1",
8 | "guzzlehttp/psr7": "dev-master"
9 | },
10 | "autoload": {
11 | "psr-4": {
12 | "App\\": "src/"
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/2-streams/writable-events-2.php:
--------------------------------------------------------------------------------
1 | on('end', fn () => print "End\n");
10 |
11 | $writable->on('close', fn () => print "Close\n");
12 |
13 | $loop->addTimer(1, fn() => $writable->end());
14 |
15 | $loop->run();
16 |
--------------------------------------------------------------------------------
/6-filesystem/directory/create.php:
--------------------------------------------------------------------------------
1 | dir('new');
11 |
12 | $dir->create()->then(
13 | fn() => print "Created\n",
14 | fn(Exception $e) => print "Error: {$e->getMessage()}\n"
15 | );
16 |
17 | $loop->run();
18 |
--------------------------------------------------------------------------------
/6-filesystem/link/read.php:
--------------------------------------------------------------------------------
1 | getAdapter()
12 | ->unlink('test_link.txt')
13 | ->then(
14 | function ($path) {
15 | echo $path . PHP_EOL;
16 | }
17 | );
18 |
19 | $loop->run();
20 |
--------------------------------------------------------------------------------
/8-child-process/pipe-io.php:
--------------------------------------------------------------------------------
1 | start($loop);
12 |
13 | $process->stdout->on(
14 | 'data',
15 | function ($data) {
16 | echo 'Total number of files and folders :' . $data;
17 | }
18 | );
19 |
20 | $loop->run();
21 |
--------------------------------------------------------------------------------
/10-promise-stream/promise-to-stream/unwrap_writable.php:
--------------------------------------------------------------------------------
1 | promise());
7 |
8 | $loop = \React\EventLoop\Factory::create();
9 | $deferred->resolve(new \React\Stream\WritableResourceStream(fopen('php://stdout', 'w'), $loop));
10 |
11 | $stream->write('Hello world!');
12 |
13 | $loop->run();
14 |
--------------------------------------------------------------------------------
/6-filesystem/directory/remove.php:
--------------------------------------------------------------------------------
1 | dir('temp');
11 |
12 | $dir->removeRecursive()->then(
13 | fn() => print "Removed\n",
14 | fn(Exception $e) => print "Error: {$e->getMessage()}\n"
15 | );
16 |
17 | $loop->run();
18 |
--------------------------------------------------------------------------------
/6-filesystem/file/exists.php:
--------------------------------------------------------------------------------
1 | file('neew.txt')
12 | ->exists()
13 | ->then(
14 | fn() => print 'File exists' . PHP_EOL,
15 | fn() => print 'File not found' . PHP_EOL
16 | );
17 |
18 | $loop->run();
19 |
--------------------------------------------------------------------------------
/6-filesystem/file/write_with_put_contents.php:
--------------------------------------------------------------------------------
1 | file('test.txt');
12 | $file
13 | ->putContents("Hello world\n")
14 | ->then(
15 | fn() => print "Data was written" . PHP_EOL
16 | );
17 |
18 | $loop->run();
19 |
--------------------------------------------------------------------------------
/6-filesystem/link/create.php:
--------------------------------------------------------------------------------
1 | getAdapter()
12 | ->symlink('test.txt', 'test_link.txt')
13 | ->then(
14 | function () {
15 | echo 'Link created' . PHP_EOL;
16 | }
17 | );
18 |
19 | $loop->run();
20 |
--------------------------------------------------------------------------------
/8-child-process/input-hello-world.php:
--------------------------------------------------------------------------------
1 | start($loop);
12 | $process->stdout->on(
13 | 'data',
14 | function ($data) {
15 | echo $data;
16 | }
17 | );
18 |
19 | $process->stdin->write("echo 'Hello World';\n");
20 | $loop->run();
21 |
22 |
--------------------------------------------------------------------------------
/2-streams/reading-control.php:
--------------------------------------------------------------------------------
1 | on(
9 | 'data',
10 | function ($data) use ($stream, $loop) {
11 | echo $data, "\n";
12 | $stream->pause();
13 | $loop->addTimer(1, fn() => $stream->resume());
14 | }
15 | );
16 |
17 | $loop->run();
18 |
--------------------------------------------------------------------------------
/4-managing-promises/all.php:
--------------------------------------------------------------------------------
1 | promise(),
10 | $secondResolver->promise(),
11 | ];
12 |
13 | $promise = \React\Promise\all($pending)->then(
14 | fn ($resolved) => print_r($resolved)
15 | );
16 |
17 | $firstResolver->resolve(10);
18 | $secondResolver->resolve(20);
19 |
--------------------------------------------------------------------------------
/6-filesystem/directory/create_recursive.php:
--------------------------------------------------------------------------------
1 | dir('new/test/test');
11 |
12 | $dir->createRecursive()->then(
13 | fn() => print "Created\n",
14 | fn(Exception $e) => print "Error: {$e->getMessage()}\n"
15 | );
16 |
17 | $loop->run();
18 |
--------------------------------------------------------------------------------
/6-filesystem/file/chown.php:
--------------------------------------------------------------------------------
1 | file('test.txt')
12 | ->chown('serega')
13 | ->then(
14 | fn() => print 'Owner changed' . PHP_EOL,
15 | fn(Exception $e) => print "Error: {$e->getMessage()}\n"
16 | );
17 |
18 | $loop->run();
19 |
--------------------------------------------------------------------------------
/2-streams/composite.php:
--------------------------------------------------------------------------------
1 | on(
12 | 'data',
13 | fn($chunk) => $composite->write('You said: ' . $chunk)
14 | );
15 |
16 | $loop->run();
17 |
--------------------------------------------------------------------------------
/2-streams/read-file.php:
--------------------------------------------------------------------------------
1 | on(
9 | 'data',
10 | function ($data) {
11 | // process data *line by line*
12 | }
13 | );
14 |
15 | $stream->on(
16 | 'end',
17 | function () {
18 | echo "finished\n";
19 | }
20 | );
21 |
22 | $loop->run();
23 |
--------------------------------------------------------------------------------
/6-filesystem/file/read.php:
--------------------------------------------------------------------------------
1 | file('test.txt');
12 | $file->getContents()->then(
13 | fn ($contents) => print 'Reading completed' . PHP_EOL
14 | );
15 | $loop->addPeriodicTimer(1, fn() => print 'Timer' . PHP_EOL);
16 |
17 | $loop->run();
18 |
--------------------------------------------------------------------------------
/2-streams/writable.php:
--------------------------------------------------------------------------------
1 | on(
11 | 'data',
12 | fn($data) => $output->write($data)
13 | );
14 |
15 | $readable->on(
16 | 'end',
17 | fn() => $output->end()
18 | );
19 |
20 | $loop->run();
21 |
--------------------------------------------------------------------------------
/6-filesystem/file/rename.php:
--------------------------------------------------------------------------------
1 | file('test.txt')->rename('new.txt')->then(
13 | function (FileInterface $file) {
14 | echo 'File was renamed to: ' . $file->getPath() . PHP_EOL;
15 | }
16 | );
17 |
18 | $loop->run();
19 |
--------------------------------------------------------------------------------
/3-ticks/future-resucrsively.php:
--------------------------------------------------------------------------------
1 | futureTick($callback);
12 | };
13 |
14 | $eventLoop->futureTick($callback);
15 | $eventLoop->futureTick(fn(LoopInterface $eventLoop) => $eventLoop->stop());
16 |
17 | $eventLoop->run();
18 | echo "Finished\n";
19 |
--------------------------------------------------------------------------------
/1-timers/controlling_by_counter.php:
--------------------------------------------------------------------------------
1 | addPeriodicTimer(
10 | 2,
11 | function (TimerInterface $timer) use (&$counter, $loop) {
12 | $counter++;
13 | echo "$counter\n";
14 |
15 | if ($counter == 5) {
16 | $loop->cancelTimer($timer);
17 | }
18 | }
19 | );
20 |
21 | $loop->run();
22 | echo "Done\n";
23 |
--------------------------------------------------------------------------------
/8-child-process/ping-with-timer.php:
--------------------------------------------------------------------------------
1 | start($loop);
12 | $process->stdout->on(
13 | 'data',
14 | function ($data) {
15 | echo $data;
16 | }
17 | );
18 |
19 | $loop->addTimer(3, fn() => $process->terminate());
20 | $process->on('exit', fn() => print 'Process exited');
21 |
22 | $loop->run();
23 |
--------------------------------------------------------------------------------
/1-timers/check_active.php:
--------------------------------------------------------------------------------
1 | addPeriodicTimer(
10 | 2,
11 | function (TimerInterface $timer) use ($loop, &$counter) {
12 | $counter++;
13 | echo "$counter ";
14 |
15 | if ($counter === 5) {
16 | $loop->cancelTimer($timer);
17 | }
18 |
19 | echo "\n";
20 | }
21 | );
22 |
23 | $loop->run();
24 | echo 'stop';
25 |
--------------------------------------------------------------------------------
/11-integration-with-sync-applications/await.php:
--------------------------------------------------------------------------------
1 | get($endpoint), $eventLoop);
17 |
18 | echo $result->getBody() . PHP_EOL;
19 | // some synchronous staff ...
20 |
--------------------------------------------------------------------------------
/8-child-process/ping.php:
--------------------------------------------------------------------------------
1 | start($loop);
12 | $process->stdout->on(
13 | 'data',
14 | function ($data) {
15 | echo $data;
16 | }
17 | );
18 |
19 | $process->on(
20 | 'exit',
21 | function ($exitCode, $termSignal) {
22 | echo "Process exited\n";
23 | }
24 | );
25 |
26 | $loop->run();
27 |
--------------------------------------------------------------------------------
/10-promise-stream/promise-to-stream/unwrap_readable.php:
--------------------------------------------------------------------------------
1 | promise());
7 |
8 | $stream->on(
9 | 'data',
10 | function ($data) {
11 | echo 'Received: ' . $data . PHP_EOL;
12 | }
13 | );
14 |
15 | $loop = \React\EventLoop\Factory::create();
16 | $deferred->resolve(new \React\Stream\ReadableResourceStream(fopen('php://stdin', 'r'), $loop));
17 |
18 | $loop->run();
19 |
--------------------------------------------------------------------------------
/6-filesystem/file/write_with_stream.php:
--------------------------------------------------------------------------------
1 | file('test.txt');
12 | $file->open('cw')->then(
13 | function (React\Stream\WritableStreamInterface $stream) {
14 | $stream->write("Hello world\n");
15 | $stream->end();
16 | echo "Data was written\n";
17 | }
18 | );
19 |
20 | $loop->run();
21 |
--------------------------------------------------------------------------------
/6-filesystem/link/remove.php:
--------------------------------------------------------------------------------
1 | getAdapter()
12 | ->unlink('dir')
13 | ->then(
14 | function () {
15 | echo 'Link removed' . PHP_EOL;
16 | },
17 | function (Exception $e) {
18 | echo $e->getMessage() . PHP_EOL;
19 | }
20 | );
21 |
22 |
23 | $loop->run();
24 |
--------------------------------------------------------------------------------
/8-child-process/process-is-already-started.php:
--------------------------------------------------------------------------------
1 | start($loop);
12 | $process->stdout->on(
13 | 'data',
14 | function ($data) {
15 | echo $data;
16 | }
17 | );
18 |
19 | $process->stdin->write("echo 'Hello World';\n");
20 |
21 | // RuntimeException will be thrown
22 | $process->start($loop);
23 | $loop->run();
24 |
25 |
--------------------------------------------------------------------------------
/6-filesystem/file/open.php:
--------------------------------------------------------------------------------
1 | file('test.txt');
12 | $file->open('r')
13 | ->then(
14 | function ($stream) {
15 | $stream->on(
16 | 'data',
17 | fn($chunk) => print 'Chunk read' . $chunk . PHP_EOL
18 | );
19 | }
20 | );
21 |
22 | $loop->run();
23 |
--------------------------------------------------------------------------------
/2-streams/spooling.php:
--------------------------------------------------------------------------------
1 | on(
15 | 'data',
16 | function ($data) use (&$spool) {
17 | $spool .= $data;
18 | }
19 | );
20 |
21 | $stream->on(
22 | 'end',
23 | function () use (&$spool) {
24 | echo $spool;
25 | }
26 | );
27 |
28 | $loop->run();
29 |
--------------------------------------------------------------------------------
/9-cancelling-promises/simple-promise.php:
--------------------------------------------------------------------------------
1 | addTimer(5, fn() => $resolve('Hello world!'));
9 | };
10 |
11 | $cancel = function (callable $resolve, callable $reject) {
12 | $reject(new \Exception('Promise cancelled!'));
13 | };
14 |
15 | $promise = new React\Promise\Promise($resolve, $cancel);
16 | $promise->then(fn($value) => print $value);
17 |
18 | $loop->run();
19 |
--------------------------------------------------------------------------------
/10-promise-stream/promise-to-stream/unwrap_readable_close.php:
--------------------------------------------------------------------------------
1 | promise());
7 |
8 | $stream->on(
9 | 'close',
10 | function () {
11 | echo 'Closed';
12 | }
13 | );
14 |
15 | $loop = \React\EventLoop\Factory::create();
16 | $stdin = new \React\Stream\ReadableResourceStream(fopen('php://stdin', 'r'), $loop);
17 | $deferred->resolve($stdin);
18 | $stdin->close();
19 |
20 | $loop->run();
21 |
--------------------------------------------------------------------------------
/6-filesystem/directory/size.php:
--------------------------------------------------------------------------------
1 | dir(__DIR__);
11 |
12 | $dir->sizeRecursive()->then(
13 | function ($size) {
14 | echo 'Directories: ' . $size['directories'] . PHP_EOL;
15 | echo 'Files: ' . $size['files'] . PHP_EOL;
16 | echo 'Bytes: ' . $size['size'] . PHP_EOL;
17 | }
18 | );
19 |
20 | $loop->run();
21 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./src/
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/2-streams/is-writable.php:
--------------------------------------------------------------------------------
1 | isWritable());
11 |
12 | $readable->on(
13 | 'data',
14 | fn($data) => $output->write($data)
15 | );
16 |
17 | $readable->on(
18 | 'end',
19 | fn() => $output->end()
20 | );
21 |
22 | $loop->run();
23 | var_dump($output->isWritable());
24 |
--------------------------------------------------------------------------------
/10-promise-stream/promise-to-stream/unwrap_readable_error.php:
--------------------------------------------------------------------------------
1 | promise());
7 |
8 | $stream->on(
9 | 'data',
10 | function ($data) {
11 | echo 'Received: ' . $data . PHP_EOL;
12 | }
13 | );
14 |
15 | $stream->on(
16 | 'error',
17 | function (Exception $error) {
18 | echo 'Error: ' . $error->getMessage() . PHP_EOL;
19 | }
20 | );
21 |
22 | $deferred->resolve('Hello!');
23 |
24 |
--------------------------------------------------------------------------------
/2-streams/writable-end.php:
--------------------------------------------------------------------------------
1 | on(
11 | 'data',
12 | fn ($data) => $output->write($data)
13 | );
14 |
15 | $readable->on(
16 | 'end',
17 | function () use ($output) {
18 | $output->end();
19 | $output->write('Hello!');
20 | }
21 | );
22 |
23 | $loop->run();
24 |
--------------------------------------------------------------------------------
/10-promise-stream/promise-to-stream/unwrap_readable_no_error.php:
--------------------------------------------------------------------------------
1 | resolve('Hello!');
7 |
8 | $stream = \React\Promise\Stream\unwrapReadable($deferred->promise());
9 |
10 | $stream->on(
11 | 'data',
12 | function ($data) {
13 | echo 'Received: ' . $data . PHP_EOL;
14 | }
15 | );
16 |
17 | $stream->on(
18 | 'error',
19 | function (Exception $error) {
20 | echo 'Error: ' . $error->getMessage() . PHP_EOL;
21 | }
22 | );
23 |
24 |
25 |
--------------------------------------------------------------------------------
/2-streams/readable-events.php:
--------------------------------------------------------------------------------
1 | on(
9 | 'data',
10 | function ($data) {
11 | echo $data, "\n";
12 | }
13 | );
14 |
15 | $stream->on(
16 | 'end',
17 | function () {
18 | echo "Finished\n";
19 | }
20 | );
21 |
22 | $stream->on(
23 | 'close',
24 | function () {
25 | echo "The stream was closed\n";
26 | }
27 | );
28 |
29 | $loop->run();
30 |
--------------------------------------------------------------------------------
/8-child-process/pipe-commands.php:
--------------------------------------------------------------------------------
1 | start($loop);
13 | $wc->start($loop);
14 |
15 | $ls->stdout->pipe($wc->stdin);
16 |
17 | $wc->stdout->on(
18 | 'data',
19 | function ($data) {
20 | echo 'Total number of files and folders: ' . $data;
21 | }
22 | );
23 |
24 | $loop->run();
25 |
26 |
--------------------------------------------------------------------------------
/4-managing-promises/any.php:
--------------------------------------------------------------------------------
1 | promise(),
10 | $secondResolver->promise(),
11 | ];
12 |
13 | $promise = \React\Promise\any($pending)->then(
14 | fn($resolved) => print $resolved . PHP_EOL
15 | );
16 |
17 | $loop = \React\EventLoop\Factory::create();
18 | $loop->addTimer(2, fn() => $firstResolver->resolve(10));
19 | $loop->addTimer(1, fn() => $secondResolver->resolve(20));
20 |
21 | $loop->run();
22 |
--------------------------------------------------------------------------------
/4-promises/resolution-forwarding.php:
--------------------------------------------------------------------------------
1 | promise()
8 | ->then(
9 | function ($data) {
10 | echo $data . PHP_EOL;
11 | return $data . ' world';
12 | }
13 | )
14 | ->then(
15 | function ($data) {
16 | echo $data . PHP_EOL;
17 | return strtoupper($data);
18 | }
19 | )->then(
20 | function ($data) {
21 | echo $data . PHP_EOL;
22 | }
23 | );
24 |
25 | $deferred->resolve('hello');
26 |
--------------------------------------------------------------------------------
/6-filesystem/directory/list.php:
--------------------------------------------------------------------------------
1 | dir(__DIR__);
11 |
12 | $dir->lsStreaming()->then(
13 | function (SplObjectStorage $nodes) {
14 | $paths = [];
15 | foreach ($nodes as $node) {
16 | $paths[] = $node->getPath();
17 | }
18 |
19 | return $paths;
20 | }
21 | )->then(
22 | function ($paths) {
23 | print_r($paths);
24 | }
25 | );
26 |
27 | $loop->run();
28 |
--------------------------------------------------------------------------------
/11-integration-with-sync-applications/request-with-sleep.php:
--------------------------------------------------------------------------------
1 | get($endpoint);
16 | $promise->then(
17 | function ($response) {
18 | echo $response->getBody();
19 | }
20 | );
21 | // run an event loop for 2 seconds
22 | Block\sleep(2, $eventLoop);
23 |
24 |
25 | // some synchronous code ...
26 |
--------------------------------------------------------------------------------
/11-integration-with-sync-applications/awaitAny.php:
--------------------------------------------------------------------------------
1 | get($endpoint1),
17 | $browser->get($endpoint2),
18 | ];
19 | $result = awaitAny($requests, $eventLoop);
20 |
21 | echo $result->getBody() . PHP_EOL;
22 | // some synchronous staff ...
23 |
--------------------------------------------------------------------------------
/5-socket/chat-server/src/Output.php:
--------------------------------------------------------------------------------
1 | colorMessage("01;31", $message);
10 | }
11 |
12 | public function info(string $message): string
13 | {
14 | return $this->colorMessage("0;32", $message);
15 | }
16 |
17 | public function message(string $text): string
18 | {
19 | return $this->colorMessage("0;36", $text);
20 | }
21 |
22 | private function colorMessage(string $hexColor, string $message): string
23 | {
24 | return "\033[{$hexColor}m{$message}\033[0m";
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/7-restulf-api-with-mysql/src/Controller/ListUsers.php:
--------------------------------------------------------------------------------
1 | users = $users;
16 | }
17 |
18 | public function __invoke(ServerRequestInterface $request)
19 | {
20 | return $this->users->all()
21 | ->then(
22 | function (array $users) {
23 | return JsonResponse::ok($users);
24 | }
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/9-cancelling-promises/cancel-simple-promise.php:
--------------------------------------------------------------------------------
1 | addTimer(
11 | 5,
12 | function () {
13 | echo "resolved\n";
14 | }
15 | );
16 | };
17 |
18 | $cancel = function (callable $resolve, callable $reject) use (&$timer) {
19 | echo "cancelled\n";
20 | };
21 |
22 | $promise = new React\Promise\Promise($resolve, $cancel);
23 |
24 | timeout($promise, 2, $loop);
25 |
26 | $loop->run();
27 |
--------------------------------------------------------------------------------
/4-managing-promises/some.php:
--------------------------------------------------------------------------------
1 | promise(),
11 | $secondResolver->promise(),
12 | $thirdResolver->promise(),
13 | ];
14 |
15 | $promise = \React\Promise\some($pending, 2)
16 | ->then(
17 | function ($resolved) {
18 | echo 'Resolved' . PHP_EOL;
19 | print_r($resolved);
20 | }
21 | );
22 |
23 | $firstResolver->resolve(1);
24 | $secondResolver->resolve(2);
25 | $thirdResolver->resolve(3);
26 |
27 |
28 |
--------------------------------------------------------------------------------
/4-managing-promises/race.php:
--------------------------------------------------------------------------------
1 | promise(),
10 | $secondResolver->promise(),
11 | ];
12 |
13 | $promise = \React\Promise\race($pending)
14 | ->then(
15 | fn($resolved) => print 'Resolved with: ' . $resolved . PHP_EOL,
16 | fn($reason) => print 'Failed with: ' . $reason . PHP_EOL
17 | );
18 |
19 | $loop = \React\EventLoop\Factory::create();
20 | $loop->addTimer(2, fn() => $firstResolver->resolve(10));
21 | $loop->addTimer(1, fn() => $secondResolver->reject(20));
22 |
23 | $loop->run();
24 |
--------------------------------------------------------------------------------
/4-promises/mixed-forwarding.php:
--------------------------------------------------------------------------------
1 | promise()
8 | ->then(
9 | function ($data) {
10 | echo $data . PHP_EOL;
11 | return $data . ' world';
12 | }
13 | )
14 | ->then(
15 | function ($data) {
16 | throw new Exception('error: ' . $data);
17 | }
18 | )
19 | ->otherwise(
20 | function (Exception $e) {
21 | return $e->getMessage();
22 | }
23 | )
24 | ->then(
25 | function ($data) {
26 | echo $data . PHP_EOL;
27 | }
28 | );
29 |
30 | $deferred->resolve('hello');
31 |
--------------------------------------------------------------------------------
/9-cancelling-promises/cancel-and-stop-timer.php:
--------------------------------------------------------------------------------
1 | addTimer(5, fn() => print 'resolved');
13 | };
14 |
15 | $cancel = function (callable $resolve, callable $reject) use (&$timer) {
16 | echo "cancelled\n";
17 | /** @var TimerInterface $timer */
18 | $timer->cancel();
19 | };
20 |
21 | $promise = new React\Promise\Promise($resolve, $cancel);
22 |
23 | timeout($promise, 2, $loop);
24 |
25 | $loop->run();
26 |
--------------------------------------------------------------------------------
/4-promises/rejection-forwarding.php:
--------------------------------------------------------------------------------
1 | promise()
8 | ->otherwise(
9 | function ($data) {
10 | echo $data . PHP_EOL;
11 | throw new Exception('some ' . $data);
12 | }
13 | )
14 | ->otherwise(
15 | function (\Exception $e) {
16 | $message = $e->getMessage();
17 | echo $message . PHP_EOL;
18 | throw new Exception(strtoupper($message));
19 | }
20 | )
21 | ->otherwise(
22 | function (\Exception $e) {
23 | echo $e->getMessage() . PHP_EOL;
24 | }
25 | );
26 |
27 | $deferred->reject('error');
28 |
--------------------------------------------------------------------------------
/5-socket/client.php:
--------------------------------------------------------------------------------
1 | connect('127.0.0.1:8080')
15 | ->then(
16 | fn (ConnectionInterface $conn) => $input->pipe($conn)->pipe($output),
17 | function (Exception $exception) use ($loop) {
18 | echo "Cannot connect to server: " . $exception->getMessage();
19 | $loop->stop();
20 | }
21 | );
22 |
23 | $loop->run();
24 |
25 |
--------------------------------------------------------------------------------
/11-integration-with-sync-applications/await-reject.php:
--------------------------------------------------------------------------------
1 | get($endpoint), $eventLoop);
19 | echo $result->getBody() . PHP_EOL;
20 | } catch (Exception $exception) {
21 | // promise rejected with $exception
22 | echo 'ERROR: ' . $exception->getMessage();
23 | }
24 |
25 | // some synchronous staff ...
26 |
--------------------------------------------------------------------------------
/2-streams/is-readable.php:
--------------------------------------------------------------------------------
1 | isReadable());
10 |
11 | $stream->on(
12 | 'data',
13 | function ($data) {
14 | // process received data
15 | }
16 | );
17 |
18 | $stream->on(
19 | 'end',
20 | function () use ($stream) {
21 | echo "End\n";
22 | var_dump($stream->isReadable());
23 | }
24 | );
25 |
26 | $stream->on(
27 | 'close',
28 | function () use ($stream) {
29 | echo "Close\n";
30 | var_dump($stream->isReadable());
31 | }
32 | );
33 |
34 | $loop->run();
35 |
--------------------------------------------------------------------------------
/11-integration-with-sync-applications/awaitAll.php:
--------------------------------------------------------------------------------
1 | get($endpoint1),
20 | $browser->get($endpoint2),
21 | ];
22 | $results = awaitAll($requests, $eventLoop);
23 |
24 | echo $results[0]->getBody() . PHP_EOL; // result for $endpoint1
25 | echo $results[1]->getBody() . PHP_EOL; // result for $endpoint2
26 | // some synchronous staff ...
27 |
--------------------------------------------------------------------------------
/7-restulf-api-with-mysql/src/Auth.php:
--------------------------------------------------------------------------------
1 | basicAuth = new PSR15Middleware(
18 | $loop,
19 | \Middlewares\BasicAuthentication::class,
20 | [$credentials]
21 | );
22 | }
23 |
24 | public function __invoke(ServerRequestInterface $request, callable $next)
25 | {
26 | return call_user_func($this->basicAuth, $request, $next);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/10-promise-stream/stream-to-promise/buffer.php:
--------------------------------------------------------------------------------
1 | then('trim')
17 | ->then(
18 | function ($string) {
19 | return str_replace(' ', '-', $string);
20 | }
21 | )
22 | ->then('strtolower')
23 | ->then('var_dump')
24 | ->then(
25 | function () {
26 | echo 'Done' . PHP_EOL;
27 | }
28 | );
29 |
30 | $loop->run();
31 |
--------------------------------------------------------------------------------
/8-child-process/pid.php:
--------------------------------------------------------------------------------
1 | start($loop);
12 | $process->stdout->on(
13 | 'data',
14 | function ($data) {
15 | echo $data;
16 | }
17 | );
18 |
19 | $process->on(
20 | 'exit',
21 | function ($exitCode, $termSignal) use ($loop) {
22 | echo "Process exited with signal: $termSignal";
23 | $loop->stop();
24 | }
25 | );
26 |
27 | $loop->addTimer(
28 | 3,
29 | function () use ($process, $loop) {
30 | $pid = $process->getPid();
31 | echo "Sending KILL signal to PID: $pid\n";
32 | (new Process("kill {$pid}"))->start($loop);
33 | }
34 | );
35 | $loop->run();
36 |
37 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": {
3 | "react/event-loop": "^1.1",
4 | "react/promise": "^2.8",
5 | "react/socket": "^1.5",
6 | "react/datagram": "^1.5",
7 | "react/http": "^1.0",
8 | "react/child-process": "^0.6",
9 | "react/promise-timer": "^1.6",
10 | "clue/block-react": "^1.2",
11 | "phpunit/phpunit": "^8.5",
12 | "react/promise-stream": "^1.2",
13 | "middlewares/client-ip": "^0.7.0",
14 | "middlewares/response-time": "^0.5.0",
15 | "middlewares/redirect": "^0.3.0",
16 | "middlewares/utils": "^0.14.0",
17 | "react/filesystem": "^0.1",
18 | "symfony/dom-crawler": "^3.4",
19 | "symfony/css-selector": "^3.4",
20 | "clue/mq-react": "^1.2",
21 | "react/dns": "^1.3",
22 | "react/cache": "^1.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/7-restulf-api-with-mysql/requests.http:
--------------------------------------------------------------------------------
1 | # Hello wolrd
2 |
3 | GET http://127.0.0.1:8000
4 |
5 | ###
6 |
7 | # Get all users
8 | GET http://127.0.0.1:8000/users
9 |
10 | ###
11 |
12 | # Create a user
13 | POST http://127.0.0.1:8000/users
14 | Content-Type: application/json
15 |
16 | {
17 | "name": "Mike",
18 | "email": "mike@exmaple.com"
19 | }
20 |
21 | ###
22 |
23 | # Get a user
24 | GET http://127.0.0.1:8000/users/1
25 |
26 | ###
27 |
28 | # Update a user's name
29 | PUT http://127.0.0.1:8000/users/1
30 | Content-Type: application/json
31 |
32 | {
33 | "name": "New name"
34 | }
35 |
36 | ###
37 |
38 | # Delete a user
39 | DELETE http://127.0.0.1:8000/users/1
40 | Content-Type: application/json
41 |
42 | ###
43 |
44 | # Get all users with auth
45 | GET http://127.0.0.1:8000/users
46 | Authorization: Basic dXNlcjpzZWNyZXQ= // user:secret
47 |
48 | ###
49 |
--------------------------------------------------------------------------------
/7-restulf-api-with-mysql/src/Controller/DeleteUser.php:
--------------------------------------------------------------------------------
1 | users = $users;
17 | }
18 |
19 | public function __invoke(ServerRequestInterface $request, string $id)
20 | {
21 | return $this->users->delete($id)
22 | ->then(
23 | function () {
24 | return JsonResponse::noContent();
25 | },
26 | function (UserNotFoundError $error) {
27 | return JsonResponse::notFound($error->getMessage());
28 | }
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/7-restulf-api-with-mysql/src/Controller/ViewUser.php:
--------------------------------------------------------------------------------
1 | users = $users;
17 | }
18 |
19 | public function __invoke(ServerRequestInterface $request, string $id)
20 | {
21 | return $this->users->find($id)
22 | ->then(
23 | function (array $user) {
24 | return JsonResponse::ok($user);
25 | },
26 | function (UserNotFoundError $error) {
27 | return JsonResponse::notFound($error->getMessage());
28 | }
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/4-promises/rejection-forwarding-typehints.php:
--------------------------------------------------------------------------------
1 | promise()
8 | ->otherwise(
9 | function ($data) {
10 | echo $data . PHP_EOL;
11 |
12 | throw new InvalidArgumentException('some ' . $data);
13 | }
14 | )
15 | ->otherwise(
16 | function (InvalidArgumentException $e) {
17 | $message = $e->getMessage();
18 | echo $message . PHP_EOL;
19 |
20 | throw new BadFunctionCallException(strtoupper($message));
21 | }
22 | )
23 | ->otherwise(
24 | function (InvalidArgumentException $e) { // <-- This handler will be skipped
25 | echo $e->getMessage() . PHP_EOL; // because in the previous promise
26 | }
27 | ); // we have thrown a LogicException
28 |
29 | $deferred->reject('error');
30 |
--------------------------------------------------------------------------------
/7-restulf-api-with-mysql/src/Controller/CreateUser.php:
--------------------------------------------------------------------------------
1 | users = $users;
17 | }
18 |
19 | public function __invoke(ServerRequestInterface $request)
20 | {
21 | $user = json_decode((string)$request->getBody(), true);
22 | $name = $user['name'] ?? '';
23 | $email = $user['email'] ?? '';
24 |
25 | return $this->users->create($name, $email)
26 | ->then(
27 | function () {
28 | return JsonResponse::created();
29 | },
30 | function (Exception $error) {
31 | return JsonResponse::badRequest($error->getMessage());
32 | }
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/12-promise-testing/Mocks/PromiseRejectsTest.php:
--------------------------------------------------------------------------------
1 | reject('value');
16 |
17 | $this->assertPromiseRejects($deferred->promise());
18 | }
19 |
20 | public function assertPromiseRejects(PromiseInterface $promise): void
21 | {
22 | $promise->then(null, $this->assertCallableCalledOnce());
23 | }
24 |
25 | public function assertCallableCalledOnce(): callable
26 | {
27 | $mock = $this->getMockBuilder(CallableStub::class)->getMock();
28 | $mock->expects($this->once())->method('__invoke');
29 |
30 | return $mock;
31 | }
32 | }
33 |
34 | class CallableStub
35 | {
36 | public function __invoke()
37 | {
38 |
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/7-restulf-api-with-mysql/src/JsonResponse.php:
--------------------------------------------------------------------------------
1 | 'application/json'], $body);
14 | }
15 |
16 | public static function ok($data = null): Response
17 | {
18 | return self::response(200, $data);
19 | }
20 |
21 | public static function noContent(): Response
22 | {
23 | return self::response(204);
24 | }
25 |
26 | public static function created(): Response
27 | {
28 | return self::response(201);
29 | }
30 |
31 | public static function badRequest(string $error): Response
32 | {
33 | return self::response(400, ['error' => $error]);
34 | }
35 |
36 | public static function notFound(string $error): Response
37 | {
38 | return self::response(404, ['error' => $error]);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/12-promise-testing/Mocks/PromiseFulfillsTest.php:
--------------------------------------------------------------------------------
1 | resolve();
16 |
17 | $this->assertPromiseFulfills($deferred->promise());
18 | }
19 |
20 | /**
21 | * @param PromiseInterface $promise
22 | */
23 | public function assertPromiseFulfills(PromiseInterface $promise): void
24 | {
25 | $promise->then($this->assertCallableCalledOnce());
26 | }
27 |
28 | public function assertCallableCalledOnce(): callable
29 | {
30 | $mock = $this->getMockBuilder(CallableStub::class)->getMock();
31 | $mock->expects(self::once())->method('__invoke');
32 |
33 | return $mock;
34 | }
35 | }
36 |
37 | class CallableStub
38 | {
39 | public function __invoke()
40 | {
41 |
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/10-promise-stream/stream-to-promise/spooling_problem.php:
--------------------------------------------------------------------------------
1 | on(
28 | 'data',
29 | function ($data) use (&$spool) {
30 | $spool .= $data;
31 | }
32 | );
33 |
34 | $stream->on(
35 | 'end',
36 | function () use (&$spool) {
37 | echo $spool;
38 | // ???
39 | }
40 | );
41 | }
42 | }
43 |
44 | $loop = \React\EventLoop\Factory::create();
45 |
46 | (new Provider())->get('file.txt', $loop);
47 |
48 | $loop->run();
49 |
--------------------------------------------------------------------------------
/12-promise-testing/Mocks/PromiseFulfillsWithTest.php:
--------------------------------------------------------------------------------
1 | resolve('bar');
16 |
17 | $this->assertPromiseFulfillsWith($deferred->promise(), 'bar');
18 | }
19 |
20 | /**
21 | * @param mixed $value
22 | */
23 | public function assertPromiseFulfillsWith(PromiseInterface $promise, $value): void
24 | {
25 | $promise->then($this->assertCallableCalledOnceWith($value));
26 | }
27 |
28 | /**
29 | * @param mixed $value
30 | */
31 | public function assertCallableCalledOnceWith($value): callable
32 | {
33 | $mock = $this->getMockBuilder(CallableStub::class)->getMock();
34 | $mock->expects($this->once())->method('__invoke')->with($value);
35 |
36 | return $mock;
37 | }
38 | }
39 |
40 | class CallableStub
41 | {
42 | public function __invoke()
43 | {
44 |
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/12-promise-testing/Mocks/PromiseRejectsWithTest.php:
--------------------------------------------------------------------------------
1 | reject(new \LogicException());
16 |
17 | $this->assertPromiseRejectsWith($deferred->promise(), \LogicException::class);
18 | }
19 |
20 | public function assertPromiseRejectsWith(
21 | PromiseInterface $promise,
22 | string $exceptionClass
23 | ): void {
24 | $promise->then(null, $this->assertCallableCalledOnceWithObject($exceptionClass));
25 | }
26 |
27 | public function assertCallableCalledOnceWithObject(string $className): callable
28 | {
29 | $mock = $this->getMockBuilder(CallableStub::class)->getMock();
30 | $mock->expects($this->once())->method('__invoke')->with($this->isInstanceOf($className));
31 |
32 | return $mock;
33 | }
34 | }
35 |
36 | class CallableStub
37 | {
38 | public function __invoke()
39 | {
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/7-restulf-api-with-mysql/src/Controller/UpdateUser.php:
--------------------------------------------------------------------------------
1 | users = $users;
17 | }
18 |
19 | public function __invoke(ServerRequestInterface $request, string $id)
20 | {
21 | $name = $this->extractName($request);
22 | if (empty($name)) {
23 | return JsonResponse::badRequest('"name" field is required');
24 | }
25 |
26 | return $this->users->update($id, $name)
27 | ->then(
28 | function () {
29 | return JsonResponse::noContent();
30 | },
31 | function (UserNotFoundError $error) {
32 | return JsonResponse::notFound($error->getMessage());
33 | }
34 | );
35 | }
36 |
37 | private function extractName(ServerRequestInterface $request): ?string
38 | {
39 | $params = json_decode((string)$request->getBody(), true);
40 | return $params['name'] ?? null;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/10-promise-stream/stream-to-promise/spooling_with_promise.php:
--------------------------------------------------------------------------------
1 | then('trim')
14 | ->then(
15 | function ($string) {
16 | return str_replace(' ', '-', $string);
17 | }
18 | )
19 | ->then('strtolower');
20 | }
21 | }
22 |
23 | final class Provider
24 | {
25 | public function get(string $path, LoopInterface $loop): PromiseInterface
26 | {
27 | $stream = new ReadableResourceStream(
28 | fopen($path, 'r'), $loop
29 | );
30 |
31 | return \React\Promise\Stream\buffer($stream);
32 | }
33 | }
34 |
35 | $loop = \React\EventLoop\Factory::create();
36 |
37 | $processor = new Processor();
38 | $provider = new Provider();
39 |
40 | $processor
41 | ->process($provider->get('file.txt', $loop))
42 | ->then(
43 | function ($data) {
44 | echo $data . PHP_EOL;
45 | echo 'Done' . PHP_EOL;
46 | }
47 | );
48 |
49 | $loop->run();
50 |
--------------------------------------------------------------------------------
/7-restulf-api-with-mysql/src/Router.php:
--------------------------------------------------------------------------------
1 | dispatcher = new GroupCountBased($routes->getData());
19 | }
20 |
21 | public function __invoke(ServerRequestInterface $request)
22 | {
23 | $routeInfo = $this->dispatcher->dispatch(
24 | $request->getMethod(),
25 | $request->getUri()->getPath()
26 | );
27 |
28 | switch ($routeInfo[0]) {
29 | case Dispatcher::NOT_FOUND:
30 | return new Response(404, ['Content-Type' => 'text/plain'], 'Not found');
31 | case Dispatcher::METHOD_NOT_ALLOWED:
32 | return new Response(405, ['Content-Type' => 'text/plain'], 'Method not allowed');
33 | case Dispatcher::FOUND:
34 | $params = $routeInfo[2];
35 | return $routeInfo[1]($request, ... array_values($params));
36 | }
37 |
38 | throw new LogicException('Something went wrong in routing.');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/10-promise-stream/stream-to-promise/spooling_with_promise_chunks.php:
--------------------------------------------------------------------------------
1 | then(
14 | function (array $chunks) {
15 | echo 'Total chunks: ' . count($chunks) . PHP_EOL;
16 | foreach ($chunks as $index => $chunk) {
17 | echo 'Chunk ' . ($index + 1) . ': ' . $chunk . PHP_EOL;
18 | }
19 | }
20 | );
21 | }
22 | }
23 |
24 | final class Provider
25 | {
26 | public function get(string $path, LoopInterface $loop): PromiseInterface
27 | {
28 | $stream = new ReadableResourceStream(
29 | fopen($path, 'r'), $loop
30 | );
31 |
32 | return \React\Promise\Stream\all($stream);
33 | }
34 | }
35 |
36 | $loop = \React\EventLoop\Factory::create();
37 |
38 | $processor = new Processor();
39 | $provider = new Provider();
40 |
41 | $processor->process($provider->get('file.txt', $loop))
42 | ->then(
43 | function () {
44 | echo 'Done' . PHP_EOL;
45 | }
46 | );
47 |
48 | $loop->run();
49 |
--------------------------------------------------------------------------------
/12-promise-testing/Waiting/PromiseRejectsTest.php:
--------------------------------------------------------------------------------
1 | loop = Factory::create();
22 | parent::setUp();
23 | }
24 |
25 | /** @test */
26 | public function a_promise_rejects()
27 | {
28 | $deferred = new Deferred();
29 | $deferred->resolve();
30 |
31 | $this->assertPromiseRejects($deferred->promise());
32 | }
33 |
34 |
35 | /**
36 | * @param PromiseInterface $promise
37 | * @param int|null $timeout seconds to wait for resolving
38 | * @return mixed
39 | */
40 | public function assertPromiseRejects(PromiseInterface $promise, $timeout = null)
41 | {
42 | try {
43 | $this->addToAssertionCount(1);
44 | Block\await($promise, $this->loop, $timeout ? : self::DEFAULT_TIMEOUT);
45 | } catch (Exception $exception) {
46 | return $exception;
47 | }
48 | $this->fail('Failed asserting that promise rejects. Promise was fulfilled.');
49 | }
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/9-cancelling-promises/timeout-promise-wrapper.php:
--------------------------------------------------------------------------------
1 | addTimer(
13 | 5,
14 | function () use ($resolve) {
15 | $resolve();
16 | }
17 | );
18 | };
19 |
20 | $cancel = function (callable $resolve, callable $reject) use (&$timer) {
21 | echo "Principal promise: cancelled\n";
22 | /** @var \React\EventLoop\Timer\TimerInterface $timer */
23 | $timer->cancel();
24 | };
25 |
26 | $promise = new React\Promise\Promise($resolve, $cancel);
27 |
28 | timeout($promise, 2, $loop)
29 | ->then(
30 | function () {
31 | // the principal promise resolved in 2 seconds
32 | echo "Timeout promise: Resolved before timeout.\n";
33 | }
34 | )
35 | ->otherwise(
36 | function (TimeoutException $exception) {
37 | // the principal promise cancelled due to a timeout
38 | echo "Timeout promise: Failed due to a timeout.\n";
39 | }
40 | )
41 | ->otherwise(
42 | function () {
43 | // the principal promise failed
44 | echo "Timeout promise: Failed to some error.\n";
45 | }
46 | );
47 |
48 |
49 | $loop->run();
50 |
--------------------------------------------------------------------------------
/12-promise-testing/Waiting/PromiseFulfillsTest.php:
--------------------------------------------------------------------------------
1 | loop = Factory::create();
23 | parent::setUp();
24 | }
25 |
26 | /** @test */
27 | public function a_promise_fulfills(): void
28 | {
29 | $deferred = new Deferred();
30 | $deferred->resolve();
31 |
32 | $this->assertPromiseFulfills($deferred->promise());
33 | }
34 |
35 |
36 | public function assertPromiseFulfills(PromiseInterface $promise, int $timeout = null)
37 | {
38 | $failMessage = 'Failed asserting that promise fulfills. ';
39 | try {
40 | Block\await($promise, $this->loop, $timeout ? : self::DEFAULT_TIMEOUT);
41 | } catch (TimeoutException $exception) {
42 | $this->fail($failMessage . 'Promise was rejected by timeout.');
43 | } catch (Exception $exception) {
44 | $this->fail($failMessage . 'Promise was rejected.');
45 | }
46 | $this->addToAssertionCount(1);
47 | }
48 | }
49 |
50 |
--------------------------------------------------------------------------------
/7-restulf-api-with-mysql/index.php:
--------------------------------------------------------------------------------
1 | createLazyConnection('root:@localhost/reactphp-users');
22 | $users = new Users($db);
23 |
24 | $routes = new RouteCollector(new Std(), new GroupCountBased());
25 | $routes->get('/users', new ListUsers($users));
26 | $routes->post('/users', new CreateUser($users));
27 | $routes->get('/users/{id}', new ViewUser($users));
28 | $routes->put('/users/{id}', new UpdateUser($users));
29 | $routes->delete('/users/{id}', new DeleteUser($users));
30 |
31 | $server = new Server(
32 | $loop,
33 | new Auth($loop, ['user' => 'secret']),
34 | new Router($routes),
35 | );
36 |
37 | $socket = new \React\Socket\Server('127.0.0.1:8000', $loop);
38 | $server->listen($socket);
39 |
40 | $server->on(
41 | 'error',
42 | function (Exception $exception) {
43 | echo $exception->getMessage() . PHP_EOL;
44 | }
45 | );
46 |
47 | echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . "\n";
48 |
49 | $loop->run();
50 |
--------------------------------------------------------------------------------
/10-promise-stream/stream-to-promise/spooling_with_promise_collect_error.php:
--------------------------------------------------------------------------------
1 | then(
14 | function (Exception $error) {
15 | echo 'Error ' . $error->getMessage() . PHP_EOL;
16 | }
17 | );
18 | }
19 | }
20 |
21 | final class Provider
22 | {
23 | private ReadableResourceStream $stream;
24 |
25 | public function __construct(string $path, LoopInterface $loop)
26 | {
27 | $this->stream = new ReadableResourceStream(
28 | fopen($path, 'r'), $loop
29 | );
30 | }
31 |
32 | public function getData(): PromiseInterface
33 | {
34 | return \React\Promise\Stream\buffer($this->stream);
35 | }
36 |
37 | public function getFirstError(): PromiseInterface
38 | {
39 | $promise = \React\Promise\Stream\first($this->stream, 'error');
40 | $this->stream->emit(
41 | 'error',
42 | [new Exception('Something went wrong')]
43 | );
44 |
45 | return $promise;
46 | }
47 | }
48 |
49 | $loop = \React\EventLoop\Factory::create();
50 |
51 | $logger = new Logger();
52 | $provider = new Provider('file.txt', $loop);
53 |
54 | $logger->log($provider->getFirstError());
55 |
56 | $loop->run();
57 |
--------------------------------------------------------------------------------
/12-promise-testing/Waiting/PromiseFulfillsWithTest.php:
--------------------------------------------------------------------------------
1 | loop = Factory::create();
23 | parent::setUp();
24 | }
25 |
26 | /** @test */
27 | public function a_promise_fulfills_with_a_value(): void
28 | {
29 | $deferred = new Deferred();
30 | $deferred->resolve('foo');
31 |
32 | $this->assertPromiseFulfillsWith($deferred->promise(), 'foo');
33 | }
34 |
35 | public function assertPromiseFulfillsWith(
36 | PromiseInterface $promise,
37 | $expectedValue,
38 | int $timeout = null
39 | ): void {
40 | $failMessage = 'Failed asserting that promise fulfills with a specified value. ';
41 | try {
42 | $fulfilledValue = Block\await($promise, $this->loop, $timeout ?: self::DEFAULT_TIMEOUT);
43 | } catch (TimeoutException $exception) {
44 | $this->fail($failMessage . 'Promise was rejected by timeout.');
45 | } catch (Exception $exception) {
46 | $this->fail($failMessage . 'Promise was rejected.');
47 | }
48 |
49 | $this->assertEquals($expectedValue, $fulfilledValue, $failMessage);
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/5-socket/chat-server/src/Connections.php:
--------------------------------------------------------------------------------
1 | connections = new SplObjectStorage();
18 | }
19 |
20 | public function add(ConnectionInterface $connection, string $name): void
21 | {
22 | $this->connections->offsetSet($connection, $name);
23 | }
24 |
25 | public function hasWithUsername(string $name): bool
26 | {
27 | return $this->getByUserName($name) !== null;
28 | }
29 |
30 | public function getByUsername(string $name): ?ConnectionInterface
31 | {
32 | /** @var ConnectionInterface $connection */
33 | foreach ($this->connections as $connection) {
34 | $takenName = $this->connections->offsetGet($connection);
35 | if ($takenName === $name) {
36 | return $connection;
37 | }
38 | }
39 |
40 | return null;
41 | }
42 |
43 | public function getUsername(ConnectionInterface $connection): string
44 | {
45 | return $this->connections->offsetGet($connection);
46 | }
47 |
48 | public function exists(ConnectionInterface $connection): bool
49 | {
50 | return $this->connections->offsetExists($connection);
51 | }
52 |
53 | public function remove(ConnectionInterface $connection): void
54 | {
55 | $this->connections->detach($connection);
56 | }
57 |
58 | public function getIterator()
59 | {
60 | return $this->connections;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/5-socket/chat-server/src/Message.php:
--------------------------------------------------------------------------------
1 | from = $from;
20 |
21 | preg_match('/^@(\w+):\s*(.+)/s', $text, $matches);
22 | if (!empty($matches)) {
23 | $this->to = $matches[1];
24 | $this->text = $matches[2];
25 | } else {
26 | $this->text = $text;
27 | }
28 | }
29 |
30 | public static function from(string $text, string $from): self
31 | {
32 | return new self($text, $from);
33 | }
34 |
35 | public static function warning(string $text): self
36 | {
37 | return self::colored($text, self::COLOR_RED);
38 | }
39 |
40 | public static function info(string $text): self
41 | {
42 | return self::colored($text, self::COLOR_GREEN);
43 | }
44 |
45 | private static function colored(string $text, string $hexColor): self
46 | {
47 | return new self(self::coloredString($text, $hexColor));
48 | }
49 |
50 | private static function coloredString(string $text, string $hexColor): string
51 | {
52 | return "\033[{$hexColor}m{$text}\033[0m";
53 | }
54 |
55 | public function toString(): string
56 | {
57 | if ($this->from === null) {
58 | return $this->text;
59 | }
60 |
61 | $text = "{$this->from}: {$this->text}";
62 | if ($this->to !== null) {
63 | $text = self::coloredString($text, self::COLOR_CYAN);
64 | }
65 |
66 | return $text;
67 | }
68 |
69 | public function isPublic(): bool
70 | {
71 | return $this->to === null;
72 | }
73 |
74 | public function to(): ?string
75 | {
76 | return $this->to;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/7-restulf-api-with-mysql/src/Users.php:
--------------------------------------------------------------------------------
1 | db = $db;
16 | }
17 |
18 | public function all(): PromiseInterface
19 | {
20 | return $this->db
21 | ->query('SELECT id, name, email FROM users ORDER BY id')
22 | ->then(fn(QueryResult $queryResult) => $queryResult->resultRows);
23 | }
24 |
25 | public function find(string $id): PromiseInterface
26 | {
27 | return $this->db->query('SELECT id, name, email FROM users WHERE id = ?', [$id])
28 | ->then(
29 | function (QueryResult $result) {
30 | if (empty($result->resultRows)) {
31 | throw new UserNotFoundError();
32 | }
33 |
34 | return $result->resultRows[0];
35 | }
36 | );
37 | }
38 |
39 | public function update(string $id, string $newName): PromiseInterface
40 | {
41 | return $this->find($id)
42 | ->then(
43 | function () use ($id, $newName) {
44 | $this->db->query('UPDATE users SET name = ? WHERE id = ?', [$newName, $id]);
45 | }
46 | );
47 | }
48 |
49 | public function delete(string $id): PromiseInterface
50 | {
51 | return $this->db
52 | ->query('DELETE FROM users WHERE id = ?', [$id])
53 | ->then(
54 | function (QueryResult $result) {
55 | if ($result->affectedRows === 0) {
56 | throw new UserNotFoundError();
57 | }
58 | }
59 | );
60 | }
61 |
62 | public function create(string $name, string $email): PromiseInterface
63 | {
64 | return $this->db->query('INSERT INTO users(name, email) VALUES (?, ?)', [$name, $email]);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/12-promise-testing/Waiting/PromiseRejectsWithTest.php:
--------------------------------------------------------------------------------
1 | loop = Factory::create();
24 | parent::setUp();
25 | }
26 |
27 | /** @test */
28 | public function a_promise_rejects_with_a_reason()
29 | {
30 | $deferred = new Deferred();
31 | $deferred->reject(new \LogicException());
32 |
33 | $this->assertPromiseRejectsWith($deferred->promise(), \InvalidArgumentException::class);
34 | }
35 |
36 | /**
37 | * @param PromiseInterface $promise
38 | * @param string $reasonExceptionClass
39 | * @param int|null $timeout seconds to wait for resolving
40 | * @return void
41 | */
42 | public function assertPromiseRejectsWith(PromiseInterface $promise, $reasonExceptionClass, $timeout = null)
43 | {
44 | $reason = $this->assertPromiseRejects($promise, $timeout);
45 | $this->assertInstanceOf(
46 | $reasonExceptionClass,
47 | $reason,
48 | 'Failed asserting that promise rejects with a specified reason.'
49 | );
50 | }
51 |
52 | /**
53 | * @param PromiseInterface $promise
54 | * @param int|null $timeout seconds to wait for resolving
55 | * @return mixed
56 | */
57 | public function assertPromiseRejects(PromiseInterface $promise, $timeout = null)
58 | {
59 | try {
60 | $this->addToAssertionCount(1);
61 | Block\await($promise, $this->loop, $timeout ? : self::DEFAULT_TIMEOUT);
62 | } catch (Exception $exception) {
63 | return $exception;
64 | }
65 | $this->fail('Failed asserting that promise rejects. Promise was fulfilled.');
66 | }
67 | }
68 |
69 |
--------------------------------------------------------------------------------
/5-socket/chat-server/src/Server.php:
--------------------------------------------------------------------------------
1 | socket = $socket;
18 | $this->connections = $connections;
19 | }
20 |
21 | public function run(): void
22 | {
23 | $this->socket->on(
24 | 'connection',
25 | function (ConnectionInterface $connection) {
26 | $connection->write("Enter your name: ");
27 | $this->subscribeToEvents($connection);
28 | }
29 | );
30 | }
31 |
32 | private function subscribeToEvents(ConnectionInterface $connection): void
33 | {
34 | $connection->on(
35 | 'data',
36 | fn($data) => $this->onDataReceived($connection, $data)
37 | );
38 |
39 | $connection->on(
40 | 'close',
41 | fn() => $this->onUserLeaves($connection)
42 | );
43 | }
44 |
45 | private function onDataReceived(ConnectionInterface $connection, string $data): void
46 | {
47 | if ($this->connections->exists($connection)) {
48 | $this->sendChatMessage($connection, $data);
49 | return;
50 | }
51 |
52 | $this->addNewMember($data, $connection);
53 | }
54 |
55 | private function addNewMember(string $username, ConnectionInterface $connection): void
56 | {
57 | $username = trim($username);
58 | if ($this->connections->hasWithUsername($username)) {
59 | $data = Message::warning("Name $username is already taken!\n")->toString();
60 | $connection->write($data);
61 | $connection->write("Enter your name: ");
62 | return;
63 | }
64 |
65 | $message = Message::info("$username enters the chat\n");
66 | $this->broadcast($message, $connection);
67 | $this->connections->add($connection, $username);
68 | }
69 |
70 | private function onUserLeaves(ConnectionInterface $connection): void
71 | {
72 | $username = $this->connections->getUsername($connection);
73 | $this->connections->remove($connection);
74 | $message = Message::info("$username leaves the chat\n");
75 | $this->broadcast($message);
76 | }
77 |
78 | /**
79 | * Send data to all connections from the
80 | * pool except the specified one.
81 | */
82 | private function broadcast(Message $message, ConnectionInterface $except = null): void
83 | {
84 | foreach ($this->connections as $conn) {
85 | if ($conn !== $except) {
86 | $conn->write($message->toString());
87 | }
88 | }
89 | }
90 |
91 | private function sendChatMessage(ConnectionInterface $connection, string $data): void
92 | {
93 | $username = $this->connections->getUsername($connection);
94 | $message = Message::from($data, $username);
95 | if ($message->isPublic()) {
96 | $this->broadcast($message, $connection);
97 | return;
98 | }
99 |
100 | $this->sendPrivateMessage($message, $connection);
101 | }
102 |
103 | private function sendPrivateMessage(Message $message, ConnectionInterface $connection): void
104 | {
105 | $to = $this->connections->getByUserName($message->to());
106 | if ($to !== null) {
107 | $to->write($message->toString());
108 | return;
109 | }
110 |
111 | $warning = Message::warning(
112 | "User {$message->to()} not found."
113 | );
114 | $connection->write($warning->toString());
115 | }
116 | }
117 |
--------------------------------------------------------------------------------