Symfony is a popular PHP framework. It’s built in a component form that allows users to pick and choose the components they need. In this tutorial, we’ll build a Symfony app that uses Pusher Channels to display the current number of visitors to a particular page in realtime. Here’s a preview of our app in action:
Create a new Symfony project called “countess” by running the following command:
composer create-project symfony/website-skeleton countess
We’re ready to start building. Let’s create the route for the lone page in our app. Open up the file config/routes.yaml
and replace its contents with the following:
1# config/routes.yaml 2 3 index: 4 path: /home 5 controller: App\Controller\HomeController::index
Note: We’re going to be working with YAML files quite a bit in this article. In YAML, indentation matters, so be careful to stick to what is shown!
Next, we’ll create the controller. Create the file src/Controller/HomeController.php
with the following contents:
1// src/Controller/HomeController.php 2 3 <?php 4 5 namespace App\Controller; 6 7 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; 8 9 class HomeController extends AbstractController 10 { 11 public function index() 12 { 13 $visitorCount = $this->getVisitorCount(); 14 return $this->render('index.html.twig', [ 15 'visitorCount' => $visitorCount, 16 ]); 17 } 18 }
You’ll notice we’re calling the non-existent method getVisitorCount()
to get the current visitor count before rendering the page. We’ll come back to that in a bit.
Let’s create the view that shows the visitor count. Create the file templates/index.html.twig
with the following content:
1{# templates/index.html.twig #} 2 3 {% extends 'base.html.twig' %} 4 5 {% block body %} 6 <style> 7 body { 8 font-family: "Lucida Console", monospace, sans-serif; 9 padding: 30px; 10 } 11 </style> 12 <h2 align="center" id="visitorCount">{{ visitorCount }}</h2> 13 <p align="center">person(s) currently viewing this page</p> 14 {% endblock %}
Now let’s make the visitor count live. We have two tasks to achieve here:
Here’s how we’ll do this:
visitor-updates
). This is the channel where it will receive updates on the number of visitors.channel_occupied
, which will be sent via a webhook to our backend. Also, when the user leaves the page, the Pusher connection will be terminated, resulting in a channel_vacated
notification.channel_occupied
or channel_vacated
notifications, it re-calculates the visitor count and does two things:visitor-updates
channel. Pages subscribed to this channel can then update their UI to reflect the new value.getVisitorCount
method).Okay, let’s do this!
First, we’ll write the frontend code that implements item (2). Add the following to the bottom of your view:
1{# templates/index.html.twig #} 2 3 {% block javascripts %} 4 <script src="https://js.pusher.com/4.2/pusher.min.js"></script> 5 <script> 6 7 let pusher = new Pusher("{{ pusherKey }}", { 8 cluster: "{{ pusherCluster }}", 9 }); 10 let channelName = Date.now() + Math.random().toString(36).replace(/\W+/g, ''); 11 pusher.subscribe(channelName); 12 pusher.subscribe("visitor-updates") 13 .bind('update', function (data) { 14 console.log(data) 15 let newCount = data.newCount; 16 document.getElementById('visitorCount').textContent = newCount; 17 }); 18 </script> 19 {% endblock %}
We’re referencing a few variables here in the view (pusherKey
, pusherCluster
) which we haven’t defined in the controller. We’ll get to that in a moment. First, let’s configure Pusher on our backend.
Run the following command to install the Pusher bundle for Symfony:
composer require laupifrpar/pusher-bundle
Note: When installing this, Symfony Flex will ask you if you want to execute the recipe. Choose ‘yes’. You can read more about Symfony Flex here.
You’ll notice some new lines have been added to your .env
file:
1###> pusher/pusher-php-server ### 2 PUSHER_APP_ID= 3 PUSHER_KEY= 4 PUSHER_SECRET= 5 ###< pusher/pusher-php-server ###
Add an extra line to these:
PUSHER_CLUSTER=
Then provide all the PUSHER_*
variables with your credentials from your Pusher app dashboard:
1###> pusher/pusher-php-server ### 2 PUSHER_APP_ID=your-app-id 3 PUSHER_KEY=your-app-key 4 PUSHER_SECRET=your-app-secret 5 PUSHER_CLUSTER=your-app-cluster 6 ###< pusher/pusher-php-server ###
After installing the Pusher bundle, you should have a file called pusher_php_server.yaml
in the config/packages
directory. Replace its contents with the following:
1# config/packages/pusher_php_server.yaml 2 3 services: 4 Pusher\Pusher: 5 public: true 6 arguments: 7 - '%env(PUSHER_KEY)%' 8 - '%env(PUSHER_SECRET)%' 9 - '%env(PUSHER_APP_ID)%' 10 - { cluster: '%env(PUSHER_CLUSTER)%' } 11 12 lopi_pusher: 13 key: '%env(PUSHER_KEY)%' 14 secret: '%env(PUSHER_SECRET)%' 15 app_id: '%env(PUSHER_APP_ID)%' 16 cluster: '%env(PUSHER_CLUSTER)%'
Now, let’s add the Pusher credentials for our frontend. Open up the file config/services.yaml
and replace the parameters
section near the top with this:
1$ config/services.yaml 2 3 parameters: 4 locale: 'en' 5 pusherKey: '%env(PUSHER_KEY)%' 6 pusherCluster: '%env(PUSHER_CLUSTER)%'
Here, we’re using parameters in our service container to reference the needed credentials, so we can easily access them from anywhere in our app. Now update the HomeController
‘s index
method so it looks like this:
1// src/Controller/HomeController.php 2 3 public function index() 4 { 5 $visitorCount = $this->getVisitorCount(); 6 return $this->render('index.html.twig', [ 7 'pusherKey' => $this->getParameter('pusherKey'), 8 'pusherCluster' => $this->getParameter('pusherCluster'), 9 'visitorCount' => $visitorCount, 10 ]); 11 }
We’ll create a new route to handle webhook calls from Pusher. Add a new entry to your config/routes.yaml
):
1# config/routes.yaml 2 3 webhook: 4 path: /webhook 5 methods: 6 - post 7 controller: App\Controller\HomeController::webhook
Then create the corresponding method in your controller:
1// src/Controller/HomeController.php 2 3 public function webhook(Request $request, Pusher $pusher) 4 { 5 $events = json_decode($request->getContent(), true)['events']; 6 $visitorCount = $this->getVisitorCount(); 7 foreach ($events as $event) { 8 // ignore any events from our public channel--it's only for broadcasting 9 if ($event['channel'] === 'visitor-updates') { 10 continue; 11 } 12 $visitorCount += ($event['name'] === 'channel_occupied') ? 1 : -1; 13 } 14 // save new figure and notify all clients 15 $this->saveVisitorCount($visitorCount); 16 $pusher->trigger('visitor-updates', 'update', [ 17 'newCount' => $visitorCount, 18 ]); 19 return new Response(); 20 }
The saveVisitorCount
method is where we store the new visitor count in the cache. We’ll implement that now.
We’re using a cache to store the current visitor count so we can track it across sessions. To keep this demo simple, we’ll use a file on our machine as our cache. Let’s do this.
Fortunately, since we’re using the Symfony framework bundle, the filesystem cache is already set up for us. We only need to add it in as a parameter to our controller’s constructor. Let’s update our controller and add the getVisitorCount
and updateVisitorCount
methods to make use of the cache:
1// src/Controller/HomeController.php 2 3 <?php 4 5 namespace App\Controller; 6 7 use Psr\SimpleCache\CacheInterface; 8 use Pusher\Pusher; 9 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; 10 use Symfony\Component\HttpFoundation\Request; 11 use Symfony\Component\HttpFoundation\Response; 12 13 class HomeController extends AbstractController 14 { 15 public function __construct(CacheInterface $cache) 16 { 17 $this->cache = $cache; 18 } 19 20 public function index() 21 { 22 $visitorCount = $this->getVisitorCount(); 23 return $this->render('index.html.twig', [ 24 'pusherKey' => $this->getParameter('pusherKey'), 25 'pusherCluster' => $this->getParameter('pusherCluster'), 26 'visitorCount' => $visitorCount, 27 ]); 28 } 29 30 public function webhook(Request $request, Pusher $pusher) 31 { 32 $events = json_decode($request->getContent(), true)['events']; 33 $visitorCount = $this->getVisitorCount(); 34 foreach ($events as $event) { 35 // ignore any events from our public channel--it's only for broadcasting 36 if ($event['channel'] === 'visitor-updates') { 37 continue; 38 } 39 $visitorCount += ($event['name'] === 'channel_occupied') ? 1 : -1; 40 } 41 // save new figure and notify all clients 42 $this->saveVisitorCount($visitorCount); 43 $pusher->trigger('visitor-updates', 'update', [ 44 'newCount' => $visitorCount, 45 ]); 46 return new Response(); 47 } 48 49 private function getVisitorCount() 50 { 51 return $this->cache->get('visitorCount') ?: 0; 52 } 53 54 private function saveVisitorCount($visitorCount) 55 { 56 $this->cache->set('visitorCount', $visitorCount); 57 } 58 59 }
We need to do a few things before our webhook is ready for use.
Since the application currently lives on our local machine, we need a way of exposing it via a public URL. Ngrok is an easy-to-use tool that helps with this. If you don’t already have it installed, sign up on http://ngrok.com and follow the instructions to install ngrok. Then expose http://localhost:8000 on your machine by running:
./ngrok http 8000
You should see output like this:
Copy the second Forwarding URL (the one using HTTPS). Your webhook URL will then be <your-ngrok-url>/webhook
(for instance, for the screenshot above, my webhook URL is https://fa74c4e1.ngrok.io/webhook
).
Next, you’ll need to enable channel existence webhooks for our Pusher app. On your Pusher app dashboard, click on the Webhooks tab and select the Channel existence radio button. In the text box, paste the URL of the webhook you obtained above, and click Add. Good to go!
Start the app by running:
php bin/console server:run
Now visit http://localhost:8000/home in a browser. Open it in multiple tabs and you should see the number of visitors go up or down as you open and close tabs.
Tip: If you made a mistake earlier in this tutorial, you might find that the page updates in a weird manner. This is because the cache is in an inconsistent state. To fix this, you’ll need to clear the cache. An easy way to fix this is by opening up the
config/packages/framework.yaml
file and changing the value ofprefix_seed
(under thecache
key) to some random value:
prefix_seed: hahalol
This has the same effect as telling the app to use a new cache folder.
In this tutorial, we’ve built a simple demo showing how we can add realtime capabilities to a Symfony app. We could go on to display the number of actual users by filtering by factors such as their IP address. If our app involved signing in, we could even use presence channels to know who exactly was viewing the page. I hope you enjoyed this tutorial. You can check out the source code of the completed application on GitHub.