Статьи

Создание вашего стартапа с помощью PHP: команды электронной почты

Конечный продукт
Что вы будете создавать

Это руководство является частью серии « Создай свой стартап с помощью PHP» на Envato Tuts +. В этой серии я проведу вас через запуск стартапа от концепции до реальности, используя мое приложение Meeting Planner в качестве примера из реальной жизни. На каждом этапе я буду публиковать код Планировщика собраний в качестве примеров с открытым исходным кодом, из которых вы можете извлечь уроки. Я также буду решать вопросы, связанные с бизнесом по мере их возникновения

В последнем уроке мы начали рассылать приглашения на собрания по электронной почте, которые содержали многочисленные ссылки для ответов участников: просмотр страницы собрания, принятие всех мест и времени, отклонение места или времени и т. Д.

В этом уроке я расскажу, как я выбрал конструирование и обработку этих ссылок безопасным и функциональным способом. Большинство участников собрания (особенно поначалу) раньше не пользовались Meeting Planner — они будут нам неизвестны. Тем не менее, мы хотим надежно подтвердить их подлинность, чтобы они могли просматривать и взаимодействовать с запросами на собрания и создавать свои собственные на будущее. Мы также хотим иметь некоторые меры предосторожности, когда люди пересылают приглашения на собрания со своими безопасными кодами, не думая о последствиях (возможно, неофиты или просто обычные люди).

Напомню, что весь код для Meeting Planner написан на Yii2 Framework для PHP. Если вы хотите узнать больше об Yii2, ознакомьтесь с серией моих параллельных программ по программированию на Yii2 в Envato Tuts +.

К тому времени, когда вы читаете это, вы, вероятно, сможете начать испытывать приглашения на встречи на живом веб-сайте MeetingPlanner.io (имейте в виду, что предстоит еще много работы по улучшению пользовательского опыта и улучшения). Я участвую в комментариях ниже и особенно заинтересован, если у вас есть дополнительные идеи или вы хотите предложить темы для будущих уроков. Вы также можете связаться со мной в Twitter @reifman .

В процессе проектирования я думал о командах в электронной почте как об элементах как процесса планирования собрания, так и времени, предшествующего фактическому событию.

Когда участник получает приглашение по электронной почте, ему необходимо предоставить безопасные разрешения для просмотра страницы собрания, а также для ответа на вопрос, подходят ли ему определенные места и время.

После того, как встреча завершена, мы можем начать отправлять напоминания участникам, которые предлагают специальные команды, такие как «Я опаздываю», которые отправляют текст другим участникам вашего затруднительного положения, или «запрашивают изменение на месте» или «отменяют».

Все эти команды должны проверять подлинность получателя и обеспечивать ему безопасный доступ к веб-сайту, чтобы Meeting Planner мог правильно обрабатывать их ответы. Тем не менее, сайт также должен защищать, чтобы не публиковать полный список контактов участника, если они по ошибке отправили электронное письмо с приглашением на встречу другой стороне, которая затем щелкает ссылки. Дополнительное лицо легко войдет в учетную запись получателя Планировщика собраний и сможет видеть все свои собрания и личную информацию.

По мере того, как я размышлял над концепцией приложения, существует большое количество потенциальных команд. Вот некоторые из них в первоначальном приглашении (которое я могу упростить в какой-то момент, чтобы улучшить взаимодействие с пользователем):

  • Посмотреть встречу
  • Принять все места и время
  • Отклонить приглашение
  • Принять или отклонить определенные места
  • Принять или отклонить определенные даты и время
  • Завершить встречу *
  • Предложить другое место *
  • Предложить другую дату и время *
  • Выберите последнее место *
  • Выберите окончательную дату и время *
  • Добавить или ответить на заметки встреч
  • Просмотр карты расположения мест в контексте встречи
  • Проверьте настройки электронной почты
  • Заблокировать этот органайзер от отправки вам сообщений
  • Отписаться от всех писем Планировщика собраний

Примечание. Внешний вид помеченных (*) элементов зависит от настроек собрания организатора.

После того, как встреча запланирована, есть также различные последующие команды:

  • Перенести встречу
  • Отменить встречу
  • Покажи мне карту
  • Получить направление движения
  • Запросить изменение времени
  • Запросить изменение места
  • Сообщите сторонам, что вы опаздываете

Учитывая множество различных команд, я чувствовал, что было бы полезно аутентифицировать и обрабатывать их все одинаково в одном контроллере.

На данный момент я создал единую точку обработки в MeetingController, но я ожидаю, что позже я создам выделенный CommandController. Я также рассмотрел возможность создания контроллера доступа API в будущем и направления всей функциональности приложения через эту единственную безопасную точку входа. А пока я задержусь на этом.

Для начала я дал каждой команде конкретное определение константы в модели Meeting.php:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
const COMMAND_HOME = 5;
 const COMMAND_VIEW = 10;
 const COMMAND_VIEW_MAP = 20;
 const COMMAND_FINALIZE = 50;
 const COMMAND_CANCEL = 60;
 const COMMAND_ACCEPT_ALL = 70;
 const COMMAND_ACCEPT_PLACE = 100;
 const COMMAND_REJECT_PLACE = 110;
 const COMMAND_ACCEPT_ALL_PLACES = 120;
 const COMMAND_CHOOSE_PLACE = 150;
 const COMMAND_ACCEPT_TIME = 200;
 const COMMAND_REJECT_TIME = 210;
 const COMMAND_ACCEPT_ALL_TIMES = 220;
 const COMMAND_CHOOSE_TIME = 250;
 const COMMAND_ADD_PLACE = 300;
 const COMMAND_ADD_TIME = 310;
 const COMMAND_ADD_NOTE = 320;
 const COMMAND_FOOTER_EMAIL = 400;
 const COMMAND_FOOTER_BLOCK = 410;
 const COMMAND_FOOTER_BLOCK_ALL = 420;

Я решил, что на данный момент каждая команда будет иметь следующие аргументы URL:

  • $id для meeting_Id
  • $cmd для командного действия (из констант выше)
  • $obj_id для любого объекта, на который можно воздействовать, например, место или дата
  • $actor_id для user_id, вызывающего команду
  • $k для ключа, который аутентифицирует $ actor_id для своей учетной записи

Сначала большинство участников не будут зарегистрированы, но мы создаем ключи аутентификации, связанные с их электронными письмами с приглашением при создании собрания. Вот так мы аутентифицируем их ссылки из приглашений по электронной почте.

Вот пример URL-ссылки, встроенной в электронные письма:

http://meetingplanner.io/meeting/command?id=27&cmd=70&actor_id=18&k=9cHGl...1x

Учитывая сложность создания URL-адресов с различными аргументами из разных мест кода, я создал библиотеку /common/components/MiscHelpers.php , которая началась с buildCommand :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<?php
namespace common\components;
use yii\helpers\Url;
use common\models\User;
 
//use \yii\helpers\FormatConverter;
 
class MiscHelpers {
 
  public static function buildCommand($meeting_id,$cmd=0,$obj_id=0,$actor_id=0,$auth_key=») {
    return Url::to([‘meeting/command’,’id’=>$meeting_id,’cmd’=>$cmd,’actor_id’=>$actor_id,’k’=>$auth_key,’obj_id’=>$obj_id,],true);
   }
 }
?>

Вот пример нашего файла-приглашения-html.php, который вызывает buildCommand() для отображения строк мест. В каждом месте есть команды, которые должны предоставить все эти аргументы в URL:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
   foreach($places as $p) {
     ?>
     <tr>
       <td width=»300″>
         <p>
         <?php echo $p->place->name;
         <br/ >
         <span style=»font-size:75%;»><?php echo $p->place->vicinity;
         MiscHelpers::buildCommand($meeting_id,Meeting::COMMAND_VIEW_MAP,$p->id,$user_id,$auth_key));
       </p>
     </td>
     <td width=»300″ >
       <?php echo HTML::a(Yii::t(‘frontend’,’acceptable’),MiscHelpers::buildCommand($meeting_id,Meeting::COMMAND_ACCEPT_PLACE,$p->id,$user_id,$auth_key));
       <?php
         if ($meetingSettings->participant_choose_place) { ?>
         |
         <?php
         }
         ?>
     </td>
     </tr>
   <?php
       }
   ?>

Вы можете увидеть, как они выглядят ниже:

Места для планирования собраний и команды для мест

Затем я построил функцию контроллера для аутентификации и обработки команд. Вот первая часть:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public function actionCommand($id,$cmd=0,$obj_id=0,$actor_id=0,$k=0) {
     $performAuth = true;
     $authResult = false;
     // Manage the incoming session
     if (!Yii::$app->user->isGuest) {
       if (Yii::$app->user->getId()!=$actor_id) {
         // to do: give user a choice of not logging out
         Yii::$app->user->logout();
       } else {
         // user actor_id is already logged in
         $authResult = true;
         $performAuth = false;
       }
     }

Первоначально я хотел предоставить гарантии для собственного тестирования, а также людей, пересылающих электронные письма со своими ссылками для аутентификации.

Одно событие, которое я проверяю, состоит в том, является ли $actor_id другим пользователем, чем зарегистрированный в данный момент пользователь. Это может произойти во время тестирования с несколькими учетными записями, или это может произойти, если участник перенаправит свое приглашение организатору. В конечном итоге я предоставлю информацию о ситуации и предложу выбор для людей. Однако сейчас я просто выхожу из системы перед аутентификацией запрашивающего пользователя.

Если пользователь уже вошел в систему как $actor_id , он проходит проверку подлинности. Если они не аутентифицированы, мы запускаем проверку аутентификации:

01
02
03
04
05
06
07
08
09
10
11
12
13
if ($performAuth) {
        //echo ‘guest’;
         $person = new \common\models\User;
         $identity = $person->findIdentity($actor_id);
         if ($identity->validateAuthKey($k)) {
           Yii::$app->user->login($identity);
           // echo ‘authenticated’;
           $authResult=true;
         } else {
           // echo ‘fail’;
           $authResult=false;
         }
     }

Для этого мы используем встроенные в Yii функции findIdentity и validateAuthKey .

В ближайшем будущем я планирую сделать проверку подлинности по электронной почте ограниченным доступом к функциям учетной записи. Например, когда пользователи не входят в систему, а нажимают на ссылки команд, мы ограничиваем их действия только этой встречей и несколькими связанными функциями. Они не смогут видеть другие встречи, друзей владельца учетной записи и т. Д. Однако мы предложим им дружественную ссылку для входа в свою учетную запись с помощью пароля или социальной учетной записи. Это сведет к минимуму воздействие на безопасность людей, рассылающих приглашения.

Точно так же, если новый пользователь, который никогда ранее не регистрировался, нажимает на командную ссылку, мы представим напоминания, чтобы он мог зарегистрироваться и создать пароль или социальный логин. Модель User.php имеет поля состояния, которые указывают, зарегистрировался ли пользователь когда-либо самостоятельно или был ли он пассивно приглашен на собрание.

На данный момент, если аутентификация прошла успешно, мы можем просто обработать каждую из команд:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
if (!$authResult) {
       $this->redirect([‘site/authfailure’]);
     } else {
       // TO DO check if user is PASSIVE
       // if active, set SESSION to indicate log in through command
       // if PASSIVE login
       // — if no password, setflash to link to create password
       // — meeting page — flash to security limitation of that meeting view
       // — meeting index — redirect to view only that meeting (do this on other index pages too)
       $meeting = $this->findModel($id);
       switch ($cmd) {
         case Meeting::COMMAND_HOME:
           $this->goHome();
         break;
         case Meeting::COMMAND_VIEW:
           $this->redirect([‘meeting/view’,’id’=>$id]);
         break;
         case Meeting::COMMAND_VIEW_MAP:
           $this->redirect([‘meeting/viewplace’,’id’=>$id,’meeting_place_id’=>$obj_id]);
         break;
         case Meeting::COMMAND_FINALIZE:
           $this->redirect([‘meeting/finalize’,’id’=>$id]);
         break;
         case Meeting::COMMAND_CANCEL:
           $this->redirect([‘meeting/cancel’,’id’=>$id]);
         break;
         case Meeting::COMMAND_ACCEPT_ALL:
           MeetingTimeChoice::setAll($id,$actor_id);
           MeetingPlaceChoice::setAll($id,$actor_id);
           $this->redirect([‘meeting/view’,’id’=>$id]);
         break;
         case Meeting::COMMAND_ACCEPT_ALL_PLACES:
           MeetingPlaceChoice::setAll($id,$actor_id);
           $this->redirect([‘meeting/view’,’id’=>$id]);
           break;
         case Meeting::COMMAND_ACCEPT_ALL_TIMES:
           MeetingTimeChoice::setAll($id,$actor_id);
           $this->redirect([‘meeting/view’,’id’=>$id]);
           break;
         case Meeting::COMMAND_ADD_PLACE:
           $this->redirect([‘meeting-place/create’,’meeting_id’=>$id]);
         break;
         case Meeting::COMMAND_ADD_TIME:
           $this->redirect([‘meeting-time/create’,’meeting_id’=>$id]);
         break;
         case Meeting::COMMAND_ADD_NOTE:
           $this->redirect([‘meeting-note/create’,’meeting_id’=>$id]);
         break;
         case Meeting::COMMAND_ACCEPT_PLACE:
           $mpc = MeetingPlaceChoice::find()->where([‘meeting_place_id’=>$obj_id,’user_id’=>$actor_id])->one();
           MeetingPlaceChoice::set($mpc->id,MeetingPlaceChoice::STATUS_YES);
           $this->redirect([‘meeting/view’,’id’=>$id]);
           break;
         case Meeting::COMMAND_REJECT_PLACE:
           $mpc = MeetingPlaceChoice::find()->where([‘meeting_place_id’=>$obj_id,’user_id’=>$actor_id])->one();
           MeetingPlaceChoice::set($mpc->id,MeetingPlaceChoice::STATUS_NO);
           $this->redirect([‘meeting/view’,’id’=>$id]);
           break;
         case Meeting::COMMAND_CHOOSE_PLACE:
           MeetingPlace::setChoice($id,$obj_id,$actor_id);
           $this->redirect([‘meeting/view’,’id’=>$id]);
         break;
         case Meeting::COMMAND_ACCEPT_TIME:
           $mtc = MeetingTimeChoice::find()->where([‘meeting_time_id’=>$obj_id,’user_id’=>$actor_id])->one();
           MeetingTimeChoice::set($mtc->id,MeetingTimeChoice::STATUS_YES);
           $this->redirect([‘meeting/view’,’id’=>$id]);
           break;
         case Meeting::COMMAND_REJECT_TIME:
           $mtc = MeetingTimeChoice::find()->where([‘meeting_time_id’=>$obj_id,’user_id’=>$actor_id])->one();
           MeetingTimeChoice::set($mtc->id,MeetingTimeChoice::STATUS_NO);
           $this->redirect([‘meeting/view’,’id’=>$id]);
           break;
         case Meeting::COMMAND_CHOOSE_TIME:
           MeetingTime::setChoice($id,$obj_id,$actor_id);
           $this->redirect([‘meeting/view’,’id’=>$id]);
         break;
         case Meeting::COMMAND_FOOTER_EMAIL:
         case Meeting::COMMAND_FOOTER_BLOCK:
         case Meeting::COMMAND_FOOTER_BLOCK_ALL:
           $this->redirect([‘site\unavailable’,’meeting_id’=>$id]);
         break;
         default:
           $this->redirect([‘site\error’,’meeting_id’=>$id]);
           break;
       }
     }

Для функций, которые я еще не создал, я создал представление, указывающее, что функция недоступна, например, /views/site/unavailable.php или, если команда неправильно понята, то /views/site/error.php .

Давайте посмотрим на две команды примера. Во-первых, давайте посмотрим на предложение другого места:

1
2
3
case Meeting::COMMAND_ADD_PLACE:
           $this->redirect([‘meeting-place/create’,’meeting_id’=>$id]);
         break;

В этом случае функциональность требует, чтобы пользователь вернулся на наш веб-сайт, чтобы заполнить форму, где он может выбрать новое место. Итак, мы просто перенаправляем их на страницу создания места встречи для этого meeting_id . И они уже аутентифицированы и авторизованы сверху.

Вот пример этого — обратите внимание, что меню «хлебные крошки» отражает контекст встречи, например, Встреча на завтрак :

Команды электронной почты в Планировщике собраний - добавление места встречи

Во-вторых, давайте посмотрим на принятие всех дат и времени:

1
2
3
4
case Meeting::COMMAND_ACCEPT_ALL_TIMES:
           MeetingTimeChoice::setAll($id,$actor_id);
           $this->redirect([‘meeting/view’,’id’=>$id]);
           break;

В этом случае нам нужно принять все время для этой встречи и $actor_id . Принятие делается прозрачно за кадром. После этого мы можем перенаправить их для просмотра встречи.

Вот как это выглядит при достижении вида собрания со всем принятым, например, хорошо , хорошо , хорошо для мест и времени ниже:

Команды электронной почты Планировщика собраний - Принять все места и время

Реализация всех этих команд определенно заняла некоторое время, но функции Meeting Planner действительно начали отнимать жизнь. И я смог отправить свои первые приглашения в мир.

Женщина, с которой я встречалась, знала, что я близка к завершению этой функции, поэтому она решила побудить меня закончить это быстрее. Она сказала:

«Я понятия не имею, когда я увижу вас в следующий раз, потому что я еще не получил приглашение на встречу».

После нескольких дней дополнительной работы я отправил ей второе приглашение для планирования встречи — первое пришло к другу на тестирование.

Впечатляюще, когда моя подруга получила ее приглашение, она быстро попросила две полезные функции. Во-первых, она сказала, что не уверена, что сможет появиться на нашей дате, если событие не будет в календаре Google ее телефона (обычно я предпочитаю встречаться с пользователями iOS, а не Android). Следующий урок расскажет историю создания файла iCal (.ics) для импорта (чтобы моя дата знала, куда идти). Я не буду держать вас в напряжении — я закончил эту функцию как раз к нашему свиданию.

Во-вторых, она спросила о функции, о которой я подумал, но не осознал ее важность. Она хотела иметь возможность указать место с течением времени. Другими словами, ресторан Canlis в пятницу в 19:00, а Пасео в субботу в 20:00. В настоящее время места и время предлагаются отдельно, а не в комбинации. Я сохраню эту функцию для будущего эпизода.

Это поднимает общую проблему того, как в процессе запуска вы регулярно собираете отзывы людей и интегрируете их в свои требования и планирование развития. Не все ваши пользователи будут предлагать вам даты в обмен на их любимые функции. У меня запланирован учебный эпизод, чтобы обсудить, как это сделать в будущем, несмотря на отсутствие вторичной мотивации.

В следующем эпизоде ​​я подробно опишу создание файлов Календаря (.ics) для импорта в Календарь Google, Outlook и Apple Calendar с подробной информацией о приглашении. Включение контактных данных и карт, а также управление проблемами часовых поясов — все это ключевые аспекты.

Следите за будущими уроками в моей серии «Построение стартапа с помощью PHP» — надеюсь, вам не терпится опробовать Meeting Planner. Попробуйте прямо сейчас!

Пожалуйста, не стесняйтесь добавлять свои вопросы и комментарии ниже; Я стараюсь участвовать в дискуссиях на регулярной основе. Вы также можете связаться со мной в Twitter @reifman .