In this tutorial, you will learn how to build a realtime app with desktop notifications, using Pusher's Notifications API.
Realtime desktop notifications are now very common in modern web applications, as site owners want to keep users engaged and informed of happenings on their platform. Notifications are also a great way to build addiction, and make sure users keep coming back to your platform to see “what’s new”.
With Laravel and some Pusher magic, I will be demonstrating how to build a realtime app, with desktop notifications, utilising the Notification API.
We will build a simple News notification module which will alert everyone on a website when a new post is published. Ideally, this would be part of a larger app, but we will build it in isolation here to showcase our desktop notifications.
At the end of this tutorial, you will have learned how to:
To follow along, you will need a working knowledge of PHP and JavaScript. Basic knowledge of Laravel and Vue.js are also needed. Laravel 5.4 and Vue.js 2.3 are used.
Pusher is a service that makes it super easy to add realtime functionality to web and mobile applications. We will be using it in our application, so sign up to a free account here, create an app, and copy out the app credentials (App ID, Key and Secret) from the “App Keys” section.
For starters, let us set up our app, and do the necessary configuration. We will call the app news-talk
.
To create a new app with the Laravel installer, run this command:
1laravel new news-talk
The next set of commands should be run in the app’s root directory.
Installing the Pusher PHP library:
1composer require pusher/pusher-php-server
Installing the Laravel Frontend Dependencies (these include Bootstrap, Axios, Vue.js and a couple of other things which are nice to have):
1npm install
Installing Laravel Echo and Pusher-js which we will use to listen for broadcast events:
1npm install -S laravel-echo pusher-js
Next, we will do some more minor configuration to let Laravel know we will be using Pusher to manage our broadcasts.
Editing the .env
:
1# ./.env 2 3BROADCAST_DRIVER=pusher 4 5PUSHER_APP_ID=your_pusher_add_id 6PUSHER_APP_KEY=your_pusher_app_key 7PUSHER_APP_SECRET=your_pusher_app_secret
You can edit some more optional configuration for Pusher in the ./config/broadcasting.php
file generated by Laravel. You can see other options here.
Note: If you created your app in a different cluster to the default
us-east-1
, you must configure the cluster option. It is optional if you chose the default option.
Finally, we will configure Echo to use Pusher. We do that by uncommenting and editing the values at the bottom of resources/assets/js/bootstrap.js
:
1// ./resources/assets/js/bootstrap.js 2 3import Echo from "laravel-echo" 4 5 window.Echo = new Echo({ 6 broadcaster: 'pusher', 7 key: 'your_pusher_key' 8});
Let’s create a table for our posts. This is where data we create via our app will be persisted to. We will use a Laravel migration file, and an Eloquent model for communication with the database.
To create a Post
model run this command:
1php artisan make:model Post -m -c
The -m
and c
flags are for automatically generating the migration and controller files respectively.
Next, we edit the generated migration file located in the ./database/migrations
folder. We adjust the up
method to look like this:
1public function up() { 2 Schema::create('posts', function (Blueprint $table) { 3 $table->increments('id'); 4 $table->string('title'); 5 $table->text('description'); 6 $table->timestamps(); 7 }); 8}
Then, after editing the .env
with your database details, you can create the table with this command:
1php artisan migrate
Tip: You can read more on the Laravel .env file here
We should also edit the mass-assignable properties on the model:
1# ./app/Post.php 2 3class Post extends Model 4{ 5 6 /** 7 * The attributes that are mass assignable. 8 * 9 * @var array 10 */ 11 protected $fillable = ['title', 'description']; 12}
Next, we will add a route and controller method to save a new post.
We will be making an API call from the front-end to save our posts, so we can add the new route to the API routes at ./routes/api.php
. Whatever route defined here will be prefixed by api
, and belongs to the api
middleware group.
1# ./routes/api 2 3Route::post('/post', 'PostController@store');
Adding the corresponding controller method:
1# ./app/Http/Controllers/PostController.php 2 3use App\Post; 4 5class PostController extends Controller { 6 7 /** 8 * Saves a new post to the database 9 */ 10 public function store(Request $request) { 11 // ... 12 // validation can be done here before saving 13 // with $this->validate($request, $rules) 14 // ... 15 16 // get data to be saved in an associative array using $request->only() 17 $data = $request->only(['title', 'description']); 18 19 // save post and assign return value of created post to $post array 20 $post = Post::create($data); 21 22 // return post as response, Laravel automatically serializes this to JSON 23 return response($post, 201); 24 } 25}
Events are a great way to separate out application logic. We can define events to be triggered in our application when an action occurs, and we can define listeners, to listen for such events and carry out other activities.
Laravel allows for easy definition of events and listeners out of the box. It also includes helper functions and classes to allow us easily trigger and broadcast events.
We can create a new event with this command:
1php artisan make:event PostPublished
The event class file is created at ./app/Events
.
We can then edit it to suit our needs:
1# ./app/Events/PostPublished.php 2 3class PostPublished implements ShouldBroadcast { 4 use Dispatchable, InteractsWithSockets, SerializesModels; 5 6 public $post; 7 8 public function __construct($post) { 9 $this->post = $post; 10 } 11 12 /** 13 * Get the channels the event should broadcast on. 14 * 15 * @return Channel|array 16 */ 17 public function broadcastOn() { 18 return new Channel('posts'); 19 } 20 21 public function broadcastWith() { 22 return [ 23 'title' => $this->post->title, 24 ]; 25 } 26}
The Illuminate\Contracts\Broadcasting\ShouldBroadcast
interface on the event class is used to inform Laravel that this event should be broadcast.
The broadcastOn
method returns the channel that we want to broadcast our event on. The Channel
class is used for broadcasting on public channels. PrivateChannel
and PresenceChannel
are for private channels (these require authentication for access). You can read more about the various Pusher channels here.
By default, Laravel broadcasts all of an event class’ public properties as its payload… broadcastWith
helps us override that behaviour and choose what we want to send.
In our app, we want to dispatch the PostPublished
event after a post has been saved. In Laravel, we can dispatch events using the Event
Facade, or the event()
helper function.
To dispatch our PostPublished
event, we can edit the store
method in the PostController
, and place the event call right after the post is saved:
1# ./app/Http/Controllers/PostController.php 2 3use App\Events\PostPublished; 4 5// save post and assign return value of created post to $post array 6$post = Post::create($data); 7 8// fire PostPublished event after post is successfully added to database 9event(new PostPublished($post)); 10// or 11// \Event::fire(new PostPublished($post))
The final PostController
file will look like this:
1# ./app/Http/Controllers/PostController.php 2 3namespace App\Http\Controllers; 4 5use Illuminate\Http\Request; 6use App\Events\PostPublished; 7use App\Post; 8 9class PostController extends Controller { 10 11 /** 12 * Saves a new post to the database 13 */ 14 public function store(Request $request) { 15 // ... 16 // validation can be done here before saving 17 // with $this->validate($request, $rules) 18 // ... 19 20 // get data to save in an associative array using $request->only() 21 $data = $request->only(['title', 'description']); 22 23 // save post and assign return value of created post to $post array 24 $post = Post::create($data); 25 26 // fire PostPublished event after post is successfully added to database 27 event(new PostPublished($post)); 28 // or 29 // \Event::fire(new PostPublished($post)) 30 31 // return post as response, Laravel automatically serializes this to JSON 32 return response($post, 201); 33 } 34}
Now that we are done with building the backend, we can proceed to create our view and event listener on the Frontend.
To create a basic page view for our app, we can edit the default welcome.blade.php
file created by Laravel. We can replace its contents with the following:
1<!-- ./resources/views/welcome.blade.php --> 2 3<!DOCTYPE html> 4<html lang="{{ config('app.locale') }}"> 5<head> 6 <meta charset="utf-8"> 7 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 8 <meta name="viewport" content="width=device-width, initial-scale=1"> 9 10 <!-- CSRF Token --> 11 <meta name="csrf-token" content="{{ csrf_token() }}"> 12 13 <title>News Talk</title> 14 15 <!-- Styles --> 16 <link href="{{ asset('css/app.css') }}" rel="stylesheet"> 17 18 <style> 19 .container { 20 padding-top: 100px; 21 } 22 </style> 23 24 <!-- Scripts --> 25 <script> 26 window.Laravel = {!! json_encode([ 'csrfToken' => csrf_token(), ]) !!}; 27 </script> 28</head> 29<body> 30 31 <div id="app"> 32 <!-- home Vue component --> 33 <home></home> 34 </div> 35 36 <!-- Scripts --> 37 <script src="{{ asset('js/app.js') }}"></script> 38</body> 39</html>
Most of the code above is boilerplate Laravel HTML content with relevant scripts and CSS files attached. We will generate them later on.
We also included a Vue component (home
) which hasn’t been defined yet. Let us go ahead to create and define it.
Creating the home
Vue component:
1<!-- ./resources/assets/js/components/Home.vue --> 2<template> 3 <div class="container"> 4 <div class="row"> 5 <div class="col-sm-6 col-sm-offset-3"> 6 <div class="form-group"> 7 <label for="title">Post Title</label> 8 <input v-model="newPostTitle" id="title" type="text" class="form-control"> 9 </div> 10 <div class="form-group"> 11 <label for="description">Post Description</label> 12 <textarea v-model="newPostDesc" id="description" rows="8" class="form-control"></textarea> 13 </div> 14 <button @click="addPost(newPostTitle, newPostDesc)" 15 :class="{disabled: (!newPostTitle || !newPostDesc)}" 16 class="btn btn-block btn-primary">Submit</button> 17 </div> 18 </div> 19 </div> 20</template> 21 22<script> 23 export default { 24 data() { 25 return { 26 newPostTitle: "", 27 newPostDesc: "" 28 } 29 }, 30 created() { 31 this.listenForChanges(); 32 }, 33 methods: { 34 addPost(postName, postDesc) { 35 // check if entries are not empty 36 if(!postName || !postDesc) 37 return; 38 39 // make API to save post 40 axios.post('/api/post', { 41 title: postName, description: postDesc 42 }).then( response => { 43 if(response.data) { 44 this.newPostTitle = this.newPostDesc = ""; 45 } 46 }) 47 }, 48 listenForChanges() { 49 Echo.channel('posts') 50 .listen('PostPublished', post => { 51 if (! ('Notification' in window)) { 52 alert('Web Notification is not supported'); 53 return; 54 } 55 56 Notification.requestPermission( permission => { 57 let notification = new Notification('New post alert!', { 58 body: post.title, // content for the alert 59 icon: "https://pusher.com/static_logos/320x320.png" // optional image url 60 }); 61 62 // link to page on clicking the notification 63 notification.onclick = () => { 64 window.open(window.location.href); 65 }; 66 }); 67 }) 68 } 69 } 70 } 71</script>
In the above code, we define two methods. addPost()
and listenForChanges
. The addPost
method makes a post request to our API with the required payload when a user adds a new post.
In the listenForChanges
method, we use Echo to subscribe to the posts
channel, which is the channel we are broadcasting to, from our backend. We also listen for PostPublished
events, and define a callback that activates our desktop notification whenever an event is fired.
We are using the notifications API for desktop notifications. We first request permission to send desktop notifications, then notify the user once permission is granted.
We can also check if a browser supports desktop notifications this way:
1if (window.Notification) { 2 console.log('Notifications are supported!'); 3} else { 4 alert('Notifications aren\'t supported on your browser! :('); 5}
We create an instance of the Notification object with our post title as the body. An optional icon
parameter can also be set, as we did.
Finally, we define the component as a global component in app.js
:
1// ./resources/assets/js/app.js 2 3Vue.component('home', require('./components/Home.vue'));
We can compile our assets easily using Laravel Mix!:
1npm run dev
Now, we can navigate to the app’s homepage to see it in action. If you use Laravel Valet, you can also share the app, and visit it via another device to test the desktop notifications.
We have learned how to build an event-driven basic realtime app enabled with desktop notifications, thanks to Laravel and Pusher. As a next step, you could also learn how to build a chat application with Pusher, and integrate desktop notifications whenever a user receives a message… awesome, right?
If you’ve thought of any other great ways to use Pusher and Laravel, let us know in the comments!
The entire code for this tutorial is hosted on Github. You can look through and ask questions if you need more information.