介绍
本教程是Envato Tuts +上的“ 使用PHP构建启动”系列的一部分。 在本系列文章中,我将以我的Meeting Planner应用程序作为真实示例,指导您完成从概念到现实的启动。 在此过程的每一步中,我都会将Meeting Planner代码作为开放源代码示例发布,您可以从中学习。 我还将解决与启动相关的业务问题。
此剧集涵盖了哪些内容?
在上一教程中 ,我们开始通过电子邮件发送会议邀请,其中包括许多链接,供参与者响应,即查看会议页面,接受所有地点和时间,拒绝地点或时间等。
在本教程中,我将回顾如何选择以安全,实用的方式构造和处理这些链接。 大多数会议参与者(尤其是最初参加会议的人)以前不会使用过会议计划程序-他们对我们来说是未知的。 但是,我们将需要对其进行安全地身份验证,以允许他们查看会议请求并与之交互并为将来创建自己的会议。 我们还希望在人们使用安全密码转发会议要求时,不用考虑后果(新芽胞藻!或者仅仅是普通人),就需要采取一些保护措施。
提醒一下,Meeting Planner的所有代码都是在PHP的Yii2框架中编写的。 如果您想了解有关Yii2的更多信息,请在Envato Tuts +上阅读我的平行系列“ 使用Yii2编程” 。
在阅读本文时,您可能可以开始在实时网站MeetingPlanner.io上尝试会议邀请了(请记住,仍有很多用户体验需要改进和完善)。 我确实参加了下面的评论主题,如果您有其他想法或想为以后的教程提出建议,我尤其感兴趣。 您也可以通过Twitter @reifman与我联系 。
会议计划者命令
命令的重要性
在设计过程中,我认为电子邮件中的命令既是会议计划过程的组成部分,又是导致实际事件发生的时间。
当参与者收到电子邮件邀请时,需要向他们提供安全的权限,使其可以查看会议页面,还需要响应特定的地点和时间是否适合他们。
会议结束后,我们可能会开始向那些发出专门命令的与会者发送提醒,例如“我快迟到了”,这些命令会向其他各方发短信给您您的困境,或者“要求更改位置”或“取消”。
所有这些命令都需要对收件人进行身份验证,并为他们提供对网站的安全访问,以便Meeting Planner可以正确处理其答复。 但是,如果该站点错误地将会议邀请电子邮件转发给另一方,然后单击该链接,该站点还需要保证不会释放该成员的整个联系人列表。 二级方可以轻松地登录到收件人的会议计划者帐户,并能够查看其所有会议和个人信息。
需要什么命令?
当我进一步考虑应用程序的愿景时,存在大量潜在的命令。 以下是初始邀请函中的一些内容(为了简化用户体验,我可能会对此进行简化):
- 查看会议
- 接受所有地点和时间
- 拒绝邀请
- 接受或拒绝特定地点
- 接受或拒绝特定的日期和时间
- 完成会议*
- 推荐其他地方*
- 建议其他日期和时间*
- 选择最终地点*
- 选择最终日期和时间*
- 添加或回复会议记录
- 查看会议范围内地点的位置图
- 查看您的电子邮件设置
- 阻止此组织者向您发送电子邮件
- 退订所有会议计划者电子邮件
注意:加星号(*)项的外观取决于组织者的会议设置。
计划好会议之后,还有各种后续命令:
- 重新安排会议
- 取消会议
- 显示地图
- 获取行车路线
- 要求更改时间
- 要求更改地点
- 通知各方您迟到了
建筑考虑
考虑到各种各样的命令,我觉得在一个控制器中对所有命令进行相同的身份验证和处理将很有用。
现在,我在MeetingController中创建了一个处理点,但是我希望以后再创建专用的CommandController。 我还考虑过在将来创建一个API访问控制器,并通过此单个安全入口点来传递应用程序的所有功能。 现在,我将推迟。
首先,我在Meeting.php模型中为每个命令指定了一个特定的常量定义:
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参数:
- Meeting_Id的
$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
:
<?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中提供所有这些参数的命令:
<?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; ?> <?php echo HTML::a(Yii::t('frontend','view map'),
MiscHelpers::buildCommand($meeting_id,Meeting::COMMAND_VIEW_MAP,$p->id,$user_id,$auth_key)); ?></span>
</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 echo HTML::a(Yii::t('frontend','reject'),MiscHelpers::buildCommand($meeting_id,Meeting::COMMAND_REJECT_PLACE,$p->id,$user_id,$auth_key)); ?>
<?php
if ($meetingSettings->participant_choose_place) { ?>
| <?php echo HTML::a(Yii::t('frontend','choose'),MiscHelpers::buildCommand($meeting_id,Meeting::COMMAND_CHOOSE_PLACE,$p->id,$user_id,$auth_key)); ?>
<?php
}
?>
</td>
</tr>
<?php
}
?>
您可以在下面看到它们的外观:
处理命令
然后,我构建了控制器功能来验证和处理命令。 这是第一部分:
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
身份登录,则将对其进行身份验证。 如果未通过身份验证,我们将运行身份验证检查:
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模型具有状态字段,这些状态字段指示用户是否已经注册自己,或者是否被动邀请他们参加会议。
现在,如果身份验证成功,我们可以处理以下每个命令:
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
。
两个示例命令
让我们看两个示例命令。 首先,让我们看一下建议另一个地方:
case Meeting::COMMAND_ADD_PLACE:
$this->redirect(['meeting-place/create','meeting_id'=>$id]);
break;
在这种情况下,该功能要求用户返回我们的网站以填写表格,供他们选择新的地点。 因此,我们只将它们重定向到该meeting_id
的创建会议场所页面。 并且它们已经通过身份验证并从上面登录。
这是一个示例-注意,面包屑菜单反映了会议的上下文,例如Breakfast Meeting :
其次,让我们看一下接受所有日期和时间:
case Meeting::COMMAND_ACCEPT_ALL_TIMES:
MeetingTimeChoice::setAll($id,$actor_id);
$this->redirect(['meeting/view','id'=>$id]);
break;
在这种情况下,我们需要接受该会议的所有时间和$actor_id
。 验收是在后台透明进行的。 之后,我们可以重定向他们以查看会议。
这是在接受所有接受的条件后进入会议视图的样子,例如okay , okay , okay,适用于以下地点和时间:
一个有趣的故事
实施所有这些命令肯定要花费一些时间,但是Meeting Planner的功能确实开始流行起来。 而且我能够向世界发送我的第一个邀请。
我曾经约会的一位女士知道我即将完成此功能,因此她决定激励我更快地完成此功能。 她说:
“我不知道什么时候下次见面,因为我还没有收到会议计划者邀请。”
经过几天的额外工作,我向她发送了第二次会议筹办者邀请,第一次邀请了一位朋友进行测试。
令人印象深刻的是,当我的约会对象收到她的邀请时,她Swift要求提供两个有用的功能。 首先,她说,除非活动在手机的Google日历中显示(通常我希望与iOS用户约会,而不是与Android用户约会),否则她不确定是否可以参加我们的约会。 下一个教程将讲述构建要导入的iCal(.ics)文件的故事(这样我的约会对象就会知道该去哪里)。 我不会让您感到悬念,我会在我们约会之前及时完成此功能。
其次,她要求提供一个我曾想过但尚未意识到其重要性的功能。 她希望能够指定一个有时间的地方。 换句话说,Canlis餐厅在星期五晚上7点,但Paseo在星期六晚上8点。 当前,地点和时间是分开提供的,而不是结合在一起提供的。 我将保存此功能以备将来使用。
这引发了一个普遍的问题,即在启动过程中您如何定期收集人们的反馈并将其集成到您的需求和开发计划中。 并非所有用户都会为您提供日期,以换取他们最喜欢的功能。 尽管缺乏辅助动力,但我计划安排一个教程集来讨论将来如何进行此操作。
下一步是什么?
在下一个情节中,我将详细介绍构建日历文件(.ics)并与邀请详细信息一起导入到Google Calendar,Outlook和Apple Calendar。 包括联系方式和地图以及管理时区问题都是这方面的关键方面。
在“ 用PHP构建您的启动”系列中观看即将出现的教程—希望您急于尝试使用Meeting Planner。 立即尝试!
请随时在下面添加您的问题和评论; 我尝试定期参加讨论。 您也可以通过Twitter @reifman与我联系 。
相关链接
翻译自: https://code.tutsplus.com/tutorials/building-your-startup-with-php-email-commands--cms-23288