Implementing desktop notifications in your Laravel app
You will need PHP and Laravel (5.4 or above) installed on your machine. You should have a working knowledge of PHP and JavaScript.
Realtime 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.
Our application
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:
- Broadcast events in Laravel, using Pusher
- Listen for events on channels, using Laravel Echo and Vue.js
- Use the Notification API to display desktop notifications
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.
Introduction to Pusher
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 for a free Pusher account, create an app, and copy out the app credentials (App ID, Key and Secret) from the App Keys section.
Setup and configuration
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:
laravel new news-talk
The next set of commands should be run in the app’s root directory.
Installing the Pusher PHP library:
composer 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):
npm install
Installing Laravel Echo and Pusher-js which we will use to listen for broadcast events:
npm 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
:
# ./.env
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your_pusher_add_id
PUSHER_APP_KEY=your_pusher_app_key
PUSHER_APP_SECRET=your_pusher_app_secret
Edit some more optional configuration for Pusher in the ./config/broadcasting.php
file generated by Laravel. Refer to Driver prerequisites. options.
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
:
// ./resources/assets/js/bootstrap.js
import Echo from "laravel-echo"
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'your_pusher_key'
});
Building the backend
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:
php 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:
public function up() {
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->string('title');
$table->text('description');
$table->timestamps();
});
}
Then, after editing the .env
with your database details, you can create the table with this command:
php artisan migrate
Tip: Read more on the Laravel .env file
We should also edit the mass-assignable properties on the model:
# ./app/Post.php
class Post extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['title', 'description'];
}
Saving a post
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.
# ./routes/api
Route::post('/post', 'PostController@store');
Adding the corresponding controller method:
# ./app/Http/Controllers/PostController.php
use App\Post;
class PostController extends Controller {
/**
* Saves a new post to the database
*/
public function store(Request $request) {
// ...
// validation can be done here before saving
// with $this->validate($request, $rules)
// ...
// get data to be saved in an associative array using $request->only()
$data = $request->only(['title', 'description']);
// save post and assign return value of created post to $post array
$post = Post::create($data);
// return post as response, Laravel automatically serializes this to JSON
return response($post, 201);
}
}
Working with events
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:
php artisan make:event PostPublished
The event class file is created at ./app/Events
.
We can then edit it to suit our needs:
# ./app/Events/PostPublished.php
class PostPublished implements ShouldBroadcast {
use Dispatchable, InteractsWithSockets, SerializesModels;
public $post;
public function __construct($post) {
$this->post = $post;
}
/**
* Get the channels the event should broadcast on.
*
* @return Channel|array
*/
public function broadcastOn() {
return new Channel('posts');
}
public function broadcastWith() {
return [
'title' => $this->post->title,
];
}
}
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.
Dispatching events
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:
# ./app/Http/Controllers/PostController.php
use App\Events\PostPublished;
// save post and assign return value of created post to $post array
$post = Post::create($data);
// fire PostPublished event after post is successfully added to database
event(new PostPublished($post));
// or
// \Event::fire(new PostPublished($post))
The final PostController
file will look like this:
# ./app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Events\PostPublished;
use App\Post;
class PostController extends Controller {
/**
* Saves a new post to the database
*/
public function store(Request $request) {
// ...
// validation can be done here before saving
// with $this->validate($request, $rules)
// ...
// get data to save in an associative array using $request->only()
$data = $request->only(['title', 'description']);
// save post and assign return value of created post to $post array
$post = Post::create($data);
// fire PostPublished event after post is successfully added to database
event(new PostPublished($post));
// or
// \Event::fire(new PostPublished($post))
// return post as response, Laravel automatically serializes this to JSON
return response($post, 201);
}
}
Now that we are done with building the backend, we can proceed to create our view and event listener on the Frontend.
Building 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:
<!-- ./resources/views/welcome.blade.php -->
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>News Talk</title>
<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
<style>
.container {
padding-top: 100px;
}
</style>
<!-- Scripts -->
<script>
window.Laravel = {!! json_encode([
'csrfToken' => csrf_token(),
]) !!};
</script>
</head>
<body>
<div id="app">
<!-- home Vue component -->
<home></home>
</div>
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}"></script>
</body>
</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:
<!-- ./resources/assets/js/components/Home.vue -->
<template>
<div class="container">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div class="form-group">
<label for="title">Post Title</label>
<input v-model="newPostTitle" id="title" type="text" class="form-control">
</div>
<div class="form-group">
<label for="description">Post Description</label>
<textarea v-model="newPostDesc" id="description" rows="8" class="form-control"></textarea>
</div>
<button @click="addPost(newPostTitle, newPostDesc)"
:class="{disabled: (!newPostTitle || !newPostDesc)}"
class="btn btn-block btn-primary">Submit</button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
newPostTitle: "",
newPostDesc: ""
}
},
created() {
this.listenForChanges();
},
methods: {
addPost(postName, postDesc) {
// check if entries are not empty
if(!postName || !postDesc)
return;
// make API to save post
axios.post('/api/post', {
title: postName, description: postDesc
}).then( response => {
if(response.data) {
this.newPostTitle = this.newPostDesc = "";
}
})
},
listenForChanges() {
Echo.channel('posts')
.listen('PostPublished', post => {
if (! ('Notification' in window)) {
alert('Web Notification is not supported');
return;
}
Notification.requestPermission( permission => {
let notification = new Notification('New post alert!', {
body: post.title, // content for the alert
icon: "https://pusher.com/static_logos/320x320.png" // optional image url
});
// link to page on clicking the notification
notification.onclick = () => {
window.open(window.location.href);
};
});
})
}
}
}
</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:
if (window.Notification) {
console.log('Notifications are supported!');
} else {
alert('Notifications aren\'t supported on your browser! :(');
}
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
:
// ./resources/assets/js/app.js
Vue.component('home', require('./components/Home.vue'));
Bringing it all together
We can compile our assets easily using Laravel Mix!:
npm 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.
Conclusion
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.
11 May 2018
by Olayinka Omole