Fuente:
En este post, vamos a hacer un ejemplo de cómo usar notificaciones en tiempo real emitidos desde Laravel a un cliente con Angular.
El flujo de información es tal como sigue: en Laravel tenemos un formulario para enviar un mensaje a una ruta de Laravel que disparará un evento. El manejador de ese evento (listener) recogerá el mensaje y lo enviará a Redis por un canal que nombraremos nosotros. Redis enviará el mensaje por otro canal que el servidor Node estará escuchando. Node emite un evento por un socket que será escuchado por Socket.io desde el cliente Angular. Por último, el cliente Angular creará una notificación en cuanto le llegue el mensaje desde el socket.
En primer lugar, debemos tener instalado en nuestro PC lo siguiente:
- Redis desde este repositorio.
- Node.js desde su web oficial.
- Composer desde este link.
- Xampp o cualquier servidor web para el desarollo web en local, desde aquí.
Un vez descargados e instalados los programas necesarios, instalaremos de forma global con composer el instalador para proyectos nuevos en Laravel con el comando:
1 |
composer global require "laravel/installer" |
Ahora, crearemos en nuestro servidor local, en mi caso Xampp, una carpeta con un proyecto nuevo de laravel. Para ello, navegamos al directorio «C:\xampp\htdocs» y con el atajo de teclado SHIFT+CLICK DERECHO del ratón , abrimos un Terminal y ejecutamos el comando:
1 |
laravel new larachat |
Siendo «larachat» el nombre de nuestro proyecto Laravel desde el cual empezaremos a trabajar en este tutorial.
A continuación, estos son los pasos divido en 3 partes:
- Parte Laravel:
- Instalar el paquete Redis.
- Configurar Laravel para gestionar la emisión de mensajes (broadcasting) por Redis.
- Crear rutas para mostrar formulario y gestión del mensaje.
- Crear controlador que gestione los mensajes.
- Crear evento y su manejador (event and listener).
- Crear la vista para el envio del mensaje.
- Crear script con jquery para el envio del mensaje por AJAX e incrustarlo en la vista de envio del mensaje.
- Parte Node:
- Crear un package.json con las dependencias necesarias.
- Crear un servidor con Node que usará Redis, Socket.io y Express principalmente.
- Parte Angular:
- Crear un bower.json con las dependencias.
- Crear las rutas del chat.
- Crear las vistas del chat.
- Crear el controlador de los mensajes recibidos por socket.
Parte Laravel
Instalamos el paquete de Redis para Laravel:
1 |
composer require predis/predis |
Para evitar conflicto con Redis en el entorno PHP, renombramos el Facade «Redis» de Laravel a «LRedis» , tal que:
1 2 3 4 |
De.. 'Redis' => 'Illuminate\Support\Facades\Redis', A.. 'LRedis' => 'Illuminate\Support\Facades\Redis', |
Editamos el archivo «.env» para que use el driver Redis:
1 2 |
. . . . BROADCAST_DRIVER=redis |
Configuramos las rutas:
1 2 |
Route::get('writemessage', 'SocketController@writeMessage'); Route::post('sendmessage', 'SocketController@sendMessage'); |
Turno del controlador:
1 |
php artisan make:controller SocketController |
Abrimos y editamos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?php namespace App\Http\Controllers; use App\Events\SendNotification; use Illuminate\Http\Request; class SocketController extends Controller { public function writeMessage() { return view('writemessage'); } public function sendMessage(Request $request){ $data = [ "message" => $request->message, "channel" => "user.47", "emit" => "notification" ]; \Event::fire(new SendNotification($data)); // event(new SendNotification($data)); } } |
Creamos el evento y su manejador:
1 2 3 |
php artisan make:event SendNotification php artisan make:listener SendNotificationFired --event="SendNotification" |
Abrimos y editamos el evento:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class SendNotification { use InteractsWithSockets, SerializesModels; public $message; public function __construct($data) { $this->message = $data; } . . . . } |
Abrimos y editamos el manejador del evento:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
use App\Events\SendNotification; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use LRedis; class SendNotificationFired { public function handle(SendNotification $event) { $redis = LRedis::connection(); $redis->publish( $event->message['channel'] , json_encode( $event->message) ); } } |
Creamos la vista de formulario para el envio de mensaje:
writemessage.blade.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title> Laravel Real-Time Notifications </title> </meta> <meta content="{{ csrf_token() }}" name="csrf-token"> <link href="//netdna.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet"> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"> <style type="text/css"> h1 { width: 100%; font-size: 15px; background: #03A9F4; padding: 20px; color: white; box-shadow: 0 5px 5px 0 rgba(0, 0, 0, 0.05); border-radius: 3px 3px 0 0; } .nb-demo { background: linear-gradient( 45deg, #E91E63, #F378A2 ); padding: 30px 0; } .material-form { max-width: 500px; padding: 30px 15px; margin: 30px auto 0; display: block; background: #FFF; overflow: hidden; } .material-form .group { position: relative; margin-bottom: 45px; } .material-form .form-inner { overflow: hidden; } .material-form form { padding-top: 20px; } .material-form input { font-size: 18px; padding: 10px 10px 10px 5px; display: block; width: 100%; border: none; border-bottom: 1px solid #f0f0f0; } .material-form textarea { font-size: 18px; padding: 10px 10px 10px 5px; display: block; width: 100%; max-width: 100%; min-width: 100%; border: none; border-bottom: 1px solid #f0f0f0; } .material-form input:focus, .material-form textarea:focus { outline: none; } /*-------------------labels--------------*/ .material-form label { color: #999; font-size: 18px; font-weight: normal; position: absolute; pointer-events: none; left: 5px; top: 10px; transition: 0.2s ease all; -moz-transition: 0.2s ease all; -webkit-transition: 0.2s ease all; } /* active state */ .material-form input:focus ~ label, .material-form input:valid ~ label, .material-form textarea:focus ~ label, .material-form textarea:valid ~ label { top: -20px; font-size: 14px; color: #E91E63; } .material-form .bar { position: relative; display: block; width: 100%; } .material-form .bar:before, .bar:after { content: ''; height: 2px; width: 0; bottom: -1px; position: absolute; background: #E91E63; transition: 0.2s ease all; -moz-transition: 0.2s ease all; -webkit-transition: 0.2s ease all; } .material-form .bar:before { left: 50%; } .material-form .bar:after { right: 50%; } /* active state */ .material-form input:focus ~ .bar:before, .material-form input:focus ~ .bar:after, .material-form textarea:focus ~ .bar:before, .material-form textarea:focus ~ .bar:after { width: 100%; } /*-------SUBMIT----------*/ .material-form input.btn { display: inline-block; padding: 10px 15px; color: #fff; width: auto !important; text-transform: uppercase; font-size: 16px; border-radius: 0; box-shadow: 1px 1px 5px #ddd; transition: all 0.4s ease-in-out; border: 1px solid #F378A2; background: #E91E63; } .material-form input.btn:hover, .material-form input.btn:focus, .material-form input.btn:active { color: #fff; outline: 0; box-shadow: none; } /*----------------Highlights------------*/ .material-form .highlight { position: absolute; height: 60%; width: 100px; top: 25%; left: 0; pointer-events: none; opacity: 0.5; } /*---------------on active---------------*/ .material-form input:focus ~ .highlight { -webkit-animation: inputHighlighter 0.3s ease; -moz-animation: inputHighlighter 0.3s ease; animation: inputHighlighter 0.3s ease; } /*----------------animation-------------*/ @-webkit-keyframes inputHighlighter { from { background: #E91E63; } to { width: 0; background: transparent; } } @-moz-keyframes inputHighlighter { from { background: #E91E63; } to { width: 0; background: transparent; } } @keyframes inputHighlighter { from { background: #E91E63; } to { width: 0; background: transparent; } } .material-form{ padding-top: 0px !important; } </style> </link> </link> </meta> </head> <body> <section class="nb-demo"> <div class="container"> <div class="row"> <div class="col-sm-12"> <div class="material-form"> <div class="form-inner"> <h1> Envia una Notificación </h1> <form action="sendmessage" id="formMessage" method="POST"> <!-- <div class="group"> <input required="" type="text"> <span class="highlight"> </span> <span class="bar"> </span> <label> Name </label> </input> </div> --> <!-- <div class="group"> <input required="" type="text"> <span class="highlight"> </span> <span class="bar"> </span> <label> Email </label> </input> </div> --> <!-- <div class="group"> <input required="" type="text"> <span class="highlight"> </span> <span class="bar"> </span> <label> Subject </label> </input> </div> --> <div class="group"> <textarea id="message" name="message" required="" type="text"> </textarea> <span class="highlight"> </span> <span class="bar"> </span> <label> Message </label> </div> <div class="group"> <input class="submit" type="submit" value="Send Message"> </input> </div> {{ csrf_field() }} </form> </div> </div> </div> </div> </div> </section> <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"> </script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"> </script> <script src="https://cdn.socket.io/socket.io-1.4.5.js"> </script> <script> $(document).on('ready', function (){ $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); var m = document.getElementById("message"); m.value = ""; $(document).on('submit','#formMessage',function (e){ e.preventDefault(); var url = '{{ URL::to('/sendmessage') }}'; var msg = m.value.trim()=="" ? false : m.value.trim(); if(msg){ $.post( url ,{ message: msg } ); m.value = ""; m.focus(); } }); }); </script> </body> </html> |
Parte NODE
En la raiz de nuestro proyecto Laravel, creamos un diretorio «node» y dentro creamos un archivo con las dependencias necesarias de nuestro servidor Node ejecutando el comando «npm init»:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{ "name": "chat", "version": "0.0.1", "description": "none", "main": "server.js", "author": "Francisco Fontanes", "license": "ISC", "private": true, "scripts": { "start":"nodemon -e js,ejs,html -w .env -w . -w public -w views -w routes -w models server.js" }, "dependencies": { "express": "^4.13.4", "ioredis": "^1.15.1", "jsonwebtoken": "^7.0.0", "node-env-file": "^0.1.8", "socket.io": "^1.4.6" } } |
Instalamos las depencias con el comando «npm install» que creará la carpeta «node_modules» con las dependencias que necesitamos.
Para tener centralizadas las constantes de nuestra aplicación en general, añadiremos esta constante de Node.js al archivo de variables de entorno de Laravel:
1 |
SOCKETIO_PORT=8890 |
Turno de crear el servidor Node.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
var $path = './node_modules/', ENV = require($path + 'node-env-file')('../.env'), app = require($path + 'express')(), server = require('http').Server(app), io = require($path + 'socket.io')(server), jwt = require($path + 'jsonwebtoken'), Redis = require($path + 'ioredis'), redis = new Redis(); redis.psubscribe('*', function(err, count) {}); redis.on('pmessage', function(subscribed, channel, data) { var $data = JSON.parse(data); console.log("suscrito a: ", subscribed); // suscrito a: * console.log("canal es: ", channel); // canal es: message console.log("mensaje: ", $data.message); // mensaje: hola mundo console.log("evento: ", $data.emit); // evento: hola mundo io.emit($data.emit,{username: 'laravel',message: $data.message}); }); server.listen(ENV.SOCKETIO_PORT, function() { console.log('listening on *:' + ENV.SOCKETIO_PORT); }); |
Parte Angular
Creamos un directorio en «C:\xampp\htdocs» que llamaremos «angular»
Instalamos los siguientes paquetes de Node – Yeoman, Bower, Grunt y Angular Generator – de forma global:
1 |
npm install -g yo bower grunt-cli generator-angular |
Apartir de aquí, desarollaremos nuestra aplicacion Angular generando los archivos a base de comandos tal y como hacemos con Laravel.
Creamos la app de angular:
1 |
yo angular Messages |
Descargamos los módulos que necesitamos:
1 |
bower install angular angular-ui-router angular-resource angular-socket-io angular-web-notification --save |
Creamos las rutas:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
var app = angular .module('MessagesApp', [ 'ui.router', 'ngResource', 'btford.socket-io', 'angular-web-notification', ]); app.config(['$stateProvider', '$urlRouterProvider',function ($stateProvider,$urlRouterProvider){ $urlRouterProvider.otherwise('/'); $stateProvider .state('notifications', { url: '/notifications', title: 'notifications', controller: 'NotificationsCtrl', templateUrl: 'views/notifications.html' }) .state('main', { url: '/', title: 'main', controller: 'MainCtrl', templateUrl: 'views/main.html' }) }]) |
Creamos el servicio Socket para usarlo en cualquier controlador donde lo inyectemos:
1 |
yo angular:factory Socket |
Abrimos y editamos:
1 2 3 4 5 |
.factory('Socket', function (socketFactory) { return socketFactory({ ioSocket: io.connect('http://localhost:8890',{secure: true, query: 'jwt=' + localStorage.jwt}) }); }); |
Creamos el controlador para recibir las notificaciones:
1 |
yo angular:controller Notifications |
Abrimos y editamos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
angular.module('MessagesApp').controller('NotificationsCtrl', ['Socket', 'webNotification', function(Socket, webNotification) { Socket.connect(); Socket.on('notification', function(data) { console.log('data: ', data); webNotification.showNotification('@' + data.username + ' dice: ', { body: data.message, icon: 'images/laravel.png', onClick: function onNotificationClicked() { console.log('Notification clicked.'); }, autoClose: 4000 }, function onShow(error, hide) { if (error) { window.alert('Unable to show notification: ' + error.message); } else { console.log('Notification Shown.'); setTimeout(function hideNotification() { console.log('Hiding notification....'); hide(); }, 5000); } }); }); } ]); |
Creamos una vista básica y simple para ver las notificaciones:
1 |
yo angular:view Notifications |
Abrimos y editamos la vista index.html añadiéndole al final el script de socket.io:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
<!doctype html> <html> <head> <meta charset="utf-8"> <title></title> <meta name="description" content=""> <meta name="viewport" content="width=device-width"> <!-- Place favicon.ico and apple-touch-icon.png in the root directory --> <!-- build:css(.) styles/vendor.css --> <!-- bower:css --> <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" /> <!-- endbower --> <!-- endbuild --> <!-- build:css(.tmp) styles/main.css --> <link rel="stylesheet" href="styles/main.css"> <!-- endbuild --> </head> <body ng-app="MessagesApp"> <div class="header"> <div class="navbar navbar-default" role="navigation"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#js-navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#/">chat</a> </div> <div class="collapse navbar-collapse" id="js-navbar-collapse"> <ul class="nav navbar-nav"> <li class="active"><a href="/">Home</a></li> <li><a ui-sref="notifications">Notificaciones</a></li> <li><a ng-href="#/">Contact</a></li> </ul> </div> </div> </div> </div> <div class="container"> <div ui-view></div> </div> <div class="footer"> <div class="container"> <p><span class="glyphicon glyphicon-heart"></span> from the Yeoman team</p> </div> </div> <!--Socket IO--> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.7.2/socket.io.js"></script> <!-- build:js(.) scripts/vendor.js --> <!-- bower:js --> . . . . <!-- endbower --> <!-- endbuild --> <!-- build:js({.tmp,app}) scripts/scripts.js --> . . . . <!-- endbuild --> </body> </html> |
Por último, vamos a probar el funcionamiento, para ello hay que abrir:
- Un Terminal para Redis, como está instalado de forma global en el SO, ejecutamos «redis-cli monitor».
- Un Terminal para Node, desde la carpeta «node» ejecutamos «npm start».
- Un Terminal para la app de Angular, desde la carpeta «angular» ejecutamos «grunt serve» que abrirá el navegador, clicamos sobre la opción «Notificaciones».
- Una pestaña del navegador y vamos a esta URL: http://localhost/writemessage.
Si todo va bien, al escribir un mensaje en la pestaña donde se ejecuta «writemessage» y pulsando enviar, veremos cómo y qué recibe Redis y luego qué recibe Node en los Terminales.
Si después de enviar el mensaje, pulsamos sobre la pestaña de la app Angular, veremos que salta la notificación HTML5.
Esto es todo por este tutorial, espero que te haya gustado!