├── tests └── .gitkeep ├── src ├── config │ ├── .gitkeep │ └── config.php ├── Sidney │ └── Latchet │ │ ├── Generators │ │ ├── content │ │ │ └── routes.php │ │ ├── stubs │ │ │ ├── Connection.php │ │ │ └── TestTopic.php │ │ └── Generator.php │ │ ├── LatchetException.php │ │ ├── Handlers │ │ ├── HandlerInterface.php │ │ ├── ConnectionEventHandler.php │ │ └── TopicEventHandler.php │ │ ├── BaseConnection.php │ │ ├── LatchetFacade.php │ │ ├── LatchetServiceProvider.php │ │ ├── BaseTopic.php │ │ ├── LatchetPusher.php │ │ └── Latchet.php └── commands │ ├── GenerateCommand.php │ └── ListenCommand.php ├── .gitignore ├── .travis.yml ├── phpunit.xml ├── composer.json └── README.md /tests/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store -------------------------------------------------------------------------------- /src/Sidney/Latchet/Generators/content/routes.php: -------------------------------------------------------------------------------- 1 | Latchet::connection('Connection'); 2 | Latchet::topic('test-topic', 'TestTopic'); -------------------------------------------------------------------------------- /src/Sidney/Latchet/LatchetException.php: -------------------------------------------------------------------------------- 1 | close(); 20 | 21 | throw new Exception($exception); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Sidney/Latchet/Generators/stubs/TestTopic.php: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sidney/latchet", 3 | "description": "WebSockets with Laravel 4", 4 | "authors": [ 5 | { 6 | "name": "Sidney Widmer", 7 | "email": "sidneywidmer@gmail.com" 8 | } 9 | ], 10 | "require": { 11 | "php": ">=5.3.0", 12 | "illuminate/support": "4.*", 13 | "cboden/ratchet": "0.3.*" 14 | }, 15 | "autoload": { 16 | "classmap": [ 17 | "src/commands" 18 | ], 19 | "psr-0": { 20 | "Sidney\\Latchet": "src/" 21 | } 22 | }, 23 | "extra": { 24 | "branch-alias": { 25 | "dev-master": "1.0-dev" 26 | } 27 | }, 28 | "minimum-stability": "dev" 29 | } 30 | -------------------------------------------------------------------------------- /src/Sidney/Latchet/Handlers/ConnectionEventHandler.php: -------------------------------------------------------------------------------- 1 | controller = $controller; 23 | } 24 | 25 | /** 26 | * Execute the handler 27 | * 28 | * @param string $event 29 | * @return void 30 | */ 31 | public function run($event) 32 | { 33 | $this->callController($event); 34 | } 35 | 36 | /** 37 | * Set the Ratchet variables 38 | * 39 | * @param array $parameters 40 | * @return void 41 | */ 42 | public function setWsParameters($parameters) 43 | { 44 | $this->wsParameters = $parameters; 45 | } 46 | 47 | /** 48 | * Call the registered controller 49 | * with the right event 50 | * 51 | * @param string $event (either open or close) 52 | */ 53 | protected function callController($event) 54 | { 55 | $parameters = $this->wsParameters; 56 | 57 | return call_user_func_array(array($this->controller, $event), $parameters); 58 | } 59 | } -------------------------------------------------------------------------------- /src/Sidney/Latchet/Generators/Generator.php: -------------------------------------------------------------------------------- 1 | files = $files; 23 | } 24 | 25 | /** 26 | * Copy all the stubs to /socket 27 | * 28 | * @return void 29 | */ 30 | public function make($path) 31 | { 32 | if($this->copyFiles($path)) 33 | { 34 | //all stubs were copied successfuly 35 | //so we can now edit the routes.php file 36 | return $this->editRoutesFile(); 37 | } 38 | 39 | return false; 40 | } 41 | 42 | /** 43 | * Copy all the files to /socket 44 | * 45 | * @param string $path 46 | * @return bool 47 | */ 48 | protected function copyFiles($path) 49 | { 50 | if (!$this->files->isDirectory($path)) 51 | { 52 | return $this->files->copyDirectory(__DIR__.'/stubs', $path); 53 | } 54 | 55 | return false; 56 | } 57 | 58 | /** 59 | * Edit the app/routes.php file to register the previously 60 | * copied handlers 61 | * 62 | * @return bool 63 | */ 64 | protected function editRoutesFile() 65 | { 66 | return $this->files->append( 67 | app_path().'/routes.php', 68 | $this->getContent(__DIR__.'/content/routes.php') 69 | ); 70 | } 71 | 72 | /** 73 | * get content of a file 74 | * 75 | * @param string $path 76 | * @return srting 77 | */ 78 | protected function getContent($path) 79 | { 80 | return PHP_EOL.$this->files->get($path); 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /src/Sidney/Latchet/LatchetServiceProvider.php: -------------------------------------------------------------------------------- 1 | package('sidney/latchet'); 23 | } 24 | 25 | /** 26 | * Register the service provider. 27 | * 28 | * @return void 29 | */ 30 | public function register() 31 | { 32 | $this->registerLatchet(); 33 | $this->registerCommands(); 34 | } 35 | 36 | /** 37 | * Register the application bindings. 38 | * 39 | * @return void 40 | */ 41 | private function registerLatchet() 42 | { 43 | $this->app['latchet'] = $this->app->share(function($app) 44 | { 45 | $latchet = new Latchet($app); 46 | return $latchet; 47 | }); 48 | } 49 | 50 | /** 51 | * Register the artisan commands. 52 | * 53 | * @return void 54 | */ 55 | private function registerCommands() 56 | { 57 | $this->app['command.latchet.listen'] = $this->app->share(function($app) 58 | { 59 | return new ListenCommand($app); 60 | }); 61 | 62 | $this->app['command.latchet.generate'] = $this->app->share(function($app) 63 | { 64 | $path = app_path() . '/socket'; 65 | 66 | $generator = new Generator($app['files']); 67 | 68 | return new GenerateCommand($generator, $path); 69 | }); 70 | 71 | $this->commands( 72 | 'command.latchet.listen', 73 | 'command.latchet.generate' 74 | ); 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /src/commands/GenerateCommand.php: -------------------------------------------------------------------------------- 1 | path = $path; 50 | $this->generator = $generator; 51 | } 52 | 53 | /** 54 | * Execute the console command. 55 | * 56 | * @return void 57 | */ 58 | public function fire() 59 | { 60 | $this->generate(); 61 | } 62 | 63 | /** 64 | * Generate all the necessary files for a websocket connection. 65 | * This means a ConnectonHandler and at least a TopicHandler 66 | * 67 | * @return void 68 | */ 69 | protected function generate() 70 | { 71 | if (!$this->generator->make($this->path)) 72 | { 73 | $this->error('Couldn\'t generate all the necessary files because the \'app/socket\' folder already exists.'); 74 | } 75 | else 76 | { 77 | $this->info('All files created successfully!'); 78 | } 79 | } 80 | 81 | } -------------------------------------------------------------------------------- /src/Sidney/Latchet/BaseTopic.php: -------------------------------------------------------------------------------- 1 | 0) 24 | { 25 | $this->broadcastExclude($topic, $msg, $exclude); 26 | } 27 | elseif (count($eligible) > 0) 28 | { 29 | $this->broadcastEligible($topic, $msg, $eligible); 30 | } 31 | else 32 | { 33 | $topic->broadcast($msg); 34 | } 35 | } 36 | 37 | /** 38 | * Broadcast message only to clients which 39 | * are not in the exclude array (blacklist) 40 | * 41 | * @param Ratchet\Wamp\Topic $topic 42 | * @param mixed $msg 43 | * @param array $exclude 44 | * @return void 45 | */ 46 | protected function broadcastExclude($topic, $msg, $exclude) 47 | { 48 | foreach ($topic->getIterator() as $client) 49 | { 50 | if (!in_array($client->WAMP->sessionId, $exclude)) 51 | { 52 | $client->event($topic, $msg); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Broadcast message only to clients which 59 | * are in the eligible array (whitelist) 60 | * 61 | * @param Ratchet\Wamp\Topic $topic 62 | * @param mixed $msg 63 | * @param array $eligible 64 | * @return void 65 | */ 66 | protected function broadcastEligible($topic, $msg, $eligible) 67 | { 68 | foreach ($topic->getIterator() as $client) 69 | { 70 | if (in_array($client->WAMP->sessionId, $eligible)) 71 | { 72 | $client->event($topic, $msg); 73 | } 74 | } 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /src/Sidney/Latchet/LatchetPusher.php: -------------------------------------------------------------------------------- 1 | subscribers; 15 | } 16 | } 17 | 18 | function __set($name, $value) 19 | { 20 | if ($name == 'subscribedTopics') { 21 | $this->subscribers = $value; 22 | } 23 | } 24 | 25 | /** 26 | * add a new topic (subscriber) to our lookup array 27 | * 28 | * @param Ratchet\Wamp\Connection 29 | * @param Ratchet\Wamp\Topic 30 | * @return void 31 | */ 32 | public function addSubscriber($conneciton, $topic) 33 | { 34 | if (!array_key_exists($topic->getId(), $this->subscribers)) { 35 | $this->subscribers[$topic->getId()] = ['topic' => $topic, 'connections' => []]; 36 | } 37 | $this->subscribers[$topic->getId()]['connections'][] = $conneciton; 38 | } 39 | 40 | /** 41 | * Reset subscriber after the connection is closed 42 | */ 43 | public function removeSubscriber($conneciton) 44 | { 45 | foreach ($this->subscribers as $topicId => &$subscribe) { 46 | // remove connection from connections 47 | if (($key = array_search($conneciton, $subscribe['connections'])) !== false) { 48 | unset($subscribe['connections'][$key]); 49 | } 50 | // remove topic if connections is empty 51 | if (!$subscribe['connections']) { 52 | unset($this->subscribers[$topicId]); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * json we recieve from ZerMQ 59 | * 60 | * @param string $message 61 | * @return void 62 | */ 63 | public function serverPublish($message) 64 | { 65 | $message = json_decode($message, true); 66 | // If the lookup topic object isn't set there is no one to publish to 67 | if (!array_key_exists($message['topic'], $this->subscribers)) { 68 | return; 69 | } 70 | $topicId = $message['topic']; 71 | $subscriber = $this->subscribers[$topicId]; 72 | $subscriber['topic']->broadcast($message); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/Sidney/Latchet/Handlers/TopicEventHandler.php: -------------------------------------------------------------------------------- 1 | callController($event); 38 | } 39 | 40 | /** 41 | * Set the Ratchet variables 42 | * 43 | * @param array $parameters 44 | * @return void 45 | */ 46 | public function setWsParameters($parameters) 47 | { 48 | $this->wsParameters = $parameters; 49 | } 50 | 51 | /** 52 | * Set the matching request parameters array on the handler 53 | * 54 | * @param array $parameters 55 | * @return void 56 | */ 57 | public function setRequestParameters($parameters) 58 | { 59 | $this->requestParameters = $parameters; 60 | } 61 | 62 | /** 63 | * Call the registered controller 64 | * with the right event 65 | * 66 | * @param string $event (subscribe, publish, call, unsubscribe) 67 | */ 68 | protected function callController($event) 69 | { 70 | $parameters = $this->getMergedParameters(); 71 | 72 | return call_user_func_array(array($this->requestParameters['_controller'], $event), $parameters); 73 | } 74 | 75 | /** 76 | * get the final parameters for the actual 77 | * call to the controller 78 | * 79 | * @return array 80 | */ 81 | protected function getMergedParameters() 82 | { 83 | $variables = $this->compile()->getVariables(); 84 | 85 | // To get the parameter array, we need to spin the names of the variables on 86 | // the compiled route and match them to the parameters that we got when a 87 | // route is matched by the router, as routes instances don't have them. 88 | $parameters = array(); 89 | 90 | foreach ($variables as $variable) 91 | { 92 | $parameters[$variable] = $this->requestParameters[$variable]; 93 | } 94 | 95 | return $this->mergedParameters = array_merge($this->wsParameters,$parameters); 96 | } 97 | 98 | } 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | 1111, 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Enable Push Option 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Latchet gives you the possibility to easily push messages to 24 | | subscribed Topics. To be able to push messages, we need the 25 | | ZeroMQ Library (libzmq). It can be a little tricky to install the 26 | | library and the PECL extension. A lot of hosters won't 27 | | even allow you to install something so it's optional and you 28 | | can enable it here. 29 | | 30 | */ 31 | 32 | 'enablePush' => false, 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | ZeroMQ Socket Default Port 37 | |-------------------------------------------------------------------------- 38 | | 39 | | Port for the ZeroMQ connection. This is used so we can connect 40 | | to all Socket connections and broadcast messages from e.g an 41 | | Ajax Request. 42 | | 43 | */ 44 | 45 | 'zmqPort' => 5555, 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Allow Flash 50 | |-------------------------------------------------------------------------- 51 | | 52 | | Allow legacy browsers to connect with the websocket polyfill 53 | | https://github.com/gimite/web-socket-js 54 | | 55 | */ 56 | 57 | 'allowFlash' => true, 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Flash Port 62 | |-------------------------------------------------------------------------- 63 | | 64 | | If Flash is allowed and Websockets are not supported by the client 65 | | browser, you have to provide a Flash socket policy file for the 66 | | web-socket-js fallback. 67 | | 68 | | This is automatically done by latchet. However, you have to set a port on which 69 | | this policy is located. By default, flash always starts looking for this 70 | | policy at port 843. You are free to set your own port here, if you are 71 | | not allowed to bind something to some of the lower ports. 72 | | 73 | | This will cause a connection delay of 2-3 seconds, and don't forget to 74 | | tell the client where the policy is located. In JS: 75 | | WebSocket.loadFlashPolicyFile("xmlsocket://myhost.com:61011"); 76 | */ 77 | 78 | 'flashPort' => 843, 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | ZMQ socket push persistent id 83 | |-------------------------------------------------------------------------- 84 | | you should make it unique with environment there are multi environment on your server 85 | | sprintf('latchet.push.%s', App::environment()); 86 | */ 87 | 'socketPushId' => 'latchet.push', 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | ZMQ socket pull persistent id 92 | |-------------------------------------------------------------------------- 93 | | you should make it unique with environment there are multi environment on your server 94 | | sprintf('latchet.pull.%s', App::environment()); 95 | */ 96 | 'socketPullId' => 'latchet.pull' 97 | 98 | ); 99 | -------------------------------------------------------------------------------- /src/commands/ListenCommand.php: -------------------------------------------------------------------------------- 1 | latchet = $app->make('latchet'); 36 | parent::__construct(); 37 | } 38 | 39 | /** 40 | * Execute the console command. 41 | * 42 | * @return void 43 | */ 44 | public function fire() 45 | { 46 | $loop = \React\EventLoop\Factory::create(); 47 | 48 | if(\Config::get('latchet::enablePush')) 49 | { 50 | $this->enablePush($loop); 51 | } 52 | 53 | // Set up our WebSocket server for clients wanting real-time updates 54 | $webSock = new \React\Socket\Server($loop); 55 | $webSock->listen($this->option('port'), '0.0.0.0'); // Binding to 0.0.0.0 means remotes can connect 56 | $webServer = new \Ratchet\Server\IoServer( 57 | new \Ratchet\Http\HttpServer( 58 | new \Ratchet\WebSocket\WsServer( 59 | new \Ratchet\Wamp\WampServer( 60 | $this->latchet 61 | ) 62 | ) 63 | ), $webSock 64 | ); 65 | 66 | 67 | 68 | if(\Config::get('latchet::allowFlash')) 69 | { 70 | $this->allowFlash($loop); 71 | } 72 | 73 | $this->info('Listening on port ' . $this->option('port')); 74 | $loop->run(); 75 | } 76 | 77 | /** 78 | * Allow Flash sockets to connect to our server. 79 | * For this we have to listen on port 843 and return 80 | * the flashpolicy 81 | * 82 | * @param React\EventLoop\StreamSelectLoop $loop 83 | * @return void 84 | */ 85 | protected function allowFlash($loop) 86 | { 87 | // Allow Flash sockets (Internet Explorer) to connect to our app 88 | $flashSock = new \React\Socket\Server($loop); 89 | $flashSock->listen(\Config::get('latchet::flashPort'), '0.0.0.0'); 90 | $policy = new \Ratchet\Server\FlashPolicy; 91 | $policy->addAllowedAccess('*', $this->option('port')); 92 | $webServer = new \Ratchet\Server\IoServer($policy, $flashSock); 93 | 94 | $this->info('Flash connection allowed'); 95 | } 96 | 97 | /** 98 | * Enable the option to push messages from 99 | * the Server to the client 100 | * 101 | * @param React\EventLoop\StreamSelectLoop $loop 102 | * @return void 103 | */ 104 | protected function enablePush($loop) 105 | { 106 | // Listen for the web server to make a ZeroMQ push after an ajax request 107 | $context = new \React\ZMQ\Context($loop); 108 | $pull = $context->getSocket(\ZMQ::SOCKET_PULL, \Config::get('latchet::socketPullId', sprintf('latchet.pull.%s', \App::environment()))); 109 | $pull->bind('tcp://127.0.0.1:'.\Config::get('latchet::zmqPort')); // Binding to 127.0.0.1 means the only client that can connect is itself 110 | $pull->on('message', array($this->latchet, 'serverPublish')); 111 | 112 | $this->info('Push enabled'); 113 | } 114 | 115 | 116 | /** 117 | * Get the console command options. 118 | * 119 | * @return array 120 | */ 121 | protected function getOptions() 122 | { 123 | return array( 124 | array('port', 'p', InputOption::VALUE_OPTIONAL, 'The Port on which we listen for new connections', \Config::get('latchet::socketPort')), 125 | ); 126 | } 127 | 128 | } -------------------------------------------------------------------------------- /src/Sidney/Latchet/Latchet.php: -------------------------------------------------------------------------------- 1 | container = $container; 68 | 69 | $this->topicEventHandlers = new RouteCollection; 70 | 71 | $this->enablePush = \Config::get('latchet::enablePush'); 72 | 73 | if($this->enablePush) 74 | { 75 | //this is a optional dependency 76 | //if we enabled push in the config file make 77 | //shure reaxt/zmq ist loaded 78 | if(!class_exists('\React\ZMQ\Context')) 79 | { 80 | throw new LatchetException("react/zmq dependency is required if push is enabled"); 81 | } 82 | 83 | $this->pusher = new LatchetPusher; 84 | } 85 | } 86 | 87 | /** 88 | * Create and add a new handler to the 89 | * RouteCollection a.k.a topicEventHandlers 90 | * 91 | * @param string $pattern 92 | * @param string $controller 93 | * @return void 94 | */ 95 | public function topic($pattern, $controller) 96 | { 97 | if(is_subclass_of($controller, '\Sidney\Latchet\BaseTopic')) 98 | { 99 | $topicEventHandler = new TopicEventHandler($pattern, array('_controller' => $this->getCallback($controller))); 100 | $this->topicEventHandlers->add($pattern, $topicEventHandler); 101 | } 102 | else 103 | { 104 | throw new LatchetException($controller . " has to extend BaseTopic"); 105 | } 106 | } 107 | 108 | /** 109 | * Create a new connection handler instance 110 | * 111 | * @param string $controller 112 | * @return void 113 | */ 114 | public function connection($controller) 115 | { 116 | if(is_subclass_of($controller, '\Sidney\Latchet\BaseConnection')) 117 | { 118 | $this->connectionEventHandler = new ConnectionEventHandler($this->getCallback($controller)); 119 | } 120 | else 121 | { 122 | throw new LatchetException($controller . " has to extend BaseConnection"); 123 | } 124 | } 125 | 126 | /** 127 | * Push a message to a client 128 | * This function get's fired e.g after a ajax request and not 129 | * after a websocket request. Because of that we don't have access 130 | * to all the connections and there for have to connect to the 131 | * latchet/ratchet server 132 | * 133 | * @param string $channel 134 | * @param array $message 135 | * @return void 136 | */ 137 | public function publish($channel, $message) 138 | { 139 | if(!$this->enablePush) 140 | { 141 | throw new LatchetException("Publish not allowed."); 142 | } 143 | 144 | $message = array_merge(array('topic' => $channel), $message); 145 | $this->getSocket()->send(json_encode($message)); 146 | } 147 | 148 | /** 149 | * get zmqSocket to push messages 150 | * 151 | * @return ZMQSocket instance 152 | */ 153 | protected function getSocket() 154 | { 155 | //we don't have to connect the socket 156 | //for every new message sent 157 | if(isset($this->socket)) 158 | { 159 | return $this->socket; 160 | } 161 | else 162 | { 163 | return $this->connectZmq(); 164 | } 165 | } 166 | 167 | /** 168 | * Connect to socket 169 | * 170 | * @return ZMQSocket instance 171 | */ 172 | protected function connectZmq() 173 | { 174 | $context = new \ZMQContext(); 175 | $this->socket = $context->getSocket(\ZMQ::SOCKET_PUSH, \Config::get('latchet::socketPushId', sprintf('latchet.push.%s', \App::environment()))); 176 | $this->socket->connect("tcp://localhost:".\Config::get('latchet::zmqPort')); 177 | 178 | return $this->socket; 179 | } 180 | 181 | /** 182 | * Redirect serverPublish to LathcetPusher 183 | * 184 | * @param string $message 185 | * @return void 186 | */ 187 | public function serverPublish($message) 188 | { 189 | $this->pusher->serverPublish($message); 190 | } 191 | 192 | /** 193 | * Dispatch the 'request' 194 | * 195 | * @param string $event 196 | * @param array $variables 197 | * @return void 198 | */ 199 | protected function dispatch($event, $variables = array()) 200 | { 201 | $eventHandler = $this->findEventHandler($variables); 202 | $eventHandler->run($event); 203 | } 204 | 205 | /** 206 | * Create instance of the given Controller 207 | * 208 | * @param string $controller 209 | * @return Object 210 | */ 211 | protected function getCallback($controller) 212 | { 213 | //TODO: check if instance of BaseChannel 214 | $ioc = $this->container; 215 | $instance = $ioc->make($controller); 216 | 217 | return $instance; 218 | } 219 | 220 | /** 221 | * Find a eventHandler and set all the necessary parameters. 222 | * This can either be a topic or a connection eventhandler 223 | * 224 | * @param array $variables 225 | * @return mixed (EventInterface instance) 226 | */ 227 | protected function findEventHandler($variables) 228 | { 229 | if(array_key_exists('topic', $variables)) 230 | { 231 | $eventHandler = $this->findTopicEvent($variables); 232 | 233 | //throw error if no topicHandler is defined 234 | if(!$eventHandler instanceof TopicEventHandler) 235 | { 236 | throw new LatchetException("No TopicHandler defined"); 237 | } 238 | } 239 | else 240 | { 241 | $eventHandler = $this->connectionEventHandler; 242 | 243 | //throw error if no connectionHandler is defined 244 | if(!$eventHandler instanceof ConnectionEventHandler) 245 | { 246 | throw new LatchetException("No ConnectionHandler defined"); 247 | } 248 | 249 | $eventHandler->setWsParameters($variables); 250 | } 251 | 252 | return $eventHandler; 253 | } 254 | 255 | /** 256 | * Find and return a TopicEvent in our Symfony RouteCollection 257 | * 258 | * @param array $variables 259 | * @return Sidne\Latchet\TopicEventHandler 260 | */ 261 | protected function findTopicEvent($variables) 262 | { 263 | $topicName = $this->getTopicName($variables['topic']); 264 | 265 | try 266 | { 267 | $parameters = $this->getUrlMatcher($topicName)->match('/'.$topicName); 268 | } 269 | catch (ExceptionInterface $e) 270 | { 271 | if ($e instanceof ResourceNotFoundException) 272 | { 273 | throw new LatchetException("Requested Channel not found"); 274 | } 275 | } 276 | 277 | $eventHandler = $this->topicEventHandlers->get($parameters['_route']); 278 | 279 | $eventHandler->setWsParameters($variables); 280 | $eventHandler->setRequestParameters($parameters); 281 | 282 | return $eventHandler; 283 | } 284 | 285 | /** 286 | * Get the name of a topic/channel 287 | * just for convenience 288 | * 289 | * @param Ratchet\Wamp\Topic $topic 290 | * @return string 291 | */ 292 | protected function getTopicName(Topic $topic) 293 | { 294 | return $topic->getId(); 295 | } 296 | 297 | /** 298 | * Create a new URL matcher instance. 299 | * 300 | * @param string $topic 301 | * @return Symfony\Component\Routing\Matcher\UrlMatcher 302 | */ 303 | protected function getUrlMatcher($topicName) 304 | { 305 | $context = new RequestContext($topicName); 306 | 307 | return new UrlMatcher($this->topicEventHandlers, $context); 308 | } 309 | 310 | //possible actions 311 | //array('subscribe', 'publish', 'call', 'unsubscribe', 'open', 'close', 'error') 312 | public function onSubscribe(Conn $connection, $topic) 313 | { 314 | if($this->enablePush) 315 | { 316 | //register subscribe in case we want to pusher something serverside 317 | $this->pusher->addSubscriber($connection, $topic); 318 | } 319 | 320 | $this->dispatch('subscribe', compact('connection', 'topic')); 321 | } 322 | 323 | public function onPublish(Conn $connection, $topic, $message, array $exclude, array $eligible) 324 | { 325 | $this->dispatch('publish', compact('connection', 'topic', 'message', 'exclude', 'eligible')); 326 | } 327 | 328 | public function onCall(Conn $connection, $id, $topic, array $params) 329 | { 330 | $this->dispatch('call', compact('connection', 'id', 'topic', 'params')); 331 | } 332 | 333 | public function onUnSubscribe(Conn $connection, $topic) 334 | { 335 | $this->dispatch('unsubscribe', compact('connection', 'topic')); 336 | } 337 | 338 | public function onOpen(Conn $connection) 339 | { 340 | $this->dispatch('open', compact('connection')); 341 | } 342 | 343 | public function onClose(Conn $connection) 344 | { 345 | if ($this->enablePush) 346 | { 347 | // After connection is closed, the subscriber topic must be reset 348 | $this->pusher->removeSubscriber($connection); 349 | } 350 | 351 | $this->dispatch('close', compact('connection')); 352 | } 353 | 354 | public function onError(Conn $connection, \Exception $exception) 355 | { 356 | $this->dispatch('error', compact('connection', 'exception')); 357 | } 358 | 359 | } 360 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Latchet (Laravel 4 Package) 2 | 3 | ##Important 4 | 5 | This is not even an alpha version. There's still a lot of stuff going on. The docs aren't finished, and some of the code needs to get polished. So please don't use the package for production - yet. If you want to keep up to date you can follow me on [Twitter](https://twitter.com/sidneywidmer "Twitter") 6 | 7 | ##Intro 8 | 9 | Latchet takes the hassle out of PHP backed realtime apps. At its base, it's a extended version of [Ratchet](https://github.com/cboden/Ratchet "Ratchet") to work nicely with laravel. 10 | 11 | If you're finished setting up a basic WampServer, you'll have something like this: 12 | 13 | Latchet::topic('chat/room/{roomid}', 'ChatRoomController'); 14 | 15 | ## Installation 16 | 17 | ### Earlybird 18 | 19 | Until i submit the pakage to packagist, include it directly from github. 20 | 21 | "repositories": [ 22 | { 23 | "type": "vcs", 24 | "url": "https://github.com/sidneywidmer/latchet" 25 | } 26 | ], 27 | "require": { 28 | "sidney/latchet": "dev-master" 29 | } 30 | 31 | ### Required setup 32 | 33 | In the `require` key of `composer.json` file add the following 34 | 35 | "sidney/latchet": "dev-master" 36 | 37 | Run the Composer update comand 38 | 39 | $ composer update 40 | 41 | In your `config/app.php` add `'Sidney\Latchet\LatchetServiceProvider'` to the end of the `$providers` array 42 | 43 | 'providers' => array( 44 | 45 | 'Illuminate\Foundation\Providers\ArtisanServiceProvider', 46 | 'Illuminate\Auth\AuthServiceProvider', 47 | ... 48 | 'Sidney\Latchet\LatchetServiceProvider', 49 | 50 | ), 51 | 52 | At the end of `config/app.php` add `'Latchet' => 'Sidney\Latchet\LatchetFacade'` to the `$aliases` array 53 | 54 | 'aliases' => array( 55 | 56 | 'App' => 'Illuminate\Support\Facades\App', 57 | 'Artisan' => 'Illuminate\Support\Facades\Artisan', 58 | ... 59 | 'Latchet' => 'Sidney\Latchet\LatchetFacade', 60 | 61 | ), 62 | 63 | 64 | ### Configuration 65 | 66 | Publish the config file with the following artisan command: `php artisan config:publish sidney/latchet` 67 | 68 | There are not a lot of configuration options, the most important will be `enablePush` and `zmqPort`. This requires some extra configuration on your server and is discussed in the next section. 69 | 70 | The rest of the options should be pretty self self-explanatory. 71 | 72 | ### Enable push 73 | 74 | Todo, until then -> Braindump: 75 | 76 | * Installing zermq; (in this order) ubuntu 77 | * http://johanharjono.com/archives/633 (if this doesn't work, compile from source) 78 | * http://www.zeromq.org/intro:get-the-software 79 | * http://www.zeromq.org/bindings:php (eventualli apt-get install pkg-config, make) 80 | * add extension=zmq.so to php.ini 81 | * check if extension loaded php -m 82 | * check if zeromq package (zlib1g) is installed dpkg --get-selections 83 | 84 | ## Usage 85 | 86 | ### Introduction 87 | 88 | Like mentioned before, Latchet is based on Ratchet and just extends its functionality with some nice extra features like passing parameters in ***topics***. But Latchet also removes some of the flexibility Ratchet provides for the sake of simplicity. Latchet solely focuses on providing a WampServer for your applications. 89 | 90 | #### Topics 91 | 92 | I would really recommend you to read through the [Ratchet docs](http://socketo.me/docs/ "Ratchet docs"). They explain the basic principles very clearly. 93 | 94 | Once you get the hang of it, topics are really easy to understand. Imagine a standart laravel route as you know it. 95 | 96 | Route::get('my/route/{parameter}', 'MyController@action'); 97 | 98 | Topics (or if you are familiar with other forms of messaging, channels) are the same for websocket connections. 99 | There's always a client which subscribes to a topic. If other clients connect to the same topic, they can then broadcast messages to this subscribed topic or a specific client connected to this topic. See how to register a Controller which handles incomming connections in the next chapter. 100 | 101 | ### Server 102 | 103 | Everything we're doing in this section is just to set up a WampServer which will then be started from the command line and listen on incomming connections. Basically there are two different handlers we have to set up. One which handles different connection actions and (at least) one for our topic(s) actions. 104 | 105 | To clarify stuff: 106 | 107 | ***Connection actions are:*** 108 | 109 | * open 110 | * close 111 | * error 112 | 113 | ***Topic actions are:*** 114 | 115 | * subscribe 116 | * publish 117 | * call 118 | * unsubscribe 119 | 120 | #### Generate files - the artisan way 121 | 122 | To simplify the process, there's an easy to use artisan command to generate all the necessary files: 123 | 124 | $ php artisan latchet:generate 125 | 126 | This will do two things. First it'll create the folder app/socket and copy two files in this folder. One to handle incomming connections (Connection.php) and one to handle subscriptions e.t.c to a topic (TestTopic.php). And second it'll register theses two new classes at the end of your app/routes.php file. 127 | 128 | Make shure to add the socket folder to the laravel class loader or your composer.json file. The easiest way would be to add `app_path().'/socket',` in your `app/start/global.php` file. 129 | 130 | ClassLoader::addDirectories(array( 131 | 132 | app_path().'/controllers', 133 | ... 134 | app_path().'/socket', 135 | 136 | )); 137 | 138 | Basically you could now start the server and subscribe to `test-topic`. I'd recommend to check the next two chapters as they explain what you can do with the newly added connection and topic handlers. 139 | 140 | #### Connection handler 141 | 142 | If you've ran the above `artisan:generate` command, you'll have a connection handler registered in your routes.php file. It defines how to react on different connection actions. So anytime a new connection to the server is establish, we'll ask the controller what to do. Easy as a pie: 143 | 144 | Latchet::connection('Connection'); 145 | 146 | It handles the following actions: 147 | 148 | * open 149 | * close 150 | * error 151 | 152 | All three actions get a `Connection` object as `$connection`. Read more about this objectin the official Ratchet api documentation:[Ratchet API - Class WampConnection](http://socketo.me/api/class-Ratchet.Wamp.WampConnection.html "Ratchet API") 153 | 154 | For example, you could here close a connection `$connection->close()`, or add some additional info to the connection object: 155 | 156 | $connection->Chat = new \StdClass; 157 | $connection->Chat->rooms = array(); 158 | $connection->Chat->name = $connection->WAMP->sessionId; 159 | 160 | From now on, `$connection->Chat->name` will always be available in the `$connection` variable which gets passed to most of the action methods. 161 | 162 | Because the server should be constantly running, there's an extra function for error handling. Whenever an error occurs, the error function is triggered. In the default template, which gets generated by the artisan command, the error just gets thrown again. This stops the server and the error is displayed in your console. For production it's important that you don't rethrow the error, but instead log it. An error gets thrown if someone for example tries to connect to a non existend topic. 163 | 164 | #### Topic handlers 165 | 166 | Now it gets interesting. With latchet you can register new topics and pass parameters to it: 167 | 168 | Latchet::topic('chat/room/{roomid}', 'ChatRoomController'); 169 | 170 | And in the topic handler (e.g. `app/socket/ChatRoomController.php`): 171 | 172 | broadcast($topic, array('msg' => 'New broadcasted message!')) 191 | } 192 | 193 | There are other methods to handle the following actions: 194 | 195 | * subscribe 196 | * publish 197 | * call 198 | * unsubscribe 199 | 200 | #### Push 201 | 202 | If you have push enabled in your config file, it's also possible to publish messages from different locations in your application. 203 | 204 | Latchet::publish('chat/room/lobby', array('msg' => 'foo')); 205 | 206 | Like that you could for example react to ajax requests. 207 | 208 | 209 | #### Start the server 210 | 211 | Use the following artisan command to start the server: 212 | 213 | $ sudo php artisan latchet:listen 214 | 215 | Also make shure to read the Ratchet docs on how to deploy your app: [Ratchet Docs - Deployment](http://socketo.me/docs/deploy "Ratchet Docs") 216 | 217 | One word to the environment: Because the whole application will be running from the console, make shure to pass the desired environment as a parameter in your console command e.g: 218 | 219 | $ sudo php artisan latchet:listen --env=local 220 | 221 | ### Client 222 | 223 | Now that we have our server up and running, we somehow need to connect to it right? [Autobahn JS](http://autobahn.ws/js "Autobahn JS") to the rescue. 224 | 225 | #### Javascript / Legacy browsers 226 | 227 | [Autobahn JS](http://autobahn.ws/js "Autobahn JS") handles the client side for us. Make shure to check their docs, in the meantime, here's a basic example: 228 | 229 | conn = new ab.Session( 230 | 'ws://latchet.laravel-devbox.dev:1111', // The host (our Latchet WebSocket server) to connect to 231 | function() { // Once the connection has been established 232 | conn.subscribe('chat/room/lobby', function(topic, event) { 233 | console.log('event: '); 234 | console.log(event); 235 | }); 236 | }, 237 | function() { 238 | // When the connection is closed 239 | console.log('WebSocket connection closed'); 240 | }, 241 | { 242 | // Additional parameters, we're ignoring the WAMP sub-protocol for older browsers 243 | 'skipSubprotocolCheck': true 244 | } 245 | ); 246 | 247 | For older browsers, which do not support websockts, make shure to inlcude [web-socket-js](https://github.com/gimite/web-socket-js "web-socket-js") and allow flash in your config file. 248 | 249 | #### Demoapp 250 | 251 | Check the demo application built with laravel, the latchet package, autobahn.js and backbone: [whatup](https://github.com/sidneywidmer/whatup "whatup") 252 | And for a live demo: [whatup.im](http://whatup.im "whatup.im") 253 | --------------------------------------------------------------------------------