├── .gitignore ├── README.md ├── bin ├── chat-server-channels.php └── chat-server.php ├── composer.json └── src └── Rx └── Chat ├── Channels.php └── SocketIoObservable.php /.gitignore: -------------------------------------------------------------------------------- 1 | phpunit.xml 2 | composer.lock 3 | composer.phar 4 | /vendor/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Chat demo with Rx.PHP 2 | ===================== 3 | 4 | This repository contains a simple demo application showing some of the 5 | abilities of [Rx.PHP]. 6 | 7 | [Rx.PHP]: https://github.com/asm89/Rx.PHP 8 | 9 | To run the project: 10 | 11 | - Clone this repository 12 | - Use [composer] to install the dependencies: `composer.phar install` 13 | - Run the examples 14 | 15 | [composer]: http://getcomposer.org/ 16 | 17 | The source files in the `bin/` directory are commented in a tutorial like 18 | fashion. 19 | 20 | ## chat-server.php 21 | 22 | `chat-server.php` contains a chat application that broadcasts messages to all 23 | connected clients. To run: 24 | 25 | ```bash 26 | $ php bin/chat-server.php 27 | ``` 28 | 29 | Connect to the server multiple times to see it in action. 30 | 31 | ```bash 32 | $ telnet localhost 8080 33 | Trying 127.0.0.1... 34 | Connected to localhost. 35 | Escape character is '^]'. 36 | \o/ 37 | ``` 38 | 39 | ## chat-server-channels.php 40 | 41 | `chat-server-channels.php` builds on the other version, but now extending the 42 | functionality of the server with simple channels and commands to join, part and 43 | send messages to channels. 44 | 45 | Commands: 46 | 47 | ``` 48 | join #name 49 | part #name 50 | #name Hi all! 51 | ``` 52 | 53 | To run: 54 | 55 | ```bash 56 | $ php bin/chat-server-channels.php 57 | ``` 58 | -------------------------------------------------------------------------------- /bin/chat-server-channels.php: -------------------------------------------------------------------------------- 1 | addPeriodicTimer(60, function () { 12 | $memory = memory_get_usage() / 1024; 13 | $formatted = number_format($memory, 3).'K'; 14 | echo "Current memory usage: {$formatted}\n"; 15 | }); 16 | 17 | /* 18 | * Start of creating a new observable for our socket. 19 | */ 20 | $observable = new Rx\Chat\SocketIoObservable($loop); 21 | 22 | /* 23 | * Now subscribe a callback to it that will receive all the messages from the 24 | * observable 25 | */ 26 | $observable 27 | ->subscribeCallback(function(array $value){ echo 'GOT: ' . $value[0] . "\n"; }); 28 | 29 | /* 30 | * Create an observable of messages. 31 | */ 32 | $messagesObservable = $observable 33 | ->where(function($elem) { return $elem[0] === 'message'; }) 34 | ->select(function($elem) { return array($elem[1], $elem[2]); }); 35 | 36 | /* 37 | * We will now create three more observables: 38 | * - One containing channels join messages (e.g. "join #channel") 39 | * - One containing channels part messages (e.g. "part #channel") 40 | * - One containing channels message to a channel (e.g. "#channel hi") 41 | */ 42 | // helper function to extract the channel from the message 43 | function extractChannel($message) { 44 | if (1 !== preg_match('/#([A-Za-z0-9]+)(.*)/', $message[1], $matches)) { 45 | throw new RuntimeException(sprintf("No channel found in '%s'.", $message)); 46 | } 47 | 48 | return array($message[0], $matches[1], trim($matches[2])); 49 | } 50 | 51 | $channelJoinObservable = $messagesObservable 52 | ->where(function($elem) { return 0 === strpos($elem[1], 'join #'); }) 53 | ->select('extractChannel'); 54 | $channelPartObservable = $messagesObservable 55 | ->where(function($elem) { return 0 === strpos($elem[1], 'part #'); }) 56 | ->select('extractChannel'); 57 | $channelMessagesObservable = $messagesObservable 58 | ->where(function($elem) { return 0 === strpos($elem[1], '#'); }) 59 | ->select('extractChannel'); 60 | 61 | /* 62 | * Next we create a small collection object to hold subscription to channels. 63 | * Checkout the source. :) 64 | */ 65 | $channels = new Rx\Chat\Channels(); 66 | 67 | /* 68 | * Use the join and part messages observables to join/part the clients. 69 | */ 70 | $channelJoinObservable 71 | ->subscribeCallback(function($elem) use ($channels) { 72 | list($connection, $channel) = $elem; 73 | $channels->join($channel, $connection); 74 | echo 'Someone joining #' . $channel . "\n"; 75 | }); 76 | 77 | $channelPartObservable 78 | ->subscribeCallback(function($elem) use ($channels) { 79 | list($connection, $channel) = $elem; 80 | $channels->part($channel, $connection); 81 | echo 'Someone parting #' . $channel . "\n"; 82 | }); 83 | 84 | /* 85 | * Group the messages per channel. By: 86 | * - Providing a method that returns the key to group by 87 | * - Subscribing to the new stream of messages only for one channel 88 | * - Filter messages to have only messages from clients that are in the channel 89 | * - Broadcast to all other messages 90 | */ 91 | $channelMessagesObservable 92 | ->groupBy(function($elem) { 93 | list($connection, $channel) = $elem; 94 | 95 | return $channel; 96 | }) 97 | ->subscribeCallback(function(Rx\Observable\GroupedObservable $observable) use ($channels) { 98 | $channel = $observable->getKey(); 99 | 100 | // Observable that will only contain messages for one channel 101 | $observable 102 | ->where(function($elem) use ($channels, $channel) { return $channels->in($channel, $elem[0]); }) 103 | ->subscribeCallback(function ($elem) use ($channels, $channel) { 104 | list($from, , $message) = $elem; 105 | 106 | $clients = $channels->get($channel); 107 | foreach ($clients as $client) { 108 | if ($from !== $client) { 109 | $client->send('> #' . $channel . ': ' . $message . "\n"); 110 | } 111 | } 112 | }); 113 | }); 114 | 115 | $loop->run(); 116 | -------------------------------------------------------------------------------- /bin/chat-server.php: -------------------------------------------------------------------------------- 1 | addPeriodicTimer(60, function () { 12 | $memory = memory_get_usage() / 1024; 13 | $formatted = number_format($memory, 3).'K'; 14 | echo "Current memory usage: {$formatted}\n"; 15 | }); 16 | 17 | /* 18 | * Start of creating a new observable for our socket. 19 | */ 20 | $observable = new Rx\Chat\SocketIoObservable($loop); 21 | 22 | /* 23 | * Now subscribe a callback to it that will receive all the messages from the 24 | * observable 25 | */ 26 | $observable 27 | ->subscribeCallback(function(array $value){ echo 'GOT: ' . $value[0] . "\n"; }); 28 | 29 | /* 30 | * Let's create an observable of new connections. By: 31 | * - Filtering the original observable for "open" messages 32 | * - Selecting the only the connection from the message 33 | */ 34 | $connectObservable = $observable 35 | ->where(function(array $elem) { return $elem[0] === 'open'; }) 36 | ->select(function(array $elem) { return $elem[1]; }); 37 | 38 | /* 39 | * Now for each newly opened connection, store the connection. 40 | */ 41 | $clients = new \SplObjectStorage; 42 | $connectObservable 43 | ->subscribeCallback(function(Ratchet\ConnectionInterface $connection) use ($clients) { 44 | $clients->attach($connection); 45 | }); 46 | 47 | /* 48 | * Let's do the same for closing connections, this time removing them from the 49 | * clients map. 50 | */ 51 | $closedObservable = $observable 52 | ->where(function(array $elem) { return $elem[0] === 'closed'; }) 53 | ->select(function(array $elem) { return $elem[1]; }); 54 | $closedObservable 55 | ->subscribeCallback(function(Ratchet\ConnectionInterface $connection) use ($clients) { 56 | $clients->detach($connection); 57 | }); 58 | 59 | /* 60 | * Finally create an observable of messages and use it to send the received 61 | * messaged to all clients. 62 | */ 63 | $messagesObservable = $observable 64 | ->where(function($elem) { return $elem[0] === 'message'; }) 65 | ->select(function($elem) { return array($elem[1], $elem[2]); }); 66 | 67 | $messagesObservable 68 | ->subscribeCallback(function(array $value) use ($clients){ 69 | list($from, $message) = $value; 70 | 71 | foreach ($clients as $client) { 72 | if ($from !== $client) { 73 | $client->send($message); 74 | } 75 | } 76 | }); 77 | 78 | $loop->run(); 79 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoload": { 3 | "psr-0": { 4 | "Rx\\Chat": "src" 5 | } 6 | }, 7 | "require": { 8 | "cboden/Ratchet": "0.2.*", 9 | "asm89/rx.php": "~0.1.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Rx/Chat/Channels.php: -------------------------------------------------------------------------------- 1 | channels[$channel])) { 15 | return new SplObjectStorage(); 16 | } 17 | 18 | return $this->channels[$channel]; 19 | } 20 | 21 | public function in($channel, ConnectionInterface $connection) 22 | { 23 | if ( ! isset($this->channels[$channel])) { 24 | return false; 25 | } 26 | 27 | return $this->channels[$channel]->contains($connection); 28 | } 29 | 30 | public function join($channel, ConnectionInterface $connection) 31 | { 32 | if ( ! isset($this->channels[$channel])) { 33 | $this->channels[$channel] = new SplObjectStorage(); 34 | } 35 | 36 | $this->channels[$channel]->attach($connection); 37 | } 38 | 39 | public function part($channel, ConnectionInterface $connection) 40 | { 41 | if ( ! isset($this->channels[$channel])) { 42 | return; 43 | } 44 | 45 | $this->channels[$channel]->detach($connection); 46 | } 47 | 48 | public function partAll(ConnectionInterface $connection) 49 | { 50 | foreach ($this->channels as $channel) { 51 | $channel->detach($connection); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Rx/Chat/SocketIoObservable.php: -------------------------------------------------------------------------------- 1 | address = $address; 22 | $this->loop = $loop; 23 | $this->port = $port; 24 | } 25 | 26 | protected function doStart($scheduler) 27 | { 28 | $socket = new Reactor($this->loop); 29 | $socket->listen($this->port, $this->address); 30 | 31 | $ioServer = new IoServer($this, $socket, $this->loop); 32 | 33 | return new CallbackDisposable(function(){}); //todo: actually dispose.. 34 | } 35 | 36 | private function notifyObservers($message) 37 | { 38 | foreach ($this->observers as $observer) { 39 | $observer->onNext($message); 40 | } 41 | } 42 | 43 | public function onOpen(ConnectionInterface $connection) 44 | { 45 | if (! $this->started) { 46 | return; 47 | } 48 | 49 | $this->notifyObservers(array('open', $connection)); 50 | } 51 | 52 | public function onMessage(ConnectionInterface $from, $msg) 53 | { 54 | if (! $this->started) { 55 | return; 56 | } 57 | 58 | $this->notifyObservers(array('message', $from, $msg)); 59 | } 60 | 61 | public function onClose(ConnectionInterface $conn) 62 | { 63 | if (! $this->started) { 64 | return; 65 | } 66 | 67 | $this->notifyObservers(array('close', $conn)); 68 | } 69 | 70 | public function onError(ConnectionInterface $conn, \Exception $e) 71 | { 72 | if (! $this->started) { 73 | return; 74 | } 75 | 76 | $this->notifyObservers(array('error', $conn)); 77 | } 78 | } 79 | --------------------------------------------------------------------------------