How I’m Using Twilio and Laravel to Have a Eurovision Party in Lockdown
Time to read: 8 minutes
I've always been a fan of Eurovision. Every year one of our group hosts a Eurovision party that is themed around the host country's food and drink. The evening is spent marking each of the acts giving points for categories such as entertainment value, set, staging and props, and not forgetting any costume changes á la Bucks Fizz in 1981. We then tally them all up to come up with our overall winner and see if the jury agrees.
Here in the UK, due to the Coronavirus pandemic, 2021's Eurovision party isn't able to take place as we'd like, so I decided to take matters into my own hands and replicate the voting experience using Twilio and Laravel. Here's how I did it.
The Initial Idea
I decided to create an admin dashboard where I could add my friends as Voters. I wanted to “invite” them after they’d been added to ensure that their details had been entered correctly and that there were no issues sending SMS messages to them. This meant sending them a message to which they could respond with "OK", confirming that they wanted to participate.
I wanted a list of countries in my app and, when each country was due to play, I'd "announce" the country to the voters. When the country had finished performing, I'd open voting, and the app would then allow voters to send a single number from 0 - 30 as their vote’s combined value. I'd then close voting and move on to the next country.
At the end of the process, I could tally up the votes and declare our overall winner. If you don't want to follow along and just want to have this deployed, the full code is available on my Github page.
Setting up Laravel
I knew that I wanted the functionality behind a login as the app would be hosted on a public-facing server, so I spun up a new Laravel app and installed Laravel Breeze. This gave me some boilerplate for an admin dashboard for free.
I next added my Twilio credentials to the environment file and services configuration file and installed the Twilio SDK.
To make sure everything was set up properly, I created a test route:
With five lines of code, I’d sent my first SMS and confirmed that everything was working as it should. Super!
Making Sending Easier
Whilst the above code was super easy, I also knew that I was going to be sending messages in a number of different places throughout the app. In order to do this, I wrapped the Twilio Client in my own client that I could bind into the container and use everywhere via dependency injection.
My client looked like this:
And, to bind it into the container, I did the following:
This meant that I could now inject my own client and send messages like this:
Registering Voters
Now that I had a solid foundation on which to build, I started the process of building the registration flow for voters. My first step was to add some basic CRUD, with the following information per voter:
- Name
- Number
In addition to this data, I also added a confirmed_at
timestamp so that I could tell who had responded to the invitation and who hadn’t. This enabled me to only send messages to people who had indicated that they wanted to take part.
My Voters administration area now looked like this:
Initially, I was going to send an invitation as soon as someone had been added, but I decided instead to have a button on the Dashboard that would allow me to invite everyone at the same time:
In order to do this, I wired up a controller as follows:
This dispatches a queued job for each voter, which handles sending the actual invitation to the voter:
SMS can look a little plain, so I added an emoji at the bottom of the message. It looks like this:
Now that I could invite people, I needed to handle their response.
Twilio offers webhook functionality that will ping your app every time a number (or in my case, a messaging service) receives an SMS. Because I was running this locally for testing, I needed to use ngrok so that Twilio could communicate with my local machine. Because I was using Laravel Valet, this was as simple as typing valet share in the Terminal and copying the URL into my Twilio console.
As I knew this was going to be on a publicly accessible server, I wanted to make sure that any requests coming to the webhook were from Twilio. To do that, I wrote a custom middleware that could be applied to the webhook route:
I also needed to disable CSRF protection for the webhook URL, and then I was ready to write the webhook itself.
To enable voters to confirm their participation, I looked for “OK” as an incoming message and if I found it, update the voter with the corresponding phone number to be confirmed. The code for that looks like this:
I made sure to lowercase the incoming body from the request to cater for people responding “OK”, “ok”, “Ok”, and “oK”.
In addition to dispatching a queued job to confirm the voter, I also checked to ensure that the voter existed in the database.
The queued job is straightforward and looks like this:
The response to confirming registration looks like this:
Now that voters could confirm their participation, the next step was to add countries.
Adding Countries
To add countries, I knew that I wanted a basic CRUD setup that had the following pieces of data:
- Name
- Flag (an emoji)
- Song Title
- Artist
To know which country any incoming votes were for, I also decided to add a currently_voting boolean. I planned to toggle this to true when opening voting for a country and toggle it to false when closing voting.
I also created a seeder to add all of the country and song details to the database. Once all of the country data had been added, my country admin page looked like this:
I added buttons for actions that I knew needed to be performed on the country detail screen. That looked like this:
I was now ready to announce countries.
Announcing Countries
When a country is being introduced at Eurovision, they usually play an introductory film about it. I decided that during this film I would press the “Announce” button for the corresponding country which would send a text to the voters letting them know the song details.
I hooked up the button to a controller that dispatches a queued job to send a message to each confirmed voter like this:
This dispatches the jobs, that look like this:
So, for example, when announcing Denmark, the text that the voters receive looks like this:
Any votes cast during this time don’t count until the voting has been opened, so I decided to tackle that next.
Opening Voting
When opening voting, several steps needed to happen.
First, any other countries that had voting open would need to be closed. Second, the currently_voting flag needed to be set on the database. And third, voters needed to be told that voting was now open.
To encapsulate the first two steps, I added the following method to my Country model:
Next, I hooked up the “Open Voting” button on the Country detail screen to a controller that looked like this:
Again, this dispatches jobs that tell the voters that voting is now open for the country:
This should look familiar as it’s essentially the same idea that I used to announce a country to the voters.
The message received looks like this:
Once voting was opened, it was time to accept some votes!
Handling Votes
To handle incoming votes, I knew that I needed a Vote model. I added one with the following properties:
- Voter ID (so I knew who had cast the vote)
- Country ID (so I knew which country it was for)
- Value
I planned to parse anything that looked like a number in the webhook and then cast a vote for the voter.
I also wanted to handle a couple of edge cases:
- If voting wasn’t open, I wanted to respond by telling the voter that
- If their vote was out of the allowed value range I wanted to inform them
- If someone tried to vote twice for the same country, I wanted to stop them
To do that, I whipped up some basic guard clauses in the webhook controller. Then, if the code made it past all of those, I would dispatch a job to cast the vote.
The finished webhook controller looked like this:
The job to cast a vote looks pretty much the same as the other jobs, with the exception that it creates a vote record in the database:
This job was also set up to be synchronous. If this job was queued, if someone happened to cast a vote, and then cast another vote before the job had been processed, they could potentially cast two votes.
It would probably be a better solution to check for an existing vote in the job and just ignore subsequent votes, but that’s an exercise for next year!
Sending a vote looks like this:
The next step was to close voting before announcing the next country.
Closing Voting
Closing voting needed to take a few steps. First, it needed to close the voting for a country, and second, notify the voters that voting had been closed.
Following the same pattern as the other functionality, I hooked up the button to a controller that closed voting, and then dispatched several queued jobs to notify the voters:
The queued job looks pretty much the same as all of the others:
The resulting text looks like this:
Adding voting being closed completed all of the functionality and I could now run the Eurovision party successfully.
To make things a bit more user-friendly for me during the contest, though, I added some more information to the dashboard.
Displaying Results
Initially, the dashboard didn’t do anything, but I decided to make it a little more useful.
The first thing I did was to add a list of countries that had received votes, ordered by their score. I did this by only selecting countries that had received votes, ordered by a sum of the value of their votes.
Also, in order to keep an eye on this during the contest, but also be able to get back to the currently active country, I added a link to the country at the top of the dashboard. This allowed me to close voting really easily.
The dashboard looks like this:
It’s great to be able to see the results as they come in and the order of the countries change as countries change their position in the results.
Summary
Twilio, in combination with Laravel, made writing this Eurovision voting application easy and quick. I had the majority of the code written in a few hours, and I’ve tested it with several friends already who all think it’s awesome.
There are still some features that I’d like to add, such as notifying everyone of the results and some statistics about who scored the countries in the most “correct” order, but they’re features that I can add over time.
Even when we return to having parties, I think we’ll still use this app so that we can also include people who can’t be there, and some of our friends in different parts of the world.
I’m really happy with how it turned out.
Matthew Davis is the Technical Lead at mumsnet.com. Trained at Birmingham Conservatoire of Music in the UK, he used to be a professional musician touring the world on cruise ships and with theatre shows before turning his passion for software development into his career over 15 years ago. Since then, he’s built software for small independent businesses as well as for global companies and regularly contributes to open source.
He can be found on Twitter at @mdavis1982.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.