Статьи

Создание вашего стартапа: разработка RESTful API

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

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

Основная причина, по которой я сейчас добавляю API в Meeting Planner, — это создание основы для создания мобильного приложения для iOS. Мобильное приложение будет использовать API для регистрации и входа пользователей, а затем позволит им планировать встречи.

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

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

Напоминаем, что весь код для Meeting Planner предоставляется с открытым исходным кодом и написан на Yii2 Framework для PHP. Значительная часть этого эпизода описывает, как использовать Yii Framework для поддержки API. Если вы хотите узнать больше о Yii2, ознакомьтесь с моей параллельной серией Программирование с Yii2 .

Прежде чем я углублюсь в код API, я хотел бы предложить вам попробовать запланировать вашу первую встречу, чтобы вы знали, о чем я говорю.

Если у вас есть вопросы об этом руководстве или о самом приложении, я участвую в обсуждениях ниже, и вы также можете связаться со мной по адресу @lookahead_io в Twitter. Я всегда открыт для новых идей для планировщика собраний, а также предложений для будущих серий.

Когда я готовился к созданию API, мне нужно было понять различные концепции. Я рассмотрел некоторые из них в разделе «Программирование на Yii2: создание RESTful API (Envato Tuts +)» .

Во-первых, мне нужно было создать конечную точку для API, куда будут поступать все вызовы из мобильных приложений. Я решил использовать независимое третье дерево в среде расширенного приложения Yii , например, https://api.meetingplanner.io вместо https://meetingplanner.io/api/ . Это позволяет четко отделить код API от остальной части сервиса.

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

В-третьих, я хотел бы подготовить код API для управления версиями. Например, старое приложение iOS, которое не было обновлено, могло бы использовать API v1.0, в то время как более позднее обновление могло бы вызвать API v2.0. Yii предоставляет методы для этого , но я еще не реализовал их в текущем проекте.

В-четвертых, я хотел максимально соответствовать стандартам REST. Это то, что я начал делать, но для полной реализации потребуется больше исследований.

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

Поэтому рассмотрим это руководство как первый шаг к созданию надежного API завершенных сервисов для нашего приложения.

Создание вашего стартапа - дерево API

Meeting Planner использует платформу Yii Advanced Application , которая включает в себя интерфейсное дерево для приложения и фоновое дерево для административного компонента, и мы создадим третье дерево для API.

Я описал, как это сделать ранее, в Программировании на Yii2: Создание RESTful API (Envato Tuts +) .

Во-первых, я продублировал внутреннее дерево и настройки соответствующей среды:

1
2
3
$ 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:

1
2
3
4
5
6
<?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’);

Далее мы начнем строить основные функции.

Я создал некоторую базовую защиту, когда мы создаем и тестируем приложение для iOS. Я сделаю это более надежным в будущем.

Все вызовы API должны знать app_id и app_secret . Они будут передаваться в некоторой форме HTTPS. Однако нет никакой гарантии, что мы сможем защитить их, поэтому мы должны в конечном итоге разработать приложение, устойчивое к обнаружению этих ключей.

На данный момент я расширил файл mp.ini в / var / secure, добавив в него:

1
2
3
4
5
6
sentry_key_public = «xxxxxxxx»
sentry_key_private = «xxxxxx»
sentry_id =»nnnnnn»
app_id = «xnxnxnxxnxnxn»
app_secret =»xnxnxnxnxnxnxnxnxnxnxnxnxnxnxnxnxn»

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

01
02
03
04
05
06
07
08
09
10
11
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;
        }
      }

Затем я установил beforeAction во всех контроллерах API, чтобы использовать вышеупомянутый метод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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, eg 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 и создания токена:

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
public static function signupUser($email, $firstname=»,$lastname=») {
     $username = $fullname = $firstname.’
     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 цифр, что делает ее еще более трудной для догадки, чем верить, что Америка выберет Дональда Трампа, чтобы возглавить их.

1
2
3
4
$ut = new UserToken([
   ‘user_id’ => $user->id,
   ‘token’ => Yii::$app->security->generateRandomString(40),
   ]);

Теперь давайте посмотрим на вызовы для определенной области API, запрашивающие информацию о собраниях. Вот начальная часть /api/controllers/MeetingController.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
<?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, eg 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();
      }
    }

Обратите внимание, как каждое действие проверяет, что токены верны.

Затем каждый вызов API для Meetings идентично структурирован, как показано ниже (одобряю мою попытку дисциплины) :

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
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 для их фильтрации:

1
2
3
4
public function actionList($app_id=», $app_secret=»,$token=»,$status=0) {
  Yii::$app->response->format = Response::FORMAT_JSON;
  return MeetingAPI::meetinglist($token,$status);
}

В конце концов, API может ограничить количество запросов на собрания для каждого статуса, т.е. показать мне последние 15 встреч в режиме планирования этим пользователем.

Все методы Meeting встроены в модель MeetingAPI. Вот код для meetinglist() :

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
<?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).’
          $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;
    }

Во-первых, метод проверяет токен как принадлежащий пользователю:

1
2
3
4
$user_id = UserToken::lookup($token);
     if (!$user_id) {
       return Service::fail(‘invalid token’);
     }

Вот код для UserToken::lookup() :

01
02
03
04
05
06
07
08
09
10
11
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 :

1
2
3
4
5
6
7
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);

И, наконец, мы запрашиваем список встреч пользователя и переносим их вручную в массив объектов:

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
$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).’
         $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. Например, если я сделаю следующий URL-вызов:

1
http://apix.meetingplanner.io/meeting/list/?app_id=xxx&app_secret=xxxxx&token=yyyy

Это будет работать. Но для тестирования и для того, чтобы увидеть его в действии, я использовал Postman , расширение для Chrome , которое чрезвычайно полезно.

Вот как вы можете создать вызов API с помощью Postman UX:

Создание вашего стартапа - Почтальон API запросов

И вот как выглядят результаты:

Создание вашего стартапа - результаты Postman API

Это простой способ просмотреть исходный результат моего сервера разработки, показывая все мои встречи:

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
[
 {
   «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» .