How to Create an iCal Calendar Feed with PHP using Laravel Lumen

November 13, 2018
Written by
Charles Oduk
Contributor
Opinions expressed by Twilio contributors are their own

ical calendar feed

iCal, short for iCalendar, is an internet standard file format used to store calendar information. Being a standard format, it is compatible with most online calendars, giving you access to important dates regardless of your preferred client (Google Calendar, Outlook Calendar, Apple Calendar, etc.). Even popular online services use the iCal format to help their users remember important dates. Airbnb, for example, uses the iCal format to store room availability giving users the ability to export their Airbnb calendar and view it on an external calendar.

In this tutorial, you’ll learn about the iCal format and how to create an iCal calendar feed using Lumen, a PHP micro-framework by Laravel that allows you to quickly build elegant APIs.

Tutorial Requirements

For this tutorial, you will need:

The iCal Object

Below is a sample iCal object:

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
BEGIN:VEVENT
UID:19970610T172345Z-AF23B2@example.com
DTSTAMP:19970610T172345Z
DTSTART:19970714T170000Z
DTEND:19970715T040000Z
SUMMARY:Bastille Day Party
END:VEVENT
END:VCALENDAR

The iCal object has three parts: begin, body and end. The iCal object must start with BEGIN:VCALENDAR and end with END:VCALENDAR. The body consists of a sequence of properties and one or more calendar components.

In our example above, we have two properties, VERSION and PRODID. While there are many other properties, these two must be present in an iCal object in order for it to parse correctly.

PRODID is your company details in the format: Business Name//Product Name//Language. VERSION is the current version of iCal.

A calendar can have multiple components, each grouped inside a begin and end delimiter. A component is a collection of properties that express a particular calendar semantic. For example, the calendar component can specify an event, to-do, journal entry, time zone information, free/busy time information or an alarm. In our example above, the event properties are grouped inside the BEGIN:VEVENT and END:VEVENT delimiters.

Here is some information on a few properties of an event component:

  • UID: A unique ID for the event (required).
  • DTSTAMP: The date the event was created (required).
  • DTSTART/DTEND: The start and end timestamps of an event in UTC format.
  • SUMMARY: The event title.

For more information on the iCal object and its properties, please check out the official documentation.

Setup

Starting a Lumen Project

Lumen is a fast PHP micro-framework by Laravel. This micro-framework makes it very easy to bootstrap a new project with the ability to handle up to 1900 requests per second.

To start a new lumen project via Composer, run the following command:

$ composer create-project --prefer-dist laravel/lumen iCal

The last part of the command is the name of our project. I have named mine "iCal". Once we run the command, Composer downloads all required dependencies and prepares our application.

Let’s check if everything worked as expected. We will start our application by running the command:

 $ php -S localhost:8000 -t public

Our application is now served on localhost port 8000. On your browser, navigate to http://localhost:8000/. You should see the version of lumen you’re using. At the time of writing this tutorial, it is version 5.7.4.

Database and Environment Configuration

The next step is to create a database and configure our connection to the database. Lumen supports four database systems: MySQL, PostgreSQL, SQLite and SQL Server. I am using PostgreSQL for this tutorial. I have created a database called events.

When we created our Lumen application, a .env file was created for us. Once you have created your database, go ahead and update the database credentials in the .env file. Here is what your updated .env should look like:

APP_ENV=local
APP_DEBUG=true
APP_KEY=
APP_TIMEZONE=UTC

LOG_CHANNEL=stack
LOG_SLACK_WEBHOOK_URL=

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=events
DB_USERNAME=charlieoduk
DB_PASSWORD=

CACHE_DRIVER=file
QUEUE_DRIVER=sync

That is everything we need to do to connect our database. We’re making good progress, however, we have no way of testing our database connection just yet. Let’s go ahead and make a migration.

Create a Migration

Migrations allow you to build, modify and share the applications database schema with ease. For our events application, we need one table called tech_events. It should have the following columns:

  • id
  • name
  • starts
  • ends
  • status
  • summary
  • location
  • uid
  • created_at
  • Updated_at

To create a migration we will use the make:migration Artisan command:

$ php artisan make:migration create_tech_events_table

The new migration is placed in the database/migrations folder. If you open up the newly created migration, this is what you get:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTechEventsTable extends Migration
{
   /**
    * Run the migrations.
    *
    * @return void
    */
   public function up()
   {
       Schema::create('tech_events', function (Blueprint $table) {
           $table->increments('id');
           $table->timestamps();
       });
   }

   /**
    * Reverse the migrations.
    *
    * @return void
    */
   public function down()
   {
       Schema::dropIfExists('tech_events');
   }
}

Inside the up() method, we create a table and define its columns. The down() method reverses the migrations if we need to.

Let’s go ahead and update the up() method with the columns we require. This is what we have:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTechEventsTable extends Migration
{
   /**
    * Run the migrations.
    *
    * @return void
    */
   public function up()
   {
       Schema::create('tech_events', function (Blueprint $table) {
           $table->increments('id');
           $table->string('name');
           $table->timestamp('starts')->default(\DB::raw('CURRENT_TIMESTAMP'));
           $table->timestamp('ends')->default(\DB::raw('CURRENT_TIMESTAMP'));
           $table->string('status');
           $table->text('summary');
           $table->string('location');
           $table->string('uid');
           $table->timestamps();
       });
   }

   /**
    * Reverse the migrations.
    *
    * @return void
    */
   public function down()
   {
       Schema::dropIfExists('tech_events');
   }
}

In the above code we have added columns using this format $table->string('name'). This means we have a column called name and its datatype is a string.

Now that we have defined our schema, let’s do a migration to check if it translates to our database. Run the artisan command:

$ php artisan migrate

Check your database for the above migration. I am using Postico to check the database. This is what the database looks like:

laravel-lumen-database.png

Great! We have now set up our database, connected it and defined the schema for our tech_events table.

Create the Tech Events Model

We will use Laravel’s Eloquent ORM to interact with our database. To do so we will need to enable the Eloquent ORM by opening the file bootstrap/app.php and uncommenting these lines:

<?php
$app->withFacades();
$app->withEloquent();

Models are typically inside the app folder. In our app folder, we have a file called User.php. By default, Lumen comes with the User model when we create our application.

If we had many models, a good practice would be to create a folder and store our models in it. Since we are only adding one more model, let us leave it in the app folder.

Create the file app/TechEvents.php and add the following code in it:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;


class TechEvents extends Model
{
   protected $table = 'tech_events';

   /**
    * The attributes that are mass assignable.
    *
    * @var array
    */
   protected $fillable = [
       'name',
       'starts',
       'ends',
       'status',
       'summary',
       'location',
       'uid'
   ];
  
   /**
    * The rules for data entry
    *
    * @var array
    */
   public static $rules = [
       'name' => 'required',
       'starts' => 'required',
       'ends' => 'required',
       'status' => 'required',
       'summary' => 'required',
       'location' => 'required',
       'uid' => 'required'
   ];
}

We have defined our model. We can now interact with our database using Eloquent. In order to build our calendar feed, we will need some events in our database. Let’s go ahead and seed some data in our database. More on models can be found in the documentation.

Seed Tech Events Data

Lumen provides an easy way to add fake data using factories. We define our model factories in the database/factories/ModelFactory.php file. Let’s have a look at what our file looks like by default:

<?php
/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| Here you may define all of your model factories. Model factories give
| you a convenient way to create models for testing and seeding your
| database. Just tell the factory how a default model should look.
|
*/

use Carbon\Carbon;

$factory->define(App\User::class, function (Faker\Generator $faker) {
   return [
       'name' => $faker->name,
       'email' => $faker->email,
   ];
});

We can see a model factory is defined for the user model. It has the name and email fields. The Faker library is used to generate fake data for the fields. It is one of the dependencies that was installed when we created the application. You can confirm this by checking the composer.json file under "require-dev". Let us update the file by adding a model factory for the tech events model:

<?php
/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| Here you may define all of your model factories. Model factories give
| you a convenient way to create models for testing and seeding your
| database. Just tell the factory how a default model should look.
|
*/

use Carbon\Carbon;

$factory->define(App\User::class, function (Faker\Generator $faker) {
   return [
       'name' => $faker->name,
       'email' => $faker->email,
   ];
});

$factory->define(App\TechEvents::class, function (Faker\Generator $faker) {
   $startTime = Carbon::createFromTimeStamp($faker->dateTimeBetween('now', '+1 month')->getTimestamp());
   return [
       'name' => $faker->word,
       'starts' => $startTime,
       'ends' => Carbon::createFromFormat('Y-m-d H:i:s', $startTime)->addHours(2),
       'status' => 'CONFIRMED',
       'summary' => $faker->sentence($nbWords = 6, $variableNbWords = true),
       'location' => $faker->word,
       'uid' => $faker->domainName,
       'created_at' => Carbon::now()->toDateTimeString(),
       'updated_at' => Carbon::now()->toDateTimeString()
   ];
});

We have imported Carbon, a PHP extension for DateTime. We have also declared a variable called $startTime. This generates a random date within a month from now. For this tutorial, our events will only last two hours. In the 'ends' field we add two hours to the $startTime.

In order to seed our database with fake data, we need one more step. In the file database/seeds/DatabaseSeeder.php, we have one method called run(). When we run seeders, this is the method that is called.

This method uses model factories to generate and insert data into the database. Let’s add our newly created model factory:

<?php

use Illuminate\Database\Seeder;
use App\TechEvents;

class DatabaseSeeder extends Seeder
{
   /**
    * Run the database seeds.
    *
    * @return void
    */
   public function run()
   {
       factory(TechEvents::class, 10)->create();
   }
}

By calling factory(TechEvents::class, 10)->create() inside the run method, we are indicating that we would like to generate and insert ten records into our database.

It is time to check if we have done everything correctly. Run the command:

$ php artisan db:seed  

Your database should look like this:

We have now successfully added data to our tech_events table.

 

Building our iCal object

The iCal Controller

We need one controller for this tutorial. In the directory app/Http/Controllers. Create a controller called ICalController.php and add the following code:

<?php

namespace App\Http\Controllers;
use App\TechEvents;

class ICalController extends Controller
{
  
   /**
    * Gets the events data from the database
    * and populates the iCal object.
    *
    * @return void
    */
   public function getEventsICalObject()
   {
       $events = TechEvents::all();
       define('ICAL_FORMAT', 'Ymd\THis\Z');

       $icalObject = "BEGIN:VCALENDAR
       VERSION:2.0
       METHOD:PUBLISH
       PRODID:-//Charles Oduk//Tech Events//EN\n";
      
       // loop over events
       foreach ($events as $event) {
           $icalObject .=
           "BEGIN:VEVENT
           DTSTART:" . date(ICAL_FORMAT, strtotime($event->starts)) . "
           DTEND:" . date(ICAL_FORMAT, strtotime($event->ends)) . "
           DTSTAMP:" . date(ICAL_FORMAT, strtotime($event->created_at)) . "
           SUMMARY:$event->summary
           UID:$event->uid
           STATUS:" . strtoupper($event->status) . "
           LAST-MODIFIED:" . date(ICAL_FORMAT, strtotime($event->updated_at)) . "
           LOCATION:$event->location
           END:VEVENT\n";
       }

       // close calendar
       $icalObject .= "END:VCALENDAR";

       // Set the headers
       header('Content-type: text/calendar; charset=utf-8');
       header('Content-Disposition: attachment; filename="cal.ics"');
      
       $icalObject = str_replace(' ', '', $icalObject);
  
       echo $icalObject;
   }
}

In the getEventsICalObject() method, we start by using Eloquent’s all() method to fetch all the events from the database and store them in the $events variable. Secondly, we define the iCal format. This is used to transform our timestamps into UTC format. Then we begin building our iCal object by looping through each event and adding them to the iCal object.

After closing the object we add the required headers. That includes the file name in .ics format. I have named mine "cal.ics".

Lastly, we get rid of unwanted spaces which would otherwise make the .ics file invalid.

Creating an Endpoint

So far we have worked on the code that fetches the data and creates the iCal object. Since we still have no way of testing this code, we need to create an endpoint. Inside routes/web.php add this line of code:

<?php
$router->get("ical-events", "ICalController@getEventsICalObject");

We have just declared a route. The method to access this route is GET and the URL to hit the endpoint is http://localhost:8000/ical-events.

Let’s go ahead and test this out on Postman. Make sure your application is running on port 8000. If it isn’t, you can run the command:

$ php -S localhost:8000 -t public

Success! We are able to get our events into the iCal format. We need to test this out on different calendars to see if it works. In order to test it on Google and Outlook, we need our application to be accessible through a public URL. This is where ngrok comes in!

While still running the application on localhost, open up a new terminal and run the command:

$ ngrok http 8000

 

Test on Google and Outlook

Google Calendar

To test it out on google:

On the left, click the + icon, next to "Add a friend’s calendar" input box. Select "From URL" then enter the URL you got from ngrok. Mine is http://e574e220.ngrok.io/ical-events.

Your calendar should now have the events from your database.

Outlook Calendar

To test it on Outlook, on the top menu bar, click “Add Calendar”. Then click “From Internet”, to enter the URL and name your calendar. Click save. That’s it!

 

Conclusion

All done! We have successfully created an iCal feed and tested it out on Google and Outlook. You can now test it out on different calendars, including your local calendaring application such as Apple Calendar.

 

Next Steps?

You can create a scheduling application and add the option of downloading an iCal file or giving users a URL to access the iCal feed.

Also, you can request users to opt into SMS reminders to their calendar using the Twilio SMS API. This tutorial on sending SMS reminders from PHP is a great place to start.

I look forward to hearing about the amazing application you build. You can find the complete code on Github. You can reach me on:

Email: odukjr@gmail.com
Github: charlieoduk
Twitter: @charlieoduk

Happy Coding!