Книга: Laravel 4 Cookbook
Назад: Packages
Дальше: Multisites

Real Time Chat

One of the most pervasive and least understood technologies that underpin the internet is socket programming. It’s been a dark art for decades, but the recent standardisation of web sockets (in relation to HTML 5) has made this type of programming a little easier to get into.

The code for this chapter can be found at: https://github.com/formativ/tutorial-laravel-4-real-time-chat.

Dependencies

This tutorial depends heavily on client-side resources. We’re developing a Laravel 4 application which has lots of server-side aspects; but it’s also a chat app. There be scripts!

Bootstrap

For this; we’re using Bootstrap and EmberJS. Download Bootstrap at: http://getbootstrap.com/ and unpack it into your public folder. Where you put the individual files makes little difference, but I have put the scripts in public/js, the stylesheets in public/css and the fonts in public/fonts. Where you see those paths in my source-code; you should substitute them with your own.

EmberJS

Next up, download EmberJS at: http://emberjs.com/ and unpack it into your public folder. You’ll also need the Ember.Data script at: http://emberjs.com/guides/getting-started/obtaining-emberjs-and-dependencies/.

Ratchet

For the server-side portion of dependencies, we need to download a library called Ratchet. I’ll explain it shortly, but in the meantime we need to add it to our composer.json file:

1 "require" : { 2     "laravel/framework" : "4.0.*", 3     "cboden/Ratchet"    : "0.3.*" 4 }, 

This was extracted from composer.json.

Follow that up with:

1 composer update 

Ratchet isn’t built specifically for Laravel 4, so there are no service providers for us to add.

We’ll now have access to the Ratchet library for client-server communication, Bootstrap for styling the interface and EmberJS for connecting these two things together.

ReactPHP

Before we can understand Ratchet, we need to understand ReactPHP. ReactPHP was born out of the need to develop event-based, asynchronous PHP applications. If you’ve worked with Node.JS you’ll feel right at home developing applications with ReactPHP; as they share a similar approaches to code. We’re not going to develop our chat application in ReactPHP, but it’s a dependency for Ratchet…

You can learn more about ReactPHP at: http://reactphp.org/.

Ratchet

One of the many ways in which real-time client-server applications are made possible is by what’s called socket programming. Believe it or not; most of what you do on the internet depends on socket programming. From simple browsing to streaming — your computer opens a socket connection to a server and the server sends data back through it.

PHP supports this type of programming but PHP websites have not typically been developed with this kind of model in mind. PHP developers have preferred the typical request/response model, and it’s comparatively easier than low-level socket programming.

Enter ReactPHP. One of the requirements for building a fully-capable socket programming framework is creating what’s called an Event Loop. ReactPHP has this and Ratchet uses it, along with the Publish/Subscribe model to accept and maintain open socket connections.

ReactPHP wraps the low-level PHP functions into a nice socket programming API and Ratchet wraps that API into another API that’s even easier to use.

You can learn more about Ratchet at: http://socketo.me/.

Creating An Interface

Let’s get to the code! We’re going to need an interface (kind of like a wireframe) so we know what to build with our application. Let’s set up a simple view and plug it into EmberJS.

I should mention that I am by no means an EmberJS expert. I learned all I know of it, while writing this tutorial, by following various guides. The point of this is not to teach EmberJS so much as it is to show EmberJS integration with Laravel 4.

Creating A View

Let’s change the default routes.php file to load a custom view:

1 <?php 2  3 Route::get("/", function() 4 { 5     return View::make("index/index"); 6 }); 

This file should be saved as app/routes.php.

Then we need to add this view:

 1 <!DOCTYPE html>  2 <html lang="en">  3     <head>  4     <meta charset="UTF-8" />  5         <link  6             rel="stylesheet"  7             type="text/css"  8             href="{{ asset("css/bootstrap.3.0.0.css") }}"   9         /> 10         <link 11             rel="stylesheet" 12             type="text/css" 13             href="{{ asset("css/bootstrap.theme.3.0.0.css") }}" 14         /> 15         <title>Laravel 4 Chat</title> 16     </head> 17     <body> 18         <script type="text/x-handlebars"> 19             @{{outlet}} 20         </script> 21         <script 22             type="text/x-handlebars" 23             data-template-name="index" 24         > 25             <div class="container"> 26                 <div class="row"> 27                     <div class="col-md-12"> 28                         <h1>Laravel 4 Chat</h1> 29                         <table class="table table-striped"> 30                             @{{#each}} 31                                 <tr> 32                                     <td> 33                                         @{{user}} 34                                     </td> 35                                     <td> 36                                         @{{text}} 37                                     </td> 38                                 </tr> 39                             @{{/each}} 40                         </table> 41                     </div> 42                 </div> 43                 <div class="row"> 44                     <div class="col-md-12"> 45                         <div class="input-group"> 46                             <input 47                                 type="text" 48                                 class="form-control" 49                             /> 50                             <span class="input-group-btn"> 51                                 <button class="btn btn-default"> 52                                     Send 53                                 </button> 54                             </span> 55                         </div> 56                     </div> 57                 </div> 58             </div> 59         </script> 60         <script 61             type="text/javascript" 62             src="{{ asset("js/jquery.1.9.1.js") }}" 63         ></script> 64         <script 65             type="text/javascript" 66             src="{{ asset("js/handlebars.1.0.0.js") }}" 67         ></script> 68         <script 69             type="text/javascript" 70             src="{{ asset("js/ember.1.1.1.js") }}" 71         ></script> 72         <script 73             type="text/javascript" 74             src="{{ asset("js/ember.data.1.0.0.js") }}" 75         ></script> 76         <script 77             type="text/javascript" 78             src="{{ asset("js/bootstrap.3.0.0.js") }}" 79         ></script> 80         <script 81             type="text/javascript" 82             src="{{ asset("js/shared.js") }}" 83         ></script> 84     </body> 85 </html> 

This file should be saved as app/views/index/index.blade.php.

Both Blade and EmberJS use double-curly-brace syntax for variable and logic substitution. Luckily Blade includes a mechanism to ignore curly brace blocks, by prepending them with @ symbols. Thus our template includes @ symbols before all EmberJS blocks.

The scripts and stylesheets need to be relative to where you saved them or you’re going to see errors.

Creating An EmberJS App

You’ll notice I’ve specified shared.css and shared.js files — the CSS file is blank, but the JavaScript file contains:

 1 // 1  2 var App = Ember.Application.create();  3   4 // 2  5 App.Router.map(function() {  6     this.resource("index", {  7         "path" : "/"  8     });  9 }); 10  11 // 3 12 App.Message = DS.Model.extend({ 13     "user" : DS.attr("string"), 14     "text" : DS.attr("string") 15 }); 16  17 // 4 18 App.ApplicationAdapter = DS.FixtureAdapter.extend();  19  20 // 5 21 App.Message.FIXTURES = [  22     { 23         "id"   : 1, 24         "user" : "Chris", 25         "text" : "Hello World." 26     }, 27     { 28         "id"   : 2, 29         "user" : "Wayne", 30         "text" : "Don't dig it, man." 31     }, 32     { 33         "id"   : 3, 34         "user" : "Chris", 35         "text" : "Meh." 36     } 37 ]; 

This file should be saved as public/js/shared.js.

If you’re an EmberJS noob, like me, then it will help to understand what each piece of this script is doing.

  1. We create a new Ember application with Ember.Application.create().
  2. Routes are defined in the App.Route.map() method, and we tell the application to equate the path / to the index resource.
  3. We define a Message model. These are similar to the Eloquent models we have in Laravel 4, but they’re built to work with EmberJS (and are obviously on the client-side of the application).
  4. We specify a fixture-based data store for our application. We’re using this, temporarily, to fill our interface with some dummy data, but we’ll add a dynamic data store before too long…
  5. Here we add the fixture data. Notice that, in addition to the two model fields we defined, we also specify ID values for the fixture rows. This data is used to single out individual Message objects.

When you browse to the base URL of the application; you should now see an acceptably styled list of message objects, along with a heading and input form. Let’s make it dynamic!

You should also see some console log messages (depending on your browser) which show that EmberJS is running.

Creating A Service Provider

Following on from a previous tutorial; we’re going to be creating a service provider which will provide the various classes for our application. Create a new workbench:

1 php artisan workbench formativ/chat 

This will produce (amongst other things) a service provider template. I’ve added a few IoC bindings to it:

 1 <?php  2   3 namespace Formativ\Chat;  4   5 use Evenement\EventEmitter;  6 use Illuminate\Support\ServiceProvider;  7 use Ratchet\Server\IoServer;  8   9 class ChatServiceProvider 10 extends ServiceProvider 11 { 12     protected $defer = true; 13  14     public function register() 15     { 16         $this->app->bind("chat.emitter", function() 17         { 18             return new EventEmitter(); 19         }); 20  21         $this->app->bind("chat.chat", function() 22         { 23             return new Chat( 24                 $this->app->make("chat.emitter") 25             ); 26         }); 27  28         $this->app->bind("chat.user", function() 29         { 30             return new User(); 31         }); 32  33         $this->app->bind("chat.command.serve", function() 34         { 35             return new Command\Serve( 36                 $this->app->make("chat.chat") 37             ); 38         });    39  40         $this->commands("chat.command.serve"); 41     } 42  43     public function provides() 44     { 45         return [ 46             "chat.chat", 47             "chat.command.serve", 48             "chat.emitter", 49             "chat.server" 50         ]; 51     } 52 } 

This file should be saved as workbench/formativ/chat/src/Formativ/Chat/ChatServiceProvider.php.

The first binding is a simple alias to the EvenementEventEmitter class (which Ratchet requires). We bind it here as we cannot guarantee that Ratchet will continue to use EvenementEventEmitter and we’ll need a reliable way to unit test possible alternatives in the future.

Creating A Chat Handler

Let’s look closer at the second and third bindings. The first is to the Chat class. It implements the ChatInterface interface:

 1 <?php  2   3 namespace Formativ\Chat;  4   5 use Evenement\EventEmitterInterface;  6 use Ratchet\ConnectionInterface;  7 use Ratchet\MessageComponentInterface;  8   9 interface ChatInterface 10 extends MessageComponentInterface 11 { 12     public function getUserBySocket(ConnectionInterface $socket); 13     public function getEmitter(); 14     public function setEmitter(EventEmitterInterface $emitter); 15     public function getUsers(); 16 } 

This file should be saved as workbench/formativ/chat/src/Formativ/Chat/ChatInterface.php.

It’s interesting to note that PHP actually supports interfaces which extend other interfaces. This is useful if you want to expect a certain level of functionality, provided by a third-party library (such as SPL), but want to add your own requirements on top.

The concrete implementation looks like this:

  1 <?php   2    3 namespace Formativ\Chat;   4    5 use Evenement\EventEmitterInterface;   6 use Exception;   7 use Ratchet\ConnectionInterface;   8 use SplObjectStorage;   9   10 class Chat  11 implements ChatInterface  12 {  13     protected $users;  14     protected $emitter;  15     protected $id = 1;  16   17     public function getUserBySocket(ConnectionInterface $socket)  18     {  19         foreach ($this->users as $next)  20         {  21             if ($next->getSocket() === $socket)  22             {  23                 return $next;  24             }  25         }  26   27         return null;  28     }  29   30     public function getEmitter()  31     {  32         return $this->emitter;  33     }  34   35     public function setEmitter(EventEmitterInterface $emitter)  36     {  37         $this->emitter = $emitter;  38     }  39   40     public function getUsers()  41     {  42         return $this->users;  43     }  44   45     public function __construct(EventEmitterInterface $emitter)  46     {  47         $this->emitter = $emitter;  48         $this->users   = new SplObjectStorage();  49     }  50   51     public function onOpen(ConnectionInterface $socket)  52     {  53         $user = new User();  54         $user->setId($this->id++);  55         $user->setSocket($socket);  56   57         $this->users->attach($user);  58         $this->emitter->emit("open", [$user]);  59     }  60   61     public function onMessage(  62         ConnectionInterface $socket,  63         $message  64     )  65     {  66         $user = $this->getUserBySocket($socket);  67         $message = json_decode($message);  68   69         switch ($message->type)  70         {  71             case "name":  72             {  73                 $user->setName($message->data);  74                 $this->emitter->emit("name", [  75                     $user,  76                     $message->data  77                 ]);  78                 break;  79             }  80   81             case "message":  82             {  83                 $this->emitter->emit("message", [  84                     $user,  85                     $message->data  86                 ]);  87                 break;  88             }  89         }    90   91         foreach ($this->users as $next)  92         {  93             if ($next !== $user)  94             {  95                 $next->getSocket()->send(json_encode([  96                     "user" => [  97                         "id"   => $user->getId(),  98                         "name" => $user->getName()  99                     ], 100                     "message" => $message 101                 ])); 102             } 103         } 104     } 105  106     public function onClose(ConnectionInterface $socket) 107     { 108         $user = $this->getUserBySocket($socket); 109  110         if ($user) 111         { 112             $this->users->detach($user); 113             $this->emitter->emit("close", [$user]); 114         } 115     } 116  117     public function onError( 118         ConnectionInterface $socket, 119         Exception $exception 120     ) 121     { 122         $user = $this->getUserBySocket($socket); 123  124         if ($user) 125         { 126             $user->getSocket()->close(); 127             $this->emitter->emit("error", [$user, $exception]); 128         } 129     } 130 } 

This file should be saved as workbench/formativ/chat/src/Formativ/Chat/Chat.php.

It’s fairly simple: the (delegate) onOpen and onClose methods handle creating new User objects and disposing of them. The onMessage method translates JSON-encoded message objects into required actions and responds back to the other socket connections with further details.

Creating A Socket Wrapper

Additionally; the UserInterface interface and User class look like this:

 1 <?php  2   3 namespace Formativ\Chat;  4   5 use Evenement\EventEmitterInterface;  6 use Ratchet\ConnectionInterface;  7 use Ratchet\MessageComponentInterface;  8   9 interface UserInterface 10 { 11     public function getSocket(); 12     public function setSocket(ConnectionInterface $socket); 13     public function getId(); 14     public function setId($id); 15     public function getName(); 16     public function setName($name); 17 } 

This file should be saved as workbench/formativ/chat/src/Formativ/Chat/UserInterface.php.

 1 <?php  2   3 namespace Formativ\Chat;  4   5 use Ratchet\ConnectionInterface;  6   7 class User  8 implements UserInterface  9 { 10     protected $socket; 11     protected $id; 12     protected $name; 13  14     public function getSocket() 15     { 16         return $this->socket; 17     } 18  19     public function setSocket(ConnectionInterface $socket) 20     { 21         $this->socket = $socket; 22         return $this; 23     } 24  25     public function getId() 26     { 27         return $this->id; 28     } 29  30     public function setId($id) 31     { 32         $this->id = $id; 33         return $this; 34     } 35  36     public function getName() 37     { 38         return $this->name; 39     } 40  41     public function setName($name) 42     { 43         $this->name = $name; 44         return $this; 45     } 46 } 

This file should be saved as workbench/formativ/chat/src/Formativ/Chat/User.php.

The User class is a simple wrapper for a socket resource and name string. The way we’ve chosen to implement the Ratchet server requires that we have a class which implements the MessageComponentInterface interface; and this interface specifies that ConnectionInterface objects are passed back and forth. There’s no way to identify these, by name (and id), so we’re adding that functionality with the extra layer.

Creating A Serve Command

All these classes lead us to the artisan command which will kick things off:

  1 <?php   2    3 namespace Formativ\Chat\Command;   4    5 use Illuminate\Console\Command;   6 use Formativ\Chat\ChatInterface;   7 use Formativ\Chat\UserInterface;   8 use Ratchet\ConnectionInterface;   9 use Ratchet\Http\HttpServer;  10 use Ratchet\Server\IoServer;  11 use Ratchet\WebSocket\WsServer;  12 use Symfony\Component\Console\Input\InputOption;  13 use Symfony\Component\Console\Input\InputArgument;  14   15 class Serve  16 extends Command  17 {  18     protected $name        = "chat:serve";  19     protected $description = "Command description.";  20     protected $chat;  21   22     protected function getUserName($user)  23     {  24         $suffix = " (" . $user->getId() . ")";  25   26         if ($name = $user->getName())  27         {  28             return $name . $suffix;  29         }  30   31         return "User" . $suffix;  32     }  33   34     public function __construct(ChatInterface $chat)  35     {  36         parent::__construct();  37   38         $this->chat = $chat;  39   40         $open = function(UserInterface $user)  41         {  42             $name = $this->getUserName($user);  43             $this->line("  44                 <info>" . $name . " connected.</info>  45             ");  46         };  47   48         $this->chat->getEmitter()->on("open", $open);  49   50         $close = function(UserInterface $user)  51         {  52             $name = $this->getUserName($user);  53             $this->line("  54                 <info>" . $name . " disconnected.</info>  55             ");  56         };  57   58         $this->chat->getEmitter()->on("close", $close);  59   60         $message = function(UserInterface $user, $message)  61         {  62             $name = $this->getUserName($user);  63             $this->line("  64                 <info>New message from " . $name . ":</info>   65                 <comment>" . $message . "</comment>  66                 <info>.</info>  67             ");  68         };  69   70         $this->chat->getEmitter()->on("message", $message);  71   72         $name = function(UserInterface $user, $message)  73         {  74             $this->line("  75                 <info>User changed their name to:</info>   76                 <comment>" . $message . "</comment>  77                 <info>.</info>  78             ");  79         };  80   81         $this->chat->getEmitter()->on("name", $name);  82   83         $error = function(UserInterface $user, $exception)  84         {  85             $message = $exception->getMessage();  86   87             $this->line("  88                 <info>User encountered an exception:</info>   89                 <comment>" . $message . "</comment>  90                 <info>.</info>  91             ");  92         };  93   94         $this->chat->getEmitter()->on("error", $error);  95     }  96   97     public function fire()  98     {  99         $port = (integer) $this->option("port"); 100  101         if (!$port) 102         { 103             $port = 7778; 104         } 105  106         $server = IoServer::factory( 107             new HttpServer( 108                 new WsServer( 109                     $this->chat 110                 ) 111             ), 112             $port 113         ); 114  115         $this->line(" 116             <info>Listening on port</info> 117             <comment>" . $port . "</comment> 118             <info>.</info> 119         "); 120  121         $server->run(); 122     } 123  124     protected function getOptions() 125     { 126         return [ 127             [ 128                 "port", 129                 null, 130                 InputOption::VALUE_REQUIRED, 131                 "Port to listen on.", 132                 null 133             ] 134         ]; 135     } 136 } 

This file should be saved as workbench/formativ/chat/src/Formativ/Chat/Command/Serve.php.

The reason for us adding the event emitter to the equation should now be obvious — we need a way to tie into the delegated events, of the Chat class, without leaking the abstraction we gain from it. In other words; we don’t want the Chat class to know of the existence of the artisan command. Similarly; we don’t want the artisan command to know of the onOpen, onMessage, onError and onMessage methods so instead we use a publish/subscribe model for notifying the command of changes. The result is a clean abstraction.

The fire() method gets (or defaults) the port and starts the Ratchet web socket server.

The method we’re using, to start the server, is not the only way it can be started. You can learn more about the web socket server at: http://socketo.me/docs/websocket.

Connecting To The Socket Server

To connect to the web socket server; we need to add a bit of vanilla JavaScript:

 1 try {  2     if (!WebSocket) {  3         console.log("no websocket support");  4     } else {  5         var socket = new WebSocket("ws://127.0.0.1:7778/");  6   7         socket.addEventListener("open", function (e) {  8             console.log("open: ", e);  9         }); 10  11         socket.addEventListener("error", function (e) { 12             console.log("error: ", e); 13         }); 14  15         socket.addEventListener("message", function (e) { 16             console.log("message: ", JSON.parse(e.data)); 17         }); 18  19         console.log("socket:", socket); 20  21         window.socket = socket; 22     } 23 } catch (e) { 24     console.log("exception: " + e); 25 } 

This was extracted from public/js/shared.js.

We’ve wrapped this code in a try-catch block as not all browsers support Web Sockets yet. There are a number of libraries which will shim this functionality, but their use is outside the scope of this tutorial.

You can find a couple of these libraries at: https://github.com/gimite/web-socket-js and https://github.com/sockjs.

This code will attempt to open a socket connection to 127.0.0.1:7778 (the address and port used in the serve command) and write some console messages depending on the events that are emitted. You’ll notice we’re also assigning the socket instance to the window object; so we can send some debugging commands through it.

This allows us to see both the server-side of things, as well as the client-side…

Wiring Up The Interface

Getting our interface talking to our socket server is relatively straightforward. We begin by disabling our fixture data and modifying our model slightly:

 1 App.Message = DS.Model.extend({  2     "user_id"       : DS.attr("integer"),  3     "user_name"     : DS.attr("string"),  4     "user_id_class" : DS.attr("string"),  5     "message"       : DS.attr("string")  6 });  7   8 App.ApplicationAdapter = DS.FixtureAdapter.extend();  9  10 App.Message.FIXTURES = [ 11     // { 12     //     "id"   : 1, 13     //     "user" : "Chris", 14     //     "text" : "Hello World." 15     // }, 16     // { 17     //     "id"   : 2, 18     //     "user" : "Wayne", 19     //     "text" : "Don't dig it, man." 20     // }, 21     // { 22     //     "id"   : 3, 23     //     "user" : "Chris", 24     //     "text" : "Meh." 25     // } 26 ]; 

This was extracted from public/js/shared.js.

If you want to pre-populate your chat application with a history; you could feed this fixture configuration with data from your server.

Showing Chat Messages

Now the index template will show only the heading and form elements, but no chat messages. In order to populate these; we need to store a reference to the application data store:

 1 var store;  2   3 App.IndexRoute = Ember.Route.extend({  4     "init" : function() {  5         store = this.store;  6     },  7     "model" : function () {  8         return store.find("message");  9     } 10 }); 

This was extracted from public/js/shared.js.

We store this reference because we will need to push rows into the store once we receive them from the open socket. This leads us to the changes to web sockets:

 1 try {  2     var id = 1;  3   4     if (!WebSocket) {  5         console.log("no websocket support");  6     } else {  7   8         var socket = new WebSocket("ws://127.0.0.1:7778/");  9         var id     = 1; 10  11         socket.addEventListener("open", function (e) { 12             // console.log("open: ", e); 13         }); 14  15         socket.addEventListener("error", function (e) { 16             console.log("error: ", e); 17         }); 18  19         socket.addEventListener("message", function (e) { 20  21             var data      = JSON.parse(e.data); 22             var user_id   = data.user.id; 23             var user_name = data.user.name; 24             var message   = data.message.data; 25  26             switch (data.message.type) { 27  28                 case "name": 29                     $(".name-" + user_id).html(user_name); 30                     break; 31  32                 case "message": 33                     store.push("message", { 34                         "id"            : id++, 35                         "user_id"       : user_id, 36                         "user_name"     : user_name || "User", 37                         "user_id_class" : "name-" + user_id, 38                         "message"       : message 39                     }); 40                     break; 41  42             } 43  44         }); 45  46         // console.log("socket:", socket); 47  48         window.socket = socket; // debug 49     } 50 } catch (e) { 51     console.log("exception: " + e); 52 } 

This was extracted from public/js/shared.js.

We start by defining an id variable, to store the id’s of message objects as they are passed through the socket. Inside the onMessage event handler; we parse the JSON data string and determine the type of message being received. If it’s a name message (a user changing their name) then we update all the table cells matching the user’s server id. If it’s a normal message object; we push it into the data store.

Sending Chat Messages

This gets us part of the way, since console message commands will visually affect the UI. We still need to wire up the input form…

 1 App.IndexController = Ember.ArrayController.extend({  2     "command" : null,  3     "actions" : {  4         "send" : function(key) {  5   6             if (key && key != 13) {  7                 return;  8             }  9  10             var command = this.get("command") || ""; 11  12             if (command.indexOf("/name") === 0) { 13                 socket.send(JSON.stringify({ 14                     "type" : "name", 15                     "data" : command.split("/name")[1] 16                 })); 17             } else { 18                 socket.send(JSON.stringify({ 19                     "type" : "message", 20                     "data" : command 21                 })); 22             } 23  24             this.set("command", null); 25         } 26     } 27 }); 28  29 App.IndexView = Ember.View.extend({ 30     "keyDown" : function(e) { 31         this.get("controller").send("send", e.keyCode); 32     } 33 }); 

This was extracted from public/js/shared.js.

We create IndexController and IndexView objects. The IndexView object intercepts the keyDown event and passes it to the IndexController object. The first bit of logic tells the send() method to ignore all keystrokes that aren’t the enter key. This means enter will trigger the send() method.

It continues by checking for the presence of a /name command switch. If that’s present in the input value (via this.get(“command”)) then it sends a message to the server to change the user’s name. Otherwise it sends a normal message to the server. In order for the UI to update for the person sending the message; we need to also slightly modify the Chat class:

 1 public function onMessage(ConnectionInterface $socket, $message)  2 {  3     $user = $this->getUserBySocket($socket);  4     $message = json_decode($message);  5   6     switch ($message->type)  7     {  8         case "name":  9         { 10             $user->setName($message->data); 11             $this->emitter->emit("name", [ 12                 $user, 13                 $message->data 14             ]); 15             break; 16         } 17  18         case "message": 19         { 20             $this->emitter->emit("message", [ 21                 $user, 22                 $message->data 23             ]); 24             break; 25         } 26     } 27  28     foreach ($this->users as $next) 29     { 30         // if ($next !== $user) 31         // { 32             $next->getSocket()->send(json_encode([ 33                 "user" => [ 34                     "id"   => $user->getId(), 35                     "name" => $user->getName() 36                 ], 37                 "message" => $message 38             ])); 39         // } 40     } 41 } 

This was extracted from workbench/formativ/chat/src/Formativ/Chat/Chat.php.

The change we’ve made is to exclude the logic which prevented messages from being sent to the user from which they came. All messages will essentially be sent to everyone on the server now.

Finishing Up The Template

The final change is to the index template, as we changed the model structure and need to adjust for this in the template:

 1 <script  2     type="text/x-handlebars"  3     data-template-name="index"  4 >  5     <div class="container">  6         <div class="row">  7             <div class="col-md-12">  8                 <h1>Laravel 4 Chat</h1>  9                 <table class="table table-striped"> 10                     @{{#each message in model}} 11                         <tr> 12                             <td @{{bind-attr class="message.user_id_class"}}> 13                                 @{{message.user_name}} 14                             </td> 15                             <td> 16                                 @{{message.message}} 17                             </td> 18                         </tr> 19                     @{{/each}} 20                 </table> 21             </div> 22         </div> 23         <div class="row"> 24             <div class="col-md-12"> 25                 <div class="input-group"> 26                     @{{input 27                         type="text" 28                         value=command 29                         class="form-control" 30                     }} 31                     <span class="input-group-btn"> 32                         <button 33                             class="btn btn-default" 34                             @{{action "send"}} 35                         > 36                             Send 37                         </button> 38                     </span> 39                 </div> 40             </div> 41         </div> 42     </div> 43 </script> 

This was extracted from app/views/index/index.blade.php.

You’ll notice, apart from us using different field names; that we’ve change how the loop is done, the class added to each “label” cell and how the input field is generated.

The reason for the change to the loop structure is simply so that you can see another way to represented enumerable data in handlebars. Where previously we used a simple {{#each}} tag, we’re now being more explicit about the data we want to iterate.

We add a special class on each “label” cell as we need to target these cells and change their contents, in the event that a user decides to change their name.

Finally, we change how the input field is generated because we need to bind its value to the IndexController’s command property. This format allows that succinctly.

You can learn more about Ember JS at: http://emberjs.com/.

Note On Nginx

For persistent sockets to remain open, Apache needs to keep a single process thread occupied. This is a problem as it will consume vast amounts of RAM over a shorter time period than non-persistent connections would. For this reason; I highly recommend using either Nginx or an event-based language to create your chat application.

It’s not that Apache sucks; this is just not the sort of thing it was designed for. Nginx, on the other hand, is event-based and doesn’t hold onto a whole thread while it waits for activity through the open socket.

You can learn more about Nginx at: http://wiki.nginx.org/Main.

Назад: Packages
Дальше: Multisites