1. Code
  2. PHP
  3. Yii

Building Your Startup: Sending Reminders

Scroll to top
This post is part of a series called Building Your Startup With PHP.
Building Your Startup: Dynamic Ajax Forms for Scheduling
Building Your Startup: The Dashboard Foundation
Final product imageFinal product imageFinal product image
What You'll Be Creating

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:

1
# m h  dom mon dow   command

2
*/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:

1
// frequent cron task will call to check on due reminders

2
public static function check() {
3
  $mrs = MeetingReminder::find()->where('due_at<='.time().' and status='.MeetingReminder::STATUS_PENDING)->all();
4
  foreach ($mrs as $mr) {
5
    // process each meeting reminder

6
    MeetingReminder::process($mr);
7
  }
8
}

Processing a Reminder

MeetingReminder::process() gathers the details needed to create a reminder email. This includes the reminder recipient, meeting details, and time:

1
public static function process($mr) {
2
      // fetch the reminder

3
      // deliver the email or sms

4
      // send updates about recent meeting changes made by $user_id

5
      $user_id = $mr->user_id;
6
      $meeting_id = $mr->meeting_id;
7
      $mtg = Meeting::findOne($meeting_id);
8
      // only send reminders for meetings that are confirmed

9
      if ($mtg->status!=Meeting::STATUS_CONFIRMED) return false;
10
      // only send reminders that are less than a day late - to do - remove after testing period

11
      if ((time()-$mr->due_at)>(24*3600+1)) return false;
12
      $u = \common\models\User::findOne($user_id);
13
      // ensure there is an auth key for the recipient user

14
      if (empty($u->auth_key)) {
15
        return false;
16
      }
17
      // prepare data for the message

18
      // get time

19
      $chosen_time = Meeting::getChosenTime($meeting_id);
20
      $timezone = MiscHelpers::fetchUserTimezone($user_id);
21
      $display_time = Meeting::friendlyDateFromTimestamp($chosen_time->start,$timezone);
22
      // get place

23
      $chosen_place = Meeting::getChosenPlace($meeting_id);
24
25
      $a=['user_id'=>$user_id,
26
       'auth_key'=>$u->auth_key,
27
       'email'=>$u->email,
28
       'username'=>$u->username
29
     ];
30
       // check if email is okay and okay from this sender_id

31
      if (User::checkEmailDelivery($user_id,0)) {
32
          // Build the absolute links to the meeting and commands

33
          $links=[
34
            'home'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_HOME,0,$a['user_id'],$a['auth_key']),
35
            'view'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_VIEW,0,$a['user_id'],$a['auth_key']),
36
            'footer_email'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_FOOTER_EMAIL,0,$a['user_id'],$a['auth_key']),
37
            'footer_block'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_FOOTER_BLOCK,0,$a['user_id'],$a['auth_key']),
38
            'footer_block_all'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_FOOTER_BLOCK_ALL,0,$a['user_id'],$a['auth_key']),
39
            'running_late'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_RUNNING_LATE,0,$a['user_id'],$a['auth_key']),
40
            'view_map'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_VIEW_MAP,0,$a['user_id'],$a['auth_key'])
41
          ];
42
          // send the message

43
          $message = Yii::$app->mailer->compose([
44
            'html' => 'reminder-html',
45
            'text' => 'reminder-text'
46
          ],
47
          [
48
            'meeting_id' => $mtg->id,
49
            'sender_id'=> $user_id,
50
            'user_id' => $a['user_id'],
51
            'auth_key' => $a['auth_key'],
52
            'display_time' => $display_time,
53
            'chosen_place' => $chosen_place,
54
            'links' => $links,
55
            'meetingSettings' => $mtg->meetingSettings,
56
        ]);
57
          if (!empty($a['email'])) {
58
            $message->setFrom(['support@meetingplanner.com'=>'Meeting Planner']);
59
            $message->setTo($a['email'])
60
                ->setSubject(Yii::t('frontend','Meeting Reminder: ').$mtg->subject)
61
                ->send();
62
          }
63
       }
64
      $mr->status=MeetingReminder::STATUS_COMPLETE;
65
      $mr->update();
66
    }

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:

1
// check if email is okay and okay from this sender_id

2
      if (User::checkEmailDelivery($user_id,0)) {
3
          // Build the absolute links to the meeting and commands

4
          $links=[
5
            'home'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_HOME,0,$a['user_id'],$a['auth_key']),
6
            'view'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_VIEW,0,$a['user_id'],$a['auth_key']),
7
            'footer_email'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_FOOTER_EMAIL,0,$a['user_id'],$a['auth_key']),
8
            'footer_block'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_FOOTER_BLOCK,0,$a['user_id'],$a['auth_key']),
9
            'footer_block_all'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_FOOTER_BLOCK_ALL,0,$a['user_id'],$a['auth_key']),
10
            'running_late'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_RUNNING_LATE,0,$a['user_id'],$a['auth_key']),
11
            'view_map'=>MiscHelpers::buildCommand($mtg->id,Meeting::COMMAND_VIEW_MAP,0,$a['user_id'],$a['auth_key'])
12
          ];
13
      

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:

1
    public static function checkEmailDelivery($user_id,$sender_id) {
2
      // check if this user_id receives email and if sender_id not blocked

3
      // check if all email is turned off

4
      $us = UserSetting::safeGet($user_id);
5
      if ($us->no_email != UserSetting::EMAIL_OK) {
6
        return false;
7
      }
8
      // check if no sender i.e. system notification

9
      if ($sender_id==0) {
10
        return true;
11
      }
12
      // check if sender is blocked

13
      $ub = UserBlock::find()->where(['user_id'=>$user_id,'blocked_user_id'=>$sender_id])->one();
14
      if (!is_null($ub)) {
15
        return false;
16
      }
17
      return true;
18
    }

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:

1
<?php
2
use yii\helpers\Html;
3
use yii\helpers\Url;
4
use common\components\MiscHelpers;
5
use frontend\models\Meeting;
6
use frontend\models\UserContact;
7
/* @var $this \yii\web\View view component instance */
8
/* @var $message \yii\mail\BaseMessage instance of newly created mail message */
9
?>
10
<tr>
11
  <td align="center" valign="top" width="100%" style="background-color: #f7f7f7;" class="content-padding">
12
    <center>
13
      <table cellspacing="0" cellpadding="0" width="600" class="w320">
14
        <tr>
15
          <td class="header-lg">
16
            Reminder of Your Meeting
17
          </td>
18
        </tr>
19
        <tr>
20
          <td class="free-text">
21
            Just a reminder about your upcoming meeting <?php echo $display_time; ?>
22
            <?php
23
            // this code is similar to code in finalize-html

24
            if ($chosen_place!==false) {
25
            ?>
26
            &nbsp;at <?php echo $chosen_place->place->name; ?>&nbsp;
27
              (<?php echo $chosen_place->place->vicinity; ?>, <?php echo HTML::a(Yii::t('frontend','map'),$links['view_map']); ?>)
28
              <?php
29
            } else {
30
            ?>
31
            &nbsp;via phone or video conference.
32
            <?php
33
              }
34
            ?>
35
            <br />
36
            Click below to view more details to view the meeting page.
37
          </td>
38
        </tr>
39
      <tr>
40
        <td class="button">
41
          <div><!--[if mso]>

42
            <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">

43
              <w:anchorlock/>

44
              <center style="color:#ffffff;font-family:Helvetica, Arial, sans-serif;font-size:14px;font-weight:regular;">My Account</center>

45
            </v:roundrect>

46
          <![endif]--><a class="button-mobile" href="<?php echo $links['view'] ?>"
47
          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>
48
        </td>
49
      </tr>
50
      <tr>
51
        <td class="mini-large-block-container">
52
          <table cellspacing="0" cellpadding="0" width="100%"  style="border-collapse:separate !important;">
53
            <tr>
54
              <td class="mini-large-block">
55
                <table cellpadding="0" cellspacing="0" width="100%">
56
                  <tr>
57
                    <td style="text-align:left; padding-bottom: 30px;">
58
                      <strong>Helpful options:</strong>
59
                      <p>
60
                        <?php
61
                          echo HTML::a(Yii::t('frontend','Inform them I\'m running late.'),$links['running_late']);
62
                        ?>
63
                      </p>
64
                    </td>
65
                  </tr>
66
                </table>
67
              </td>
68
            </tr>
69
          </table>
70
        </td>
71
      </tr>
72
    </table>
73
  </center>
74
  </td>
75
</tr>
76
<?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":

Meeting Planner - Reminder of Your Meeting EmailMeeting Planner - Reminder of Your Meeting EmailMeeting Planner - Reminder of Your Meeting Email

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.

Meeting Planner Reminders - Game of Thrones Daenerys Targaryen and Dragon our editorial goddessMeeting Planner Reminders - Game of Thrones Daenerys Targaryen and Dragon our editorial goddessMeeting Planner Reminders - Game of Thrones Daenerys Targaryen and Dragon our editorial goddess

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

Meeting Planner Reminders - Late Notice EmailMeeting Planner Reminders - Late Notice EmailMeeting Planner Reminders - Late Notice Email

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:

Meeting Planner Reminders - Late Notice ConfirmationMeeting Planner Reminders - Late Notice ConfirmationMeeting Planner Reminders - Late Notice Confirmation
  • 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: 

Meeting Planner Reminders - Error message - Late Notice Already SentMeeting Planner Reminders - Error message - Late Notice Already SentMeeting Planner Reminders - Error message - Late Notice Already Sent

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:

1
public static function buildContactString($user_id,$mode='ical') {
2
    // to do - create a view for this that can be rendered

3
    $contacts = UserContact::getUserContactList($user_id);
4
    if (count($contacts)==0) return '';
5
    if ($mode=='ical') {
6
        $str='';
7
    } else if ($mode =='html') {
8
        $str='<p>';
9
    }
10
    $str = \common\components\MiscHelpers::getDisplayName($user_id).': ';
11
    if ($mode=='ical') {
12
        $str.=' \\n';
13
    } else if ($mode =='html') {
14
        $str.='<br />';
15
    }
16
    foreach ($contacts as $c) {
17
      if ($mode=='ical') {
18
        $str.=$c->friendly_type.': '.$c->info.' ('.$c->details.')\\n';
19
      } else if ($mode =='html') {
20
        $str.=$c->friendly_type.': '.$c->info.'<br />'.$c->details.'<br />';
21
      }
22
    }
23
    if ($mode=='ical') {
24
        $str.=' \\n';
25
    } else if ($mode =='html') {
26
        $str.='</p>';
27
    }
28
    return $str;
29
  }
30
}

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:

Meeting Planner Reminder - Contact DetailsMeeting Planner Reminder - Contact DetailsMeeting Planner Reminder - Contact Details

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.

Related Links

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.