Creando su startup: Enviando recordatorios
Spanish (Español) translation by Elías Nicolás (you can also view the original English article)



Este tutorial forma parte de la serie Creando su Startup con PHP en Envato Tuts +. En esta serie, te estoy guiando a través del lanzamiento de un inicio de concepto a realidad utilizando mi aplicación de Meeting Planner como un ejemplo de la vida real. Cada paso a lo largo del camino, voy a lanzar el código de Meeting Planner como ejemplo en código abierto del que puede aprender. También me ocuparé de asuntos relacionados de negocios con el startup a medida que surjan.
En este tutorial en dos partes, describo cómo construí la infraestructura para los recordatorios y su entrega. Hoy, voy a guiarlo a través de monitoreo cuando entregar recordatorios y cómo enviar los correos electrónicos.
Si aún no ha probado Meeting Planner, siga adelante y programe su primera reunión. Participo en los hilos del comentario abajo, así que ¡dígame lo que usted piensa! Me interesa especialmente si desea sugerir nuevas funciones o temas para futuros tutoriales.
Como recordatorio, todo el código para Meeting Planner está escrito en el Framework Yii2 para PHP. Si desea obtener más información acerca de Yii2, consulte nuestra serie paralela Programación con Yii2.
Tiempo de supervisión para recordatorios
A medida que pasa el tiempo, debemos vigilar la tabla MeetingReminder para saber cuándo entregar recordatorios. Idealmente, queremos que los recordatorios sean entregados exactamente a tiempo, ej. Al minuto.
Ejecución de tareas de fondo
La puntualidad depende de la periodicidad con que ejecutamos las tareas de fondo para el monitoreo. Actualmente, en nuestra etapa pre-alpha, los estoy ejecutando cada cinco minutos:
1 |
# m h dom mon dow command
|
2 |
*/5 * * * * wget -O /dev/null http://meetingplanner.io/daemon/frequent |
Este
script llama a MeetingReminder::check(), que encuentra los
recordatorios de reunión que se deben y las solicitudes a process():
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 |
}
|
Procesamiento de un recordatorio
MeetingReminder::process() reúne los detalles necesarios para crear un correo electrónico de recordatorio. Esto incluye el destinatario del recordatorio, los detalles de la reunión y la hora:
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 |
}
|
La
función User::checkEmailDelivery() verifica que el usuario no ha
bloqueado los correos electrónicos del sistema (o de personas en
particular). Se asegura de que está bien enviar el recordatorio:
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 |
Aquí tiene el método User::checkEmailDelivery. En
primer lugar, comprueba si el usuario ha bloqueado completamente el
correo electrónico (o no) o si el mensaje se envía desde un usuario
bloqueado:
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 |
}
|
La nueva plantilla de correo electrónico de recordatorio
En el episodio Entregando tu invitación de la reunión, escribí sobre el envío de mensajes de correo electrónico dentro del marco Yii. En refinando plantillas de correo electronico, describí la actualización de las plantillas para nuestras nuevas plantillas de respuesta basadas en oxígeno.
Aquí está la nueva plantilla de correo electrónico reminder_html.php:
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 |
at <?php echo $chosen_place->place->name; ?> |
27 |
(<?php echo $chosen_place->place->vicinity; ?>, <?php echo HTML::a(Yii::t('frontend','map'),$links['view_map']); ?>) |
28 |
<?php
|
29 |
} else { |
30 |
?>
|
31 |
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]) ?> |
Incluye la fecha, la hora y la ubicación elegida (con una dirección y un enlace de mapa). También he agregado los principios de un área de opciones útil con un comando inicial, "Informales que estoy llegando tarde":



Al hacer clic en él, enviaremos por correo electrónico o SMS a los otros participantes que podrían tardar entre cinco y diez minutos. No hay nada más que hacer o escribir mientras tiene prisa.
Tal vez una eventual versión móvil de Meeting Planner conocerá su ubicación GPS y hacerles saber aproximadamente cuán lejos está usted. He empezado a seguir ideas como esta en Asana para la planificación de productos—preguntaré a las diosas editoriales de Envato Tuts+ (mostradas a continuación) si puedo escribir sobre la implementación de seguimiento de características y emisiones en un futuro tutorial.



Mejoras en los recordatorios
El correo electrónico de recordatorio puede utilizar algunas funciones mejoradas:
- Completar la implementación de correo electrónico que se ejecuta tarde.
- Visualización de la información de contacto de otros participantes, como números de teléfono y direcciones de correo electrónico. El correo electrónico que se ejecuta tarde puede mostrar sólo la información de contacto de la persona que llega tarde.
- Muestra un mapa de Google estático que muestra la ubicación de la reunión.
- Enlace a una función para solicitar o requerir una reprogramación de la reunión.
- Enlace a no sólo el mapa, sino las direcciones de la ubicación.
- Enlace para ajustar los recordatorios.
Resulta que la mayoría de estas características requieren más trabajo del que hay espacio para este tutorial.
Por ejemplo, la idea de enviar un correo electrónico de ejecución tarde parece una característica simple, ¿verdad? Es un buen ejemplo del reto que los frameworks MVC a veces imponen a los desarrolladores. La implementación de una función de correo electrónico de última ejecución requería código a través de varios archivos, incluida una nueva plantilla de correo electrónico.
Implementación de la función de ejecución tardía



En lugar de compartir todos los cambios de código necesarios para esta función, resumiré los lugares en los que era necesario cambiar el entorno:
- El correo electrónico de recordatorio necesitaba un enlace con un nuevo comando
- El COMMAND_RUNNING_LATE tenía que ser definido en el modelo y controlador de la reunión, y tenía que mostrar un mensaje de confirmación.
He aquí un ejemplo de lo que ves después de pedir que se envíe un aviso tardío:



- El método
sendLateNotice()tuvo que ser construido en Meeting.php
- La plantilla de correo electrónico de late-html.php tenían que ser construida. Esto incluye una opción para que el otro participante anuncie que están "tarde también".
- El
método
UserContact::buildContactString()tuvo que completarse para incluir información de contacto para la persona que se está ejecutando tarde.
- El
ACTION_SENT_RUNNING_LATEtuvo que ser creado para registrar el envío de un aviso tardío en nombre de esta persona en el MeetingLog. - El método
sendLateNotice()tuvo que comprobar el registro y mostrar un error si el aviso tardío ya se había enviado una vez.
Esto es lo que muestra el aviso tardío enviado:



Era un montón de código para implementar lo que parecía una simple adición.
Esperé a probar la función hasta que todos los cambios anteriores se habían hecho, y me sorprendió gratamente que todos trabajaron exactamente como se pretende. Sólo tuve que hacer algunos cambios cosméticos en el texto.
Implementación de la información de contacto del participante
Si
bien esta función ya existía para los archivos iCal, necesitaba
completar esta función para las invitaciones de reunión basadas en
correo electrónico. Así que extendí UserContact::buildContactString($user_id,$mode) para $mode = 'html'.
Aquí está el código actualizado:
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 |
}
|
Estoy seguro de que necesitará un poco de pulido a medida que avanzamos en las pruebas alfa y beta, pero la funcionalidad ya está ahí.
Puede ver los detalles de contacto mostrados en el aviso completo anterior, pero aquí está el segmento que genera:



Pulido de Recordatorios
Las cosas fueron tan bien en general con estas últimas mini-características que he añadido el enlace para ajustar sus recordatorios para el correo electrónico de recordatorio original también.
Con todo este nuevo código, estoy seguro de que estaré puliendo la función de recordatorios y mejorándola regularmente durante las próximas semanas. Sin embargo, como planificador de reuniones se ha unido, más funcionalidad es a menudo posible—a menudo con poco trabajo porque hay un framework y una base. El modelo de datos limpios y el framework MVC hacen regularmente mejoras incrementales relativamente sencillas.
Todo esto es lo que hace que la construcción de un startup tanto divertido y desafiante. Y trabajando con dragones (algunos días no puedo creer que me pagan para hacer esto).
¿Que sigue?
Meeting Planner ha hecho un tremendo progreso en los últimos meses. Estoy empezando a experimentar con WeFunder basado en la implementación de las nuevas reglas de crowdfunding de la SEC. Por favor considere seguir nuestro perfil. Espero escribir sobre esto más en un futuro tutorial.
Y, sin duda, la aplicación todavía tiene un montón de deficiencias—por favor asegúrese de publicar sus comentarios en los comentarios o abrir un ticket de soporte.
Espero que hayas disfrutado este episodio. Vea
los próximos tutoriales en nuestra serie Creando su Startup Con PHP, todavía hay trabajo polaco por delante, pero también funciones más
grandes.
Por favor, siéntase libre de añadir sus preguntas y comentarios a continuación; Generalmente participo en las discusiones. También puede contactarme directamente en Twitter @reifman.



