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 tutorial, I'm describing how I built the infrastructure for reminders and their delivery. Today, I'm going to guide you through monitoring when to deliver reminders and how to send the emails.
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 to suggest new features or 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.
Monitoring Time for Reminders
As time passes, we need to watch over the MeetingReminder
table to know when to deliver reminders. Ideally, we want reminders to be delivered exactly on time, e.g. to the minute.
Running Background Tasks
Timeliness depends on how regularly we run background tasks for monitoring. Currently, in our pre-alpha stage, I'm running them every five minutes:
# m h dom mon dow command */5 * * * * wget -O /dev/null http://meetingplanner.io/daemon/frequent
This script calls MeetingReminder::check()
, which finds meeting reminders that are due and requests to process()
them:
// frequent cron task will call to check on due reminders public static function check() { $mrs = MeetingReminder::find()->where('due_at<='.time().' and status='.MeetingReminder::STATUS_PENDING)->all(); foreach ($mrs as $mr) { // process each meeting reminder MeetingReminder::process($mr); } }
Processing a Reminder
MeetingReminder::process()
gathers the details needed to create a reminder email. This includes the reminder recipient, meeting details, and time:
public static function process($mr) { // fetch the reminder // deliver the email or sms // send updates about recent meeting changes made by $user_id $user_id = $mr->user_id; $meeting_id = $mr->meeting_id; $mtg = Meeting::findOne($meeting_id); // only send reminders for meetings that are confirmed if ($mtg->status!=Meeting::STATUS_CONFIRMED) return false; // only send reminders that are less than a day late - to do - remove after testing period if ((time()-$mr->due_at)>(24*3600+1)) return false; $u = \common\models\User::findOne($user_id); // ensure there is an auth key for the recipient user if (empty($u->auth_key)) { return false; } // prepare data for the message // get time $chosen_time = Meeting::getChosenTime($meeting_id); $timezone = MiscHelpers::fetchUserTimezone($user_id); $display_time = Meeting::friendlyDateFromTimestamp($chosen_time->start,$timezone); // get place $chosen_place = Meeting::getChosenPlace($meeting_id); $a=['user_id'=>$user_id, 'auth_key'=>$u->auth_key, 'email'=>$u->email, 'username'=>$u->username ]; // check if email is okay and okay from this sender_id if (User::checkEmailDelivery($user_id,0)) { // Build the absolute links to the meeting and commands $links=[ 'home'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_HOME,0,$a['user_id'],$a['auth_key']), 'view'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_VIEW,0,$a['user_id'],$a['auth_key']), 'footer_email'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_FOOTER_EMAIL,0,$a['user_id'],$a['auth_key']), 'footer_block'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_FOOTER_BLOCK,0,$a['user_id'],$a['auth_key']), 'footer_block_all'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_FOOTER_BLOCK_ALL,0,$a['user_id'],$a['auth_key']), 'running_late'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_RUNNING_LATE,0,$a['user_id'],$a['auth_key']), 'view_map'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_VIEW_MAP,0,$a['user_id'],$a['auth_key']) ]; // send the message $message = Yii::$app->mailer->compose([ 'html' => 'reminder-html', 'text' => 'reminder-text' ], [ 'meeting_id' => $mtg->id, 'sender_id'=> $user_id, 'user_id' => $a['user_id'], 'auth_key' => $a['auth_key'], 'display_time' => $display_time, 'chosen_place' => $chosen_place, 'links' => $links, 'meetingSettings' => $mtg->meetingSettings, ]); if (!empty($a['email'])) { $message->setFrom(['[email protected]'=>'Meeting Planner']); $message->setTo($a['email']) ->setSubject(Yii::t('frontend','Meeting Reminder: ').$mtg->subject) ->send(); } } $mr->status=MeetingReminder::STATUS_COMPLETE; $mr->update(); }
The User::checkEmailDelivery()
function verifies that the user hasn't blocked emails from the system (or from particular people). It makes sure it's okay to send the reminder:
// check if email is okay and okay from this sender_id if (User::checkEmailDelivery($user_id,0)) { // Build the absolute links to the meeting and commands $links=[ 'home'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_HOME,0,$a['user_id'],$a['auth_key']), 'view'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_VIEW,0,$a['user_id'],$a['auth_key']), 'footer_email'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_FOOTER_EMAIL,0,$a['user_id'],$a['auth_key']), 'footer_block'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_FOOTER_BLOCK,0,$a['user_id'],$a['auth_key']), 'footer_block_all'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_FOOTER_BLOCK_ALL,0,$a['user_id'],$a['auth_key']), 'running_late'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_RUNNING_LATE,0,$a['user_id'],$a['auth_key']), 'view_map'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_VIEW_MAP,0,$a['user_id'],$a['auth_key']) ];
Here's the User::checkEmailDelivery
method. First, it checks to see if the user blocked all email completely (hopefully not) or whether the message is sent from a blocked user:
public static function checkEmailDelivery($user_id,$sender_id) { // check if this user_id receives email and if sender_id not blocked // check if all email is turned off $us = UserSetting::safeGet($user_id); if ($us->no_email != UserSetting::EMAIL_OK) { return false; } // check if no sender i.e. system notification if ($sender_id==0) { return true; } // check if sender is blocked $ub = UserBlock::find()->where(['user_id'=>$user_id,'blocked_user_id'=>$sender_id])->one(); if (!is_null($ub)) { return false; } return true; }
The New Reminder Email Template
In the Delivering Your Meeting Invitation episode, I wrote about sending email messages within the Yii Framework. In Refining Email Templates, I described updating the templates for our new Oxygen-based responsive templates.
Here's the new reminder_html.php email template:
<?php use yii\helpers\Html; use yii\helpers\Url; use common\components\MiscHelpers; use frontend\models\Meeting; use frontend\models\UserContact; /* @var $this \yii\web\View view component instance */ /* @var $message \yii\mail\BaseMessage instance of newly created mail message */ ?> <tr> <td align="center" valign="top" width="100%" style="background-color: #f7f7f7;" class="content-padding"> <center> <table cellspacing="0" cellpadding="0" width="600" class="w320"> <tr> <td class="header-lg"> Reminder of Your Meeting </td> </tr> <tr> <td class="free-text"> Just a reminder about your upcoming meeting <?php echo $display_time; ?> <?php // this code is similar to code in finalize-html if ($chosen_place!==false) { ?> at <?php echo $chosen_place->place->name; ?> (<?php echo $chosen_place->place->vicinity; ?>, <?php echo HTML::a(Yii::t('frontend','map'),$links['view_map']); ?>) <?php } else { ?> via phone or video conference. <?php } ?> <br /> Click below to view more details to view the meeting page. </td> </tr> <tr> <td class="button"> <div><!--[if mso]> <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="http://" style="height:45px;v-text-anchor:middle;width:155px;" arcsize="15%" strokecolor="#ffffff" fillcolor="#ff6f6f"> <w:anchorlock/> <center style="color:#ffffff;font-family:Helvetica, Arial, sans-serif;font-size:14px;font-weight:regular;">My Account</center> </v:roundrect> <![endif]--><a class="button-mobile" href="<?php echo $links['view'] ?>" style="background-color:#ff6f6f;border-radius:5px;color:#ffffff;display:inline-block;font-family:'Cabin', Helvetica, Arial, sans-serif;font-size:14px;font-weight:regular;line-height:45px;text-align:center;text-decoration:none;width:155px;-webkit-text-size-adjust:none;mso-hide:all;">Visit Meeting Page</a></div> </td> </tr> <tr> <td class="mini-large-block-container"> <table cellspacing="0" cellpadding="0" width="100%" style="border-collapse:separate !important;"> <tr> <td class="mini-large-block"> <table cellpadding="0" cellspacing="0" width="100%"> <tr> <td style="text-align:left; padding-bottom: 30px;"> <strong>Helpful options:</strong> <p> <?php echo HTML::a(Yii::t('frontend','Inform them I\'m running late.'),$links['running_late']); ?> </p> </td> </tr> </table> </td> </tr> </table> </td> </tr> </table> </center> </td> </tr> <?php echo \Yii::$app->view->renderFile('@common/mail/section-footer-dynamic.php',['links'=>$links]) ?>
It includes the date, time, and chosen location (with an address and map link). I've also added the beginnings of a helpful options area with an initial command, "Inform them I'm running late":
When clicked, we'll email or SMS the other participant(s) that you might be five to ten minutes late. There's nothing else to do or type while you're in a hurry.
Perhaps an eventual mobile version of Meeting Planner will know your GPS location and let them know roughly how far away you are. I've begun tracking ideas like this in Asana for product planning—I'll ask the Envato Tuts+ editorial goddesses (shown below) if I can write about implementing feature and issue tracking in a future tutorial.
Enhancements to Reminders
The reminder email can actually use a few enhanced features:
- Completing the running late email implementation.
- Displaying contact information of other participants such as phone numbers and email addresses. The running late email might show just the contact information of the person running late.
- Display a static Google map showing the location of the meeting.
- Link to a feature to request or require a rescheduling of the meeting.
- Link to not just the map but directions to the location.
- Link to adjust your reminders.
It turns out that most of these features require more work than there is space for in this tutorial.
For instance, the idea of sending a running late email seems like a simple feature, right? It's a good example of the challenge that MVC frameworks sometimes impose on developers. Implementing a running late email feature required code across a number of files, including a new email template.
Implementing the Running Late Feature
Rather than share all the code changes required for this feature, I'll just summarize the places where change was necessary around the framework:
- The reminder email needed a link with a new command
- The
COMMAND_RUNNING_LATE
had to be defined in the Meeting model and controller, and it had to display a confirmation message.
Here's an example of what you see after asking for a late notice to be sent:
- The
sendLateNotice()
method had to be built in Meeting.php
- The late-html.php email template had to be built. This includes an option for the other participant to announce that they are "late as well."
- The
UserContact::buildContactString()
method had to be completed to include contact information for the person running late.
- The
ACTION_SENT_RUNNING_LATE
had to be created to record sending a late notice on behalf of this person in the MeetingLog. - The
sendLateNotice()
method had to check the log and display an error if the late notice had already been sent once.
Here's what the late notice already sent displays:
It was a lot of code to implement what seemed like a simple addition.
I waited to test the feature until all of the above changes had been made, and I was pleasantly surprised that they all worked exactly as intended. I only had to make a few cosmetic changes to the text.
Implementing Display of Participant Contact Information
While this feature already existed for iCal files, I needed to complete this feature for email-based meeting invitations. So I extended UserContact::buildContactString($user_id,$mode)
for $mode='html'
.
Here's the updated code:
public static function buildContactString($user_id,$mode='ical') { // to do - create a view for this that can be rendered $contacts = UserContact::getUserContactList($user_id); if (count($contacts)==0) return ''; if ($mode=='ical') { $str=''; } else if ($mode =='html') { $str='<p>'; } $str = \common\components\MiscHelpers::getDisplayName($user_id).': '; if ($mode=='ical') { $str.=' \\n'; } else if ($mode =='html') { $str.='<br />'; } foreach ($contacts as $c) { if ($mode=='ical') { $str.=$c->friendly_type.': '.$c->info.' ('.$c->details.')\\n'; } else if ($mode =='html') { $str.=$c->friendly_type.': '.$c->info.'<br />'.$c->details.'<br />'; } } if ($mode=='ical') { $str.=' \\n'; } else if ($mode =='html') { $str.='</p>'; } return $str; } }
I'm sure it will need some polishing as we move into alpha and beta tests, but the functionality is now there.
You can see the contact details displayed in the complete late notice above, but here's the segment it generates:
Polishing Reminders
Things went so well overall with these late mini-features that I added the link to adjust your reminders to the original reminder email as well.
With all this new code, I am certain that I will be polishing the reminders feature and improving it regularly over the next few weeks. However, as Meeting Planner has come together, more functionality is often possible—often with little work because there's a framework and a foundation. The clean data model and MVC framework regularly make incremental improvements relatively straightforward.
All of this is what makes building a startup both fun and challenging. And working with dragons (some days I can't believe they pay me to do this).
What's Next?
Meeting Planner's made tremendous progress the past few months. I'm beginning to experiment with WeFunder based on the implementation of the SEC's new crowdfunding rules. Please consider following our profile. I hope to write about this more in a future tutorial.
And certainly, the app still has a lot of shortcomings—please be sure to post your feedback in the comments or open a support ticket.
I hope you've enjoyed this episode. Watch for upcoming tutorials in our Building Your Startup With PHP series—there's still polish work ahead but also more big features.
Please feel free add your questions and comments below; I generally participate in the discussions. You can also reach me on Twitter @reifman directly.
Comments