This tutorial is part of the Building Your Startup With PHP series on Envato Tuts+. In this series, I'm guiding you through launching a startup from concept to reality using my Meeting Planner app as a real-life example. Every step along the way, I'll release the Meeting Planner code as open-source examples you can learn from. I'll also address startup-related business issues as they arise.
In this two-part series, I'll describe how we built the infrastructure for reminders and their delivery. This episode will focus on the infrastructure and user experience behind configuring reminders.
If you haven't tried out Meeting Planner yet, go ahead and schedule your first meeting. I do participate in the comment threads below, so tell me what you think! I'm especially interested if you want new features or to suggest topics for future tutorials.
As a reminder, all of the code for Meeting Planner is written in the Yii2 Framework for PHP. If you'd like to learn more about Yii2, check out our parallel series Programming With Yii2.
How Reminders Will Work
Initially, I had created some simple reminder options in the UserSetting table. However, I realized that users will want much more flexibility and control over when and how their reminders arrive.
People should be able to set reminders for 30 minutes before, 3 hours before, and 48 hours before—or only 1 hour before. It should be completely up to them. They should also be able to choose whether they want to receive reminders via email, SMS or both. Meeting Planner doesn't support SMS yet, but it will soon—there will be a tutorial about that too.
Here's an example of the flexibility that Apple Calendar offers:
Allowing People to Set Up Reminders
Let's build the infrastructure to support any number of user customizable meeting reminders.
The Reminder Table
First, I created a Reminder table to support one or more reminder requests by users for all their meetings. As is typical with Yii2, I created the table with a migration.
Here's the Yii console command to create a database migration:
./yii migrate/create create_reminder_table
Then I customized this skeleton file with the properties I need:
class m160503_234630_create_reminder_table extends Migration { public function up() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->createTable('{{%reminder}}', [ 'id' => Schema::TYPE_PK, 'user_id' => Schema::TYPE_BIGINT.' NOT NULL', 'duration_friendly' => Schema::TYPE_INTEGER.' NOT NULL DEFAULT 0', 'unit' => Schema::TYPE_SMALLINT.' NOT NULL DEFAULT 0', 'duration' => Schema::TYPE_INTEGER.' NOT NULL DEFAULT 0', 'reminder_type' => Schema::TYPE_SMALLINT.' NOT NULL DEFAULT 0', 'created_at' => Schema::TYPE_INTEGER . ' NOT NULL', 'updated_at' => Schema::TYPE_INTEGER . ' NOT NULL', ], $tableOptions); $this->addForeignKey('fk_reminder_user', '{{%reminder}}', 'user_id', '{{%user}}', 'id', 'CASCADE', 'CASCADE'); }
If a reminder is 48 hours before, duration_friendly
and unit
will be 48
and UNIT_HOURS
, while the duration
field will be held in seconds, e.g. 48 * 60 minutes * 60 seconds or 172,800 seconds before a meeting. This will help with both simplifying the user interface and processing reminders.
Reminder_type
will specify email, SMS, or both.
Then I used Gii, its code scaffolding generator, to create MVC code quickly for the controller, model, and views. The initial user interface was ready in minutes. You can see the create a reminder form above and the reminder index below.
Initializing Reminders for Existing and New Users
By the time I began working on reminders, people were already using Meeting Planner to schedule meetings. So I need to initialize the Reminder table for existing people as well as each newly registered person.
I decided that there should be three default Reminders for users at first, scheduled for 3 hours, 1 day and 3 days before a meeting. The code below creates these reminders for a user:
public static function initialize($user_id) { // create initial reminders for a user $r1 = new Reminder(); $r1->user_id = $user_id; $r1->duration_friendly = 3; $r1->unit = Reminder::UNIT_HOURS; $r1->reminder_type = Reminder::TYPE_EMAIL; $r1->duration = 3600; $r1->validate(); $r1->save(); $r2 = new Reminder(); $r2->user_id = $user_id; $r2->duration_friendly = 1; $r2->unit = Reminder::UNIT_DAYS; $r2->reminder_type = Reminder::TYPE_EMAIL; $r2->duration = 1*24*3600; $r2->save(); $r3 = new Reminder(); $r3->user_id = $user_id; $r3->duration_friendly = 3; $r3->unit = Reminder::UNIT_DAYS; $r3->reminder_type = Reminder::TYPE_EMAIL; $r3->duration = $r3->duration_friendly*24*3600; $r3->save(); Reminder::processNewReminder($r1->id); Reminder::processNewReminder($r2->id); Reminder::processNewReminder($r3->id); }
But what does processNewReminder
do? It builds rows in another table that I'll describe below.
Processing Reminders
It's great that we now have a way for users to provide default reminder choices for meetings. But how does the system know when to send reminders to each user for their meetings? That's more complicated.
For performance reasons, I decided it would be best to build a MeetingReminder
table of its own. This would make a note of the user's default reminders as meetings are scheduled and track when to send out these reminders for each meeting. It's basically a meeting-specific reminder table reflecting each participant's configured reminder preferences.
As meeting times are updated, the MeetingReminder
table entries for that meeting will need to change. Similarly, if a person updates their reminder preferences, scheduled reminders in the MeetingReminder
table will also need to be refreshed.
Let's create the migration for the MeetingReminder
table:
./yii migrate/create create_meeting_reminder_table
Here's the migration code; it's pretty simple. Basically, for each reminder, there is a MeetingReminder
which corresponds to a reminder of a user for a meeting. It knows that a reminder is due_at
a certain time and has a status which determines if it's been sent yet:
class m160510_062936_create_meeting_reminder_table extends Migration { public function up() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->createTable('{{%meeting_reminder}}', [ 'id' => Schema::TYPE_PK, 'meeting_id' => Schema::TYPE_INTEGER.' NOT NULL DEFAULT 0', 'reminder_id' => Schema::TYPE_BIGINT.' NOT NULL', 'user_id' => Schema::TYPE_BIGINT.' NOT NULL', 'due_at' => Schema::TYPE_INTEGER . ' NOT NULL', 'status' => Schema::TYPE_SMALLINT.' NOT NULL DEFAULT 0', ], $tableOptions); $this->addForeignKey('fk_meeting_reminder_user', '{{%meeting_reminder}}', 'user_id', '{{%user}}', 'id', 'CASCADE', 'CASCADE'); $this->addForeignKey('fk_meeting_reminder_meeting', '{{%meeting_reminder}}', 'meeting_id', '{{%meeting}}', 'id', 'CASCADE', 'CASCADE'); }
The background monitoring job will be able to sort the MeetingReminder
table by time due and quickly know which small set of reminders actually need to be delivered. And it could track which ones have been sent for each meeting and participant.
Note: At this time, there's no feature that allows people to customize reminders for a specific meeting, so there's no user interface with the MeetingReminder table. I could add this later.
As I hinted at earlier, the MeetingReminder
table turned out to create a lot of subtle complexity:
- If people add, edit or delete reminders, this must be reflected in preconfigured meeting reminders.
- If people change the time of a meeting or cancel it, the meeting reminders must be updated to reflect this.
- If a person chooses not to attend a meeting, those reminders must be disabled.
Ultimately, building reminder functionality required a lot of helper functionality.
Here's a helper function that creates a MeetingReminder
for a specific user for their reminder for a specific meeting. If the meeting has already passed, the status reflects that:
public static function create($meeting_id,$user_id,$reminder_id,$differential) { // delete any previously existing meetingreminder for this reminder_id and meeting_id MeetingReminder::deleteAll(['meeting_id'=>$meeting_id,'reminder_id'=>$reminder_id]); $mtg = Meeting::findOne($meeting_id); if (is_null($mtg)) { return false; } $chosen_time = Meeting::getChosenTime($meeting_id); $mr = new MeetingReminder; $mr->reminder_id = $reminder_id; $mr->meeting_id = $meeting_id; $mr->user_id = $user_id; $mr->due_at = $chosen_time->start-$differential; if ($mr->due_at>time()) { $mr->status=MeetingReminder::STATUS_PENDING; } else { $mr->status=MeetingReminder::STATUS_COMPLETE; } $mr->save(); }
So, whenever a reminder is created, here's the code that creates all of the MeetingReminder
entries for each of the user's meetings:
public static function processNewReminder($reminder_id) { $rem = Reminder::findOne($reminder_id); // find all the meetings this user is a part of // create meeting reminder for all meetings where this reminder's creator is the organizer $mtgs = Meeting::find()->where(['owner_id'=>$rem->user_id])->all(); // to do performance - could add an open join above to participants foreach ($mtgs as $m) { MeetingReminder::create($m->id,$rem->user_id,$rem->id,$rem->duration); } // create meeting reminder for all meetings where this reminder's creator is a participant $part_mtgs = Participant::find()->where(['participant_id'=>$rem->user_id])->all(); foreach ($part_mtgs as $m) { MeetingReminder::create($m->id,$rem->user_id,$rem->id,$rem->duration); } }
Basically, the code finds all the meetings for a person, whether they are organizer or participant, and creates a MeetingReminder
entry for each of that person's reminders. For example, a person with three default reminder preferences and three meetings scheduled will have nine MeetingReminder
table entries.
Handling the Creation of New Reminders
When a person creates a new reminder, we have code that sets the duration based on their settings and then creates a new MeetingReminder
for all of their pending meetings:
public function actionCreate() { $model = new Reminder(); $model->user_id = Yii::$app->user->getId(); $model->duration = 0; if ($model->load(Yii::$app->request->post())) { $model->duration = $model->setDuration($model->duration_friendly,$model->unit); if ($model->validate()) { $model->save(); Reminder::processNewReminder($model->id); Yii::$app->getSession()->setFlash('success', Yii::t('frontend','Your reminder has been created for all current and future meetings.')); return $this->redirect('index'); } else { // to do set flash Yii::$app->getSession()->setFlash('error', Yii::t('frontend','There was a problem creating your reminder.')); } } return $this->render('create', [ 'model' => $model, ]); }
Handling Changes to Reminders
If a user modifies a reminder, we need to update the MeetingReminder
table for that reminder_id
:
public static function updateReminder($reminder_id) { // when user updates a reminder, update all the meeting reminders $new_reminder = Reminder::findOne($reminder_id); $mrs = MeetingReminder::find()->where(['reminder_id'=>$reminder_id])->all(); // update each meeting reminder foreach ($mrs as $mr) { $chosen_time = Meeting::getChosenTime($mr->meeting_id); $mr->due_at = $chosen_time->start-$new_reminder->duration; if ($mr->due_at>time()) { $mr->status=MeetingReminder::STATUS_PENDING; } else { $mr->status=MeetingReminder::STATUS_COMPLETE; } $mr->update(); } }
If the due_at
time for a reminder has already passed, then we set its status as complete.
Handling When Meetings Are Finalized
When a Meeting is finalized, the time is set and we need to configure MeetingReminders
based on each participant's Reminder settings. The setMeetingReminders
method does this:
public static function setMeetingReminders($meeting_id,$chosen_time=false) { // when a meeting is finalized, set reminders for the chosen time for all participants $mtg = Meeting::findOne($meeting_id); if ($chosen_time ===false) { $chosen_time = Meeting::getChosenTime($meeting_id); } // create attendees list for organizer and participants $attendees = array(); $attendees[0]=$mtg->owner_id; $cnt =1; foreach ($mtg->participants as $p) { if ($p->status ==Participant::STATUS_DEFAULT) { $attendees[$cnt]=$p->participant_id; $cnt+=1; } } // for each attendee foreach ($attendees as $a) { // for their reminders $rems = Reminder::find()->where(['user_id'=>$a])->all(); foreach ($rems as $rem) { // create a meeting reminder for that reminder at that time MeetingReminder::create($meeting_id,$a,$rem->id,$rem->duration); } } }
Handling When Meeting Times Change
Similarly, when a meeting time is changed after the fact (not yet supported in the current feature set), I created a simple function to remove and rebuild MeetingReminders
for the new time:
public static function processTimeChange($meeting_id,$chosen_time) { // when a meeting time is set or changes, reset the reminders for all participants // clear out old meeting reminders for all users for this meeting MeetingReminder::deleteAll(['meeting_id'=>$meeting_id]); // set meeting reminders for all users for this meeting // note each user has different reminders Reminder::setMeetingReminders($meeting_id,$chosen_time); }
A feature that seems simple at first turns out to require a lot of detail and oversight.
What's Next?
You've seen the foundation for reminders. In the next tutorial, I'll show you how we monitor time to know when and how to deliver reminders. And I'll show you how we deliver the reminders via email (SMS will come later).
While you're waiting, try out the reminder feature, schedule your first meeting, and then update your reminder preferences. Also, I'd appreciate it if you share your experience below in the comments, and I'm always interested in your suggestions. You can also reach me on Twitter @reifman directly.
I'm also beginning to experiment with WeFunder based on the implementation of the SEC's new crowdfunding rules. Please consider following our profile there. I may write about this more as part of our series.
Watch for upcoming tutorials in the Building Your Startup With PHP series. In addition to reminders, there's also lots of polish work and a few more big features coming up.
Comments