您将要创造的
本教程是 Envato Tuts +上的“ 使用PHP构建启动”系列 的一部分。 在本系列文章中,我 将以 我的 Meeting Planner 应用程序作为真实示例 ,指导您完成从概念到现实的启动 。 在此过程的每一步中,我都会将Meeting Planner代码作为开放源代码示例发布,您可以从中学习。 我还将解决与启动相关的业务问题。
为什么要为您的启动构建API?
我目前向Meeting Planner添加API的主要原因是为构建iOS移动应用程序奠定基础。 该移动应用将使用该API来注册和登录用户,然后允许他们安排会议。
API还具有辅助作用,可以帮助您重新思考并更好地组织您迄今为止编写的所有代码。 会议计划程序代码中肯定有一些地方令人费解。 现在,我必须再次简化它们,以使移动应用程序能够复制上面的功能。
将来可能会有其他原因来构建API。 例如,也许我想让第三方开发人员扩展Meeting Planner安排的会议和事件的种类,从而允许他们在过程中收集和共享其他数据。
提醒一下,Meeting Planner的所有代码都是开源提供的,并使用PHP的Yii2框架编写。 这一集的重要部分描述了如何使用Yii框架来支持API。 如果您想了解有关Yii2的更多信息,请查看我的平行系列“ 使用Yii2编程” 。
在介绍API代码之前,我想鼓励您尝试安排您的第一次会议,以便您了解我在说什么。
如果您对本教程或应用程序本身有疑问,我会参与下面的讨论,您也可以在Twitter上与我联系@lookahead_io 。 我总是乐于接受Meeting Planner的新功能建议以及有关未来系列剧集的建议。
设计您的API
当我准备构建API时,需要理解各种概念。 我在《 使用Yii2编程:构建RESTful API(Envato Tuts +)》中解决了其中一些问题。
首先,我需要为API创建一个端点,移动应用程序的所有调用都将到达该端点。 我决定在Yii高级应用程序框架中使用独立的第三棵树,例如https://api.meetingplanner.io而不是https://meetingplanner.io/api/ 。 这样可以将API代码与其余服务完全分开。
其次,我需要在API中设计安全性。 在今天的教程中,我将演示我们正在使用的简单的Alpha安全性,但是随着时间的推移我们将对其进行增强,并且将来我可能会写更多有关此的内容。 在使用和传输API密钥和请求方面,安全性有一个方面,但是确保API实施应用程序安全协议也很重要。 例如,如果用户不是会议组织者或其参与者之一,则用户无法请求会议的参与者。
第三,我想准备用于版本控制的API代码。 例如,尚未更新的较旧的iOS应用程序可能会使用API v1.0,而更高版本的更新可能会调用API v2.0。 Yii提供了执行此操作的方法 ,但是我尚未在当前设计中实现它们。
第四,我想尽可能地符合REST标准。 我已经开始做这件事,但是需要更多的研究才能完全实施。
最后,到目前为止,我需要解决API将提供的功能的广泛性。 最初,在移动应用程序开发中,我专注于创建只读功能。 今天的教程和代码将主要集中于只读应用程序功能,即向我展示用户的会议。 但是它也包括用户注册。 在不久的将来,我们将添加更多写功能,例如创建会议,添加参与者,添加会议地点,完成邀请等。
因此,请考虑本教程,这是向我们的应用程序提供功能强大且已完成的服务API的第一步。
构建API
创建API服务树
Meeting Planner使用Yii Advanced Application框架 ,该框架包括用于应用程序的前端树和用于管理组件的后端树,我们将为API创建第三棵树。
我在使用Yii2编程:构建RESTful API(Envato Tuts +)中更早地描述了如何执行此操作。
首先,我复制了后端树和相关的环境设置:
$ cp -R backend api
$ cp -R environments/dev/backend/ environments/dev/api
$ cp -R environments/prod/backend/ environments/prod/api
然后将@api别名添加到/common/config/bootstrap.php:
<?php
Yii::setAlias('@common', dirname(__DIR__));
Yii::setAlias('@frontend', dirname(dirname(__DIR__)) . '/frontend');
Yii::setAlias('@backend', dirname(dirname(__DIR__)) . '/backend');
Yii::setAlias('@api', dirname(dirname(__DIR__)) . '/api');
Yii::setAlias('@console', dirname(dirname(__DIR__)) . '/console');
接下来,我们将开始构建核心功能。
保护API
在构建和测试iOS应用程序时,我已经创建了一些基本的安全性。 我将在将来使它变得更强大。
所有API调用都需要知道
app_id
和
app_secret
。 这些将以某种形式的HTTPS传输。 但是,不能保证我们可以保护它们,因此我们必须最终设计应用程序以抵抗发现的这些密钥。
目前,我已经将/ var / secure中的mp.ini文件扩展为包括以下内容:
...
sentry_key_public = "xxxxxxxx"
sentry_key_private = "xxxxxx"
sentry_id ="nnnnnn"
app_id = "xnxnxnxxnxnxn"
app_secret ="xnxnxnxnxnxnxnxnxnxnxnxnxnxnxnxnxn"
然后,我创建了一个Service.php模型来管理这些密钥的验证。 随着我们对此功能的增强,我只需要修改一段代码。
class Service extends Model
{
public static function verifyAccess($app_id,$app_secret) {
if ($app_id == Yii::$app->params['app_id']
&& $app_secret == Yii::$app->params['app_secret']) {
Yii::$app->params['site']['id']=SiteHelper::SITE_SP;
return true;
} else {
return false;
}
}
接下来,我在所有API控制器中设置一个
beforeAction
以重用上述方法:
public function beforeAction($action)
{
// your custom code here, if you want the code to run before action filters,
// which are triggered on the [[EVENT_BEFORE_ACTION]] event, e.g. PageCache or AccessControl
if (!parent::beforeAction($action)) {
return false;
}
if (Service::verifyAccess(Yii::$app->getRequest()->getQueryParam('app_id'),Yii::$app->getRequest()->getQueryParam('app_secret'))) {
return true;
} else {
echo 'your api keys are from the dark side';
Yii::$app->end();
}
}
此处的关键缺点是,每次调用都会传输安全密钥,并且未对查询参数进行签名。 通过HTTPS传输它们会有所帮助,但这并不完全安全。 以后会改善。
注册和登录
完全依赖API密钥的唯一两个API调用是注册和登录。 移动用户可以通过OAuth注册并向我们发送其OAuth服务令牌,也可以直接向我们提供电子邮件地址。
收到后,将为每个用户分配一个唯一的令牌,此令牌可保护该用户将来的API调用。
我还需要做更多的工作来提高安全性,但是今天也不会介绍。
这是通过API注册用户并创建令牌的初始代码:
public static function signupUser($email, $firstname='',$lastname='') {
$username = $fullname = $firstname.' '.$lastname;
if ($username == ' ') $username ='ios';
if (isset($username) && User::find()->where(['username' => $username])->exists()) {
$username = User::generateUniqueUsername($username,'ios');
}
$password = Yii::$app->security->generateRandomString(12);
$user = new User([
'username' => $username, // $attributes['login'],
'email' => $email,
'password' => $password,
'status' => User::STATUS_ACTIVE,
]);
$user->generateAuthKey();
$user->generatePasswordResetToken();
$transaction = $user->getDb()->beginTransaction();
if ($user->save()) {
$ut = new UserToken([
'user_id' => $user->id,
'token' => Yii::$app->security->generateRandomString(40),
]);
if ($ut->save()) {
User::completeInitialize($user->id);
UserProfile::applySocialNames($user->id,$firstname,$lastname,$fullname);
$transaction->commit();
return $user->id;
} else {
print_r($auth->getErrors());
}
} else {
$transaction->rollBack();
print_r($user->getErrors());
}
}
UserToken是一个独特的40位随机字符串,这使得猜测比相信美国会选择唐纳德·特朗普来领导他们更加困难。
$ut = new UserToken([
'user_id' => $user->id,
'token' => Yii::$app->security->generateRandomString(40),
]);
会议负责人
现在,让我们看一下API特定区域的调用,请求有关会议的信息。 这是/api/controllers/MeetingController.php的初始部分:
<?php
namespace api\controllers;
use Yii;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;
use yii\web\Response;
use api\models\MeetingAPI;
use api\models\Service;
class MeetingController extends Controller
{
public function behaviors()
{
return [
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['POST'],
],
],
];
}
public function beforeAction($action)
{
// your custom code here, if you want the code to run before action filters,
// which are triggered on the [[EVENT_BEFORE_ACTION]] event, e.g. PageCache or AccessControl
if (!parent::beforeAction($action)) {
return false;
}
if (Service::verifyAccess(Yii::$app->getRequest()->getQueryParam('app_id'),Yii::$app->getRequest()->getQueryParam('app_secret'))) {
return true;
} else {
echo 'your api keys are from the dark side';
Yii::$app->end();
}
}
请注意上面的内容,每个动作如何验证令牌是否正确。
然后,对Meetings的每个API调用都具有相同的结构,如下所示(建议我遵守纪律) :
public function actionList($app_id='', $app_secret='',$token='',$status=0) {
Yii::$app->response->format = Response::FORMAT_JSON;
return MeetingAPI::meetinglist($token,$status);
}
public function actionHistory($app_id='', $app_secret='',$token='',$meeting_id=0) {
Yii::$app->response->format = Response::FORMAT_JSON;
return MeetingAPI::history($token,$meeting_id);
}
public function actionMeetingplaces($app_id='', $app_secret='',$token='',$meeting_id=0) {
Yii::$app->response->format = Response::FORMAT_JSON;
return MeetingAPI::meetingplaces($token,$meeting_id);
}
public function actionMeetingtimes($app_id='', $app_secret='',$token='',$meeting_id=0) {
Yii::$app->response->format = Response::FORMAT_JSON;
return MeetingAPI::meetingtimes($token,$meeting_id);
}
public function actionMeetingplacechoices($app_id='', $app_secret='',$token='',$meeting_place_id=0) {
Yii::$app->response->format = Response::FORMAT_JSON;
return MeetingAPI::meetingplacechoices($token,$meeting_place_id);
}
public function actionMeetingtimechoices($app_id='', $app_secret='',$token='',$meeting_time_id=0) {
Yii::$app->response->format = Response::FORMAT_JSON;
return MeetingAPI::meetingtimechoices($token,$meeting_time_id);
}
public function actionNotes($app_id='', $app_secret='',$token='',$meeting_id=0) {
Yii::$app->response->format = Response::FORMAT_JSON;
return MeetingAPI::notes($token,$meeting_id);
}
public function actionSettings($app_id='', $app_secret='',$token='',$meeting_id=0) {
Yii::$app->response->format = Response::FORMAT_JSON;
return MeetingAPI::settings($token,$meeting_id);
}
public function actionCaption($app_id='', $app_secret='',$token='',$meeting_id=0) {
Yii::$app->response->format = Response::FORMAT_JSON;
return MeetingAPI::caption($token,$meeting_id);
}
public function actionDetails($app_id='', $app_secret='',$token='',$meeting_id=0) {
Yii::$app->response->format = Response::FORMAT_JSON;
return MeetingAPI::details($token,$meeting_id);
}
public function actionReminders($app_id='', $app_secret='',$token='')
{
Yii::$app->response->format = Response::FORMAT_JSON;
return MeetingAPI::reminders($token);
}
目前,每个调用都包含登录用户的
$app_id
,
$app_secret
和
$token
。 为了安全起见,我将在不久的将来对此进行更改。 它是安全的,但并非绝对安全。
让我们看一下
actionList
,该列表列出用户的会议,并通过
$status
参数对其进行过滤:
public function actionList($app_id='', $app_secret='',$token='',$status=0) {
Yii::$app->response->format = Response::FORMAT_JSON;
return MeetingAPI::meetinglist($token,$status);
}
最终,API可能会限制每种状态的会议请求的数量,即由该用户以计划模式向我显示最近的15个会议。
所有会议方法都内置在MeetingAPI模型中。 这是
meetinglist()
方法的代码:
<?php
namespace api\models;
use Yii;
use yii\base\Model;
use common\models\User;
use common\components\MiscHelpers;
use api\models\UserToken;
use frontend\models\Meeting;
use frontend\models\MeetingLog;
use frontend\models\MeetingPlace;
use frontend\models\MeetingTime;
use frontend\models\MeetingReminder;
use frontend\models\MeetingSetting;
use frontend\models\MeetingNote;
class MeetingAPI extends Model
{
public static function meetinglist($token,$status) {
$user_id = UserToken::lookup($token);
if (!$user_id) {
return Service::fail('invalid token');
}
if ($status == Meeting::STATUS_PLANNING || $status == Meeting::STATUS_SENT) {
$queryStatus =[Meeting::STATUS_PLANNING,Meeting::STATUS_SENT];
} else {
$queryStatus = $status;
}
// get calling user's timezone
$timezone = MiscHelpers::fetchUserTimezone($user_id);
$meeting_list = Meeting::find()
->joinWith('participants')
->where(['owner_id'=>$user_id])
->orWhere(['participant_id'=>$user_id])
->andWhere(['meeting.status'=>$queryStatus])
->distinct()
->orderBy(['created_at'=>SORT_DESC])
->all();
$meetings=[];
foreach ($meeting_list as $m) {
$x = new \stdClass();
$x->id = $m->id;
$x->owner_id= $m->owner_id;
$x->meeting_type = $m->meeting_type ;
$x->subject = $m->subject ;
$x->message = $m->message ;
$x->identifier = $m->identifier ;
$x->status = $m->status ;
$x->created_at = $m->created_at ;
$x->logged_at = $m->logged_at ;
$x->sequence_id = $m->sequence_id ;
$x->cleared_at = $m->cleared_at;
$x->site_id = $m->site_id ;
if ($status >= Meeting::STATUS_CONFIRMED) {
$x->chosenTime=Meeting::getChosenTime($m->id);
$x->caption = $m->friendlyDateFromTimestamp($x->chosenTime->start,$timezone,true,true).' '.$m->getMeetingParticipants();
$x->chosenPlace = Meeting::getChosenPlace($m->id);
if ($x->chosenPlace!==false) {
$x->place = $x->chosenPlace->place;
$x->gps = $x->chosenPlace->place->getLocation($x->chosenPlace->place->id);
$x->noPlace = false;
} else {
$x->place = false;
$x->noPlace = true;
$x->gps = false;
}
} else {
$x->chosenTime=0;
$x->chosenPlace = 0;
$x->caption = $m->getMeetingParticipants();
}
$meetings[]=$x;
unset($x);
}
return $meetings;
}
首先,该方法验证令牌是否属于用户:
$user_id = UserToken::lookup($token);
if (!$user_id) {
return Service::fail('invalid token');
}
这是
UserToken::lookup()
的代码:
public static function lookup($token) {
// lookup token for user_id
$ut = UserToken::find()
->where(['token'=>$token])
->one();
if (!is_null($ut)) {
return $ut->user_id;
} else {
return false;
}
}
然后,我们检查过滤器的
$status
并获取用户的
$timezone
:
if ($status == Meeting::STATUS_PLANNING || $status == Meeting::STATUS_SENT) {
$queryStatus =[Meeting::STATUS_PLANNING,Meeting::STATUS_SENT];
} else {
$queryStatus = $status;
}
// get calling user's timezone
$timezone = MiscHelpers::fetchUserTimezone($user_id);
最后,我们查询用户会议的列表并将其手动转置为对象数组:
$meeting_list = Meeting::find()
->joinWith('participants')
->where(['owner_id'=>$user_id])
->orWhere(['participant_id'=>$user_id])
->andWhere(['meeting.status'=>$queryStatus])
->distinct()
->orderBy(['created_at'=>SORT_DESC])
->all();
$meetings=[];
foreach ($meeting_list as $m) {
$x = new \stdClass();
$x->id = $m->id;
$x->owner_id= $m->owner_id;
$x->meeting_type = $m->meeting_type ;
$x->subject = $m->subject ;
$x->message = $m->message ;
$x->identifier = $m->identifier ;
$x->status = $m->status ;
$x->created_at = $m->created_at ;
$x->logged_at = $m->logged_at ;
$x->sequence_id = $m->sequence_id ;
$x->cleared_at = $m->cleared_at;
$x->site_id = $m->site_id ;
if ($status >= Meeting::STATUS_CONFIRMED) {
$x->chosenTime=Meeting::getChosenTime($m->id);
$x->caption = $m->friendlyDateFromTimestamp($x->chosenTime->start,$timezone,true,true).' '.$m->getMeetingParticipants();
$x->chosenPlace = Meeting::getChosenPlace($m->id);
if ($x->chosenPlace!==false) {
$x->place = $x->chosenPlace->place;
$x->gps = $x->chosenPlace->place->getLocation($x->chosenPlace->place->id);
$x->noPlace = false;
} else {
$x->place = false;
$x->noPlace = true;
$x->gps = false;
}
} else {
$x->chosenTime=0;
$x->chosenPlace = 0;
$x->caption = $m->getMeetingParticipants();
}
$meetings[]=$x;
unset($x);
}
return $meetings;
虽然可能有一种更简单的方法来映射数据库结果以返回API,但手动转置最复杂的表Meeting可以让我控制API结果为程序员提供了什么。 实际上,对我来说,这是对原始代码和数据库属性进行改进和简化的API的机会。
例如,有一些代码,Meeting Planner必须在用户界面中生成未存储在数据库中的子标题。 无需要求iOS应用程序复制此复杂代码,我们只需生成子标题并将其返回到API结果中即可。
进行API调用
这是进行和测试API调用的初步方法。 例如,如果我进行以下URL调用:
http://apix.meetingplanner.io/meeting/list/?app_id=xxx&app_secret=xxxxx&token=yyyy
那可行。 但是,为了进行测试并查看其运行效果,我使用了Postman ( Chrome应用扩展程序) ,该功能非常有用。
使用Postman UX构建API调用的方法如下:
结果是这样的:
这只是浏览显示我所有会议的开发服务器原始结果的简便方法:
[
{
"id": 207,
"owner_id": 1,
"meeting_type": 0,
"subject": "New Mtg to Test",
"message": "",
"identifier": "dAefqLGi",
"status": 20,
"created_at": 1475285105,
"logged_at": 1476642100,
"sequence_id": "0",
"cleared_at": 1475780470,
"site_id": 0,
"chosenTime": 0,
"chosenPlace": 0,
"caption": "with Jeff Reifman and [email protected]"
},
{
"id": 206,
"owner_id": 1,
"meeting_type": 150,
"subject": "Ignore - just testing",
"message": "",
"identifier": "ITJpSmlo",
"status": 20,
"created_at": 1474706654,
"logged_at": 1474706702,
"sequence_id": "0",
"cleared_at": 1474706732,
"site_id": 0,
"chosenTime": 0,
"chosenPlace": 0,
"caption": "with Jeff Reifman and [email protected]"
},
{
"id": 205,
"owner_id": 1,
"meeting_type": 110,
"subject": "Our Upcoming Meeting Test",
"message": "",
"identifier": "vkVPWVmH",
"status": 20,
"created_at": 1474677013,
"logged_at": 1474921968,
"sequence_id": "0",
"cleared_at": 1474920744,
"site_id": 0,
"chosenTime": 0,
"chosenPlace": 0,
"caption": "with Jeff Reifman and [email protected]"
},
...
现在就这样。 您可以在API树中浏览发行版,并查看许多其他方法。 当我升级安全性并改善API的功能时,我将尝试写更多有关它的内容。
展望未来
希望您喜欢今天的教程。 显然,随着我们的移动开发的发展,API将不断发展和变化。 如前所述,我将增强安全性并扩展功能。
再次,如果您还没有, 请立即安排与Meeting Planner的第一次会议!
您也可以与我联系@lookahead_io 。 我总是乐于接受新功能的想法和主题建议,以供将来的教程使用。 或尝试我们的服务台并打开错误报告或功能请求通知单。
请阅读“ 用PHP构建您的启动”系列,继续关注所有这些以及以后的教程。
相关链接
- 简单计划者和会议计划者
- 关注会议计划者的资金概况
- 使用Yii2系列进行编程(Envato Tuts +)
- 使用Yii2编程:构建RESTful API(Envato Tuts +)
- Chrome的 Postman和Postman扩展
翻译自: https://code.tutsplus.com/tutorials/building-your-startup-designing-a-restful-api--cms-27682