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 the prior episode, I described building the infrastructure for tracking changes to meetings so we would know how to share updates with participants. In this episode, I'll cover monitoring the log and delivering email notifications of changes.
If you haven't tried out Meeting Planner yet, go ahead and schedule your first meeting. As your colleagues and friends respond, you'll see notifications in action. I do participate in the comment threads below, so please share your feedback. 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 Meeting Updates
With the MeetingLog from the prior episode, we'll regularly monitor for when changes are a few minutes old and consolidate them into a single update to the other participant(s) or meeting organizer.
Our DaemonController process actionFrequent will check for Meetings with fresh updates every few minutes:
public function actionFrequent() { // called every three to five minutes // notify users about fresh changes Meeting::findFresh();
Choosing the Right People to Notify
Meeting::findFresh()
looks at the log for entries that are older than MeetingLog::TIMELAPSE
, currently five minutes. When it finds them, it looks at each of the actors involved in making the changes, and it notifies them via Meeting::notify()
:
public static function findFresh() { // identify all meetings with log entries not yet cleared $meetings = Meeting::find()->where('logged_at-cleared_at>0')->all(); foreach ($meetings as $m) { // to do - choose a different safe gap, for now an hour if ((time()-$m->logged_at)>3600) { // to do - consider clearing out these old ones continue; } // uncleared log entry older than TIMELAPSE if ((time()-$m->logged_at) > MeetingLog::TIMELAPSE) { // $logs = MeetingLog::find()->where(['meeting_id'=>$m->id])->groupBy('actor_id')->all(); $current_actor=0; foreach ($logs as $log) { if ($log->actor_id<>$current_actor) { $current_actor = $log->actor_id; // new actor, let's notify others if ($log->actor_id==$m->owner_id) { // this is the organizer // notify the participants //echo 'notify participants'; foreach ($m->participants as $p) { $m->notify($m->id,$p->id); } } else { // this is a participant // notify the organizer and // to do - when there are multiple participants $m->notify($m->id,$m->owner_id); } } else { // this log entry by same actor as last continue; } } // clear the log for this meeting Meeting::clearLog($m->id); } } }
If the meeting organizer made the change, then the participant(s) need to be notified. If a participant made the change, then the organizer (and in the future other participants) need to be notified; multiple participant meetings will be implemented in later tutorials.
Creating a Text Summary of the Meeting Log
In Meeting Planner's current user interface, a number of logged changes refer to identical items and cancel each other out. For example, if you click the switch for a place, it first goes to reject. You need to click it again to accept it. Both changes are logged.
I had to write extra code to create a concise, accurate textual summary of the log.
For example, here is the historical MeetingLog of actions. Notice the repetition of actions by Cloudster that cancel each other out on the same places and times:
A basic textual representation would say:
Cloudster added note Thanks for the reminder. I'll be sure to., accepted time Thu Jun 9 at 12:00 PM, accepted time Fri Jun 10 at 12:00 PM, rejected time Fri Jun 10 at 12:00 PM, rejected time Thu Jun 9 at 12:00 PM, rejected time Wed Jun 8 at 12:30 PM, rejected place Chaco Canyon Organic Cafe, accepted place Chaco Canyon Organic Cafe, rejected place Chaco Canyon Organic Cafe, accepted place No Bones Beach Club and rejected place No Bones Beach Club.
How do we create a simple textual summary of the resulting changes they made for the notification email such as shown below:
Within Meeting::notify()
, we request a history of activity for this meeting since the last notification:
// build the english language notification $history = MeetingLog::getHistory($meeting_id,$user_id,$mtg->cleared_at);
Here's the code that builds a textual string of what's occurred. I'll likely do polishing work in the future to clean up the clarity of the description. For example, the final version provides a proper grammatical list with the appropriate use of commas then 'and' for the last time.
public static function getHistory($meeting_id,$user_id,$cleared_at) { // build a textual history of events for this meeting // not performed by this user_id and since cleared_at $str =''; $events = MeetingLog::find()->where(['meeting_id'=>$meeting_id])->andWhere('actor_id<>'.$user_id)->andWhere('created_at>'.$cleared_at)->orderBy(['created_at' => SORT_DESC,'actor_id'=>SORT_ASC])->all(); $num_events = count($events); $cnt =1; $current_actor = 0; $current_str=''; $items_mentioned =[]; foreach ($events as $e) { if ($e->actor_id <> $current_actor) { // new actor, update the overall string $str.=$current_str.'<br />'; // reset the current actor's event string $current_str=''; $current_actor = $e->actor_id; $actor = MiscHelpers::getDisplayName($e->actor_id); } else { $actor = ''; } $action = $e->getMeetingLogCommand(); $item = $e->getMeetingLogItem(); if (in_array($e->item_id,$items_mentioned)) { // only mention item the first time it appears (last action, as sorted) continue; } else { $items_mentioned[]=$e->item_id; if ($actor=='') { if ($cnt == $num_events) { $current_str.=' and '.$action.' '.$item; } else { $current_str.=', '.$action.' '.$item; } } else { $current_str.=$actor.' '.$action.' '.$item; } // count events $cnt+=1; } } // add last current_str (may be empty) $str.=$current_str.'<br />'; return $str; }
Essentially, getHistory() is user specific. The query below sorts actions in reverse order because mostly, the last change has the dominant impact:
$events = MeetingLog::find()->where(['meeting_id'=>$meeting_id])->andWhere('actor_id<>'.$user_id)->andWhere('created_at>'.$cleared_at)->orderBy(['created_at' => SORT_DESC,'actor_id'=>SORT_ASC])->all();
In the future, there will be multiple participants whose actions overlap in time, so we're grouping the history by actor_id
.
Then, I'm tracking the $current_actor
as we build the textualization so we only mention their name once, i.e. "Jeff did these actions. John did these actions," not "Jeff did this, John did this, Jeff did this and John did this and John did this."
Similarly, I'm tracking the mention of objects in $items_mentioned
and ignoring the earlier events, only providing the latest, dominant action on a place or time, i.e. "Jeff accepted No Bones Beach Club" not "Jeff rejected No Bones Beach Club, Jeff accepted No Bones Beach Club."
The code was intricate and fun to write. The resulting textualization (shown above) is fun to observe.
Delivering the Notification Email
In the Refining Email Templates tutorial, I described the switch to our new responsive Oxygen templates. This required an overhaul of the notify-html.php template and will require incremental polishing over time.
Here's an excerpt from the template:
<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"> Changes to Your Meeting </td> </tr> <tr> <td class="free-text"> <p>Hi <?php echo Html::encode(MiscHelper::getDisplayName($user->id)); ?>, changes have been made to your meeting.</p> <p><?php echo $history; ?></p> <p>Please click the button below to view the meeting page.</p> </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> </table> </center> </td> </tr>
Sending the Email
Delivering the email is done similarly to our meeting invitations with the Yii2 SwiftMailer extension via our Mailgun SMTP configuration.
$message = Yii::$app->mailer->compose([ 'html' => 'notify-html', 'text' => 'notify-text' ], [ 'meeting_id' => $mtg->id, 'sender_id'=> $user_id, 'user_id' => $a['user_id'], 'auth_key' => $a['auth_key'], 'links' => $links, 'meetingSettings' => $mtg->meetingSettings, 'history'=>$history, ]); if (!empty($a['email'])) { $message->setFrom(['[email protected]'=>'Meeting Planner']); $message->setReplyTo('mp_'.$meeting_id.'@meetingplanner.io'); $message->setTo($a['email']) ->setSubject(Yii::t('frontend','Meeting Update: ').$mtg->subject) ->send(); }
What's Next?
I hope you've enjoyed the two-part notifications tutorials. I found building the log and creating a textual description of the history to be challenging and fun to develop. And it proved useful with debugging and summarizing meeting updates for the notifications. However, testing wasn't easy as there were always timestamps and background tasks that needed to be manipulated to verify the code was working.
Please try out the notifications functionality by scheduling your first meeting. You should receive regular notifications as your invitees respond. 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. I may write about this more as part of our series.
Watch for upcoming tutorials in the Building Your Startup With PHP series. There are a few more big features coming up.
Comments