├── .gitignore ├── 1-timers ├── blocked_loop.php ├── check_active.php ├── controlling_by_counter.php ├── controlling_by_timer.php ├── one-off.php └── periodic.php ├── 10-promise-stream ├── promise-to-stream │ ├── unwrap_readable.php │ ├── unwrap_readable_close.php │ ├── unwrap_readable_error.php │ ├── unwrap_readable_no_error.php │ └── unwrap_writable.php └── stream-to-promise │ ├── buffer.php │ ├── file.txt │ ├── spooling_problem.php │ ├── spooling_with_promise.php │ ├── spooling_with_promise_chunks.php │ └── spooling_with_promise_collect_error.php ├── 11-integration-with-sync-applications ├── await-reject.php ├── await.php ├── awaitAll.php ├── awaitAny.php └── request-with-sleep.php ├── 12-promise-testing ├── Mocks │ ├── PromiseFulfillsTest.php │ ├── PromiseFulfillsWithTest.php │ ├── PromiseRejectsTest.php │ └── PromiseRejectsWithTest.php └── Waiting │ ├── PromiseFulfillsTest.php │ ├── PromiseFulfillsWithTest.php │ ├── PromiseRejectsTest.php │ └── PromiseRejectsWithTest.php ├── 2-streams ├── composite.php ├── duplex.php ├── file.txt ├── is-readable.php ├── is-writable.php ├── piping.php ├── read-file.php ├── readable-events.php ├── reading-control.php ├── spooling.php ├── through.php ├── writable-end.php ├── writable-events-1.php ├── writable-events-2.php └── writable.php ├── 3-ticks ├── all.php ├── future-closure.php ├── future-resucrsively.php └── future.php ├── 4-managing-promises ├── all.php ├── any.php ├── race.php ├── reject.php ├── resolve.php └── some.php ├── 4-promises ├── mixed-forwarding.php ├── promise-done-fatal.php ├── promise-done-full.php ├── promise-done.php ├── promise-fail.php ├── rejection-forwarding-typehints.php ├── rejection-forwarding.php └── resolution-forwarding.php ├── 5-socket ├── chat-server │ ├── composer.json │ ├── index.php │ └── src │ │ ├── Connections.php │ │ ├── Message.php │ │ ├── Output.php │ │ └── Server.php └── client.php ├── 6-filesystem ├── adapter.php ├── directory │ ├── create.php │ ├── create_recursive.php │ ├── list.php │ ├── remove.php │ ├── size.php │ └── stat.php ├── file │ ├── chmod.php │ ├── chown.php │ ├── create.php │ ├── create_with_open.php │ ├── create_with_touch.php │ ├── dest.txt │ ├── exists.php │ ├── generate_huge_test_file.php │ ├── new.txt │ ├── open.php │ ├── read.php │ ├── remove.php │ ├── rename.php │ ├── size.php │ ├── stat.php │ ├── time.php │ ├── write_with_put_contents.php │ └── write_with_stream.php └── link │ ├── create.php │ ├── read.php │ ├── remove.php │ ├── test.txt │ └── test_link.txt ├── 7-restulf-api-with-mysql ├── composer.json ├── index.php ├── requests.http └── src │ ├── Auth.php │ ├── Controller │ ├── CreateUser.php │ ├── DeleteUser.php │ ├── ListUsers.php │ ├── UpdateUser.php │ └── ViewUser.php │ ├── JsonResponse.php │ ├── Router.php │ ├── UserNotFoundError.php │ └── Users.php ├── 8-child-process ├── input-hello-world.php ├── pid.php ├── ping-with-timer.php ├── ping.php ├── pipe-commands.php ├── pipe-io.php └── process-is-already-started.php ├── 9-cancelling-promises ├── cancel-and-stop-timer.php ├── cancel-simple-promise.php ├── simple-promise.php └── timeout-promise-wrapper.php ├── README.md ├── composer.json ├── cover.jpg └── phpunit.xml /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | -------------------------------------------------------------------------------- /1-timers/blocked_loop.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /1-timers/one-off.php: -------------------------------------------------------------------------------- 1 | addTimer(2, fn () => print "Hello world\n"); 8 | $loop->run(); 9 | 10 | echo "finished\n"; 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /10-promise-stream/stream-to-promise/file.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /11-integration-with-sync-applications/await.php: -------------------------------------------------------------------------------- 1 | get($endpoint), $eventLoop); 17 | 18 | echo $result->getBody() . PHP_EOL; 19 | // some synchronous staff ... 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2-streams/composite.php: -------------------------------------------------------------------------------- 1 | on( 12 | 'data', 13 | fn($chunk) => $composite->write('You said: ' . $chunk) 14 | ); 15 | 16 | $loop->run(); 17 | -------------------------------------------------------------------------------- /2-streams/duplex.php: -------------------------------------------------------------------------------- 1 | write('hello!'); 11 | $stream->end(); 12 | 13 | $loop->run(); 14 | -------------------------------------------------------------------------------- /2-streams/file.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2-streams/piping.php: -------------------------------------------------------------------------------- 1 | pipe($output); 11 | 12 | $loop->run(); 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /2-streams/through.php: -------------------------------------------------------------------------------- 1 | pipe($through)->pipe($output); 12 | 13 | $loop->run(); 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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-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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /5-socket/chat-server/index.php: -------------------------------------------------------------------------------- 1 | run(); 12 | 13 | echo "Listening on {$socket->getAddress()}\n"; 14 | 15 | $loop->run(); 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /6-filesystem/adapter.php: -------------------------------------------------------------------------------- 1 | getAdapter()) . PHP_EOL; 11 | 12 | $loop->run(); 13 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /6-filesystem/file/dest.txt: -------------------------------------------------------------------------------- 1 | Hello world 2 | -------------------------------------------------------------------------------- /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/generate_huge_test_file.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /6-filesystem/link/test.txt: -------------------------------------------------------------------------------- 1 | Hello world -------------------------------------------------------------------------------- /6-filesystem/link/test_link.txt: -------------------------------------------------------------------------------- 1 | test.txt -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /7-restulf-api-with-mysql/src/UserNotFoundError.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reactphp-book 2 | Examples from my [Learning Event-Driven PHP With ReactPHP](https://leanpub.com/event-driven-php) book. 3 | 4 |

5 | Learning Event-Driven PHP With ReactPHP 6 |

7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seregazhuk/reactphp-book/1092dac89c31e4f3a178d5b377e9df01c3ec5c80/cover.jpg -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./src/ 15 | 16 | 17 | 18 | --------------------------------------------------------------------------------