天天看点

建立您的启动公司:设计RESTful API

建立您的启动公司:设计RESTful API

您将要创造的

本教程是 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服务树

建立您的启动公司:设计RESTful 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调用的方法如下:

建立您的启动公司:设计RESTful API

结果是这样的:

建立您的启动公司:设计RESTful 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