Pull to refresh

Создание «API-Centric» Web Application на PHP

Reading time 13 min
Views 10K
Что такое “API-Centric” Web Application?
Это веб приложение которое большая часть функционала реализуется через API. Например: если вы авторизовываетесь, то вы отправляете свои данные через функции API, а API уже возвращает результат success или же ошибку. Другой характеристикой API является то что API не зависит от состояния пользователя.

Зачем это нужно ?
Как веб разработчики мы видим, что технологии развиваются каждый день. И все знают, что люди используют не только браузеры на pc, но и android, apple, windows phone и прочие гаджеты для доступа в интернет к своим любимым сайтам.

Так причём же тут API ?
Одним из преимуществ создания api-centric приложения это помощь в построении функциональности, которая может быть использована на любом другом девайсе, будь это браузер, мобильник, планшет или даже десктопное приложение. Все, что нужно сделать, это создать API, таким образом, чтобы все эти устройства могли взаимодействовать с ним.



Создавая приложения таким способом, мы можем воспользоваться им в различных средах и различными людьми. В этой статье мы создадим простое TODO приложение и клиентскую часть взаимодействующую с серверной. let’s begin!

Шаг 1. Планирование функциональности
В todo приложении которое мы будем сейчас делать мы реализуем crud операции.
  • Create TODO items
  • Read TODO items
  • Update TODO items
  • Delete TODO items

Каждый TODO item будет иметь:
  • Заголовок
  • Дата окончания
  • Описание

Флажок, показывающий, что TODO item выполнено
Давайте создадим макет нашего приложения



Шаг 2. Создание API сервера

Создадим 2 проекта: API сервер (back end) и клиентскую часть (front end)
Создайте папку simpletodo_api и в ней создайте index.php файл. Он будет являться главным контроллером нашего API (front controller) так что все запросы к серверу API будет осуществляться через этот файл. Откройте его и вставьте следующий код:
<?php
// Define path to data folder
define('DATA_PATH', realpath(dirname(__FILE__).'/data'));
 
//include our models
include_once 'models/TodoItem.php';
 
//wrap the whole thing in a try-catch block to catch any wayward exceptions!
try {
   //get all of the parameters in the POST/GET request
   $params = $_REQUEST;
 
   //get the controller and format it correctly so the first
   //letter is always capitalized
   $controller = ucfirst(strtolower($params['controller']));
 
   //get the action and format it correctly so all the
   //letters are not capitalized, and append 'Action'
   $action = strtolower($params['action']).'Action';
 
   //check if the controller exists. if not, throw an exception
   if( file_exists("controllers/{$controller}.php") ) {
      include_once "controllers/{$controller}.php";
   } else {
      throw new Exception('Controller is invalid.');
   }
 
   //create a new instance of the controller, and pass
   //it the parameters from the request
   $controller = new $controller($params);
 
   //check if the action exists in the controller. if not, throw an exception.
   if( method_exists($controller, $action) === false ) {
      throw new Exception('Action is invalid.');
   }
 
   //execute the action
   $result['data'] = $controller->$action();
   $result['success'] = true;
 
} catch( Exception $e ) {
   //catch any exceptions and report the problem
   $result = array();
   $result['success'] = false;
   $result['errormsg'] = $e->getMessage();
}
 
//echo the result of the API call
echo json_encode($result);
exit();

Мы по существу построили простой front controller который:
  • Принимает вызовы API с любым числом параметров
  • Извлекает Controller и Action для вызова API
  • Делает необходимую проверку на существование контроллера и действия
  • Выполняет вызов API
  • Отлавливает ошибки
  • Отправляет результат

Так же создайте папки controllers, models и data.
Папка controllers будет содержать все контроллеры используемые API сервером. Мы построим mvc архитектуру, чтобы сделать наш api server чистым и гибким.
Папка models будет содержать все модели api server’a
Папка data будет хранить все данные
Зайдём в папку контроллеров и создадим todo.php
Это будет наш контроллер для других TODO списка связанных задач. Создаем заготовку класса и функционал для нашего приложения:
<?php
class Todo
{
   private $_params;
 
   public function __construct($params)
   {
      $this->_params = $params;
   }
 
   public function createAction()
   {
      //create a new todo item
   }
 
   public function readAction()
   {
      //read all the todo items
   }
 
   public function updateAction()
   {
      //update a todo item
   }
 
   public function deleteAction()
   {
      //delete a todo item
   }
}

Теперь добавим необходимый функционал действию action. Мы создаём метод createAсtion. Если вы в хорошем настроении то можете создать и для других действий функционал, я лишь предоставил интерфейс.
public function createAction()
{
   //create a new todo item
   $todo = new TodoItem();
   $todo->title = $this->_params['title'];
   $todo->description = $this->_params['description'];
   $todo->due_date = $this->_params['due_date'];
   $todo->is_done = 'false';
 
   //pass the user's username and password to authenticate the user
   $todo->save($this->_params['username'], $this->_params['userpass']);
 
   //return the todo item in array format
   return $todo->toArray();
}

Создаём todoItem.php в моделях, чтобы мы могли создать код, реализующий «Создание пункта списка». Кстати говоря в данном примере – без БД (на файлах). Вы, конечно же можете не ограничиваться.
<?php
class TodoItem
{
   public $todo_id;
   public $title;
   public $description;
   public $due_date;
   public $is_done;
 
   public function save($username, $userpass)
   {
      //get the username/password hash
      $userhash = sha1("{$username}_{$userpass}");
      if( is_dir(DATA_PATH."/{$userhash}") === false ) {
         mkdir(DATA_PATH."/{$userhash}");
      }
 
      //if the $todo_id isn't set yet, it means we need to create a new todo item
      if( is_null($this->todo_id) || !is_numeric($this->todo_id) ) {
         //the todo id is the current time
         $this->todo_id = time();
      }
 
      //get the array version of this todo item
      $todo_item_array = $this->toArray();
 
      //save the serialized array version into a file
      $success = file_put_contents(DATA_PATH."/{$userhash}/{$this->todo_id}.txt", serialize($todo_item_array));
 
      //if saving was not successful, throw an exception
      if( $success === false ) {
         throw new Exception('Failed to save todo item');
      }
 
      //return the array version
      return $todo_item_array;
   }
 
   public function toArray()
   {
      //return an array version of the todo item
      return array(
         'todo_id' => $this->todo_id,
         'title' => $this->title,
         'description' => $this->description,
         'due_date' => $this->due_date,
         'is_done' => $this->is_done
      );
   }
}

Метод createAction вызывает 2 функции модели todoItem:
  • Save() – сохраняет todoItem в файл, а также при необходимости создает todo_id для todoItem.
  • toArray() – возвращает todoItem в виде массива, индексами которых служат переменные

localhost/simpletodo_api/?controller=todo&action=create&title=test%20title&description=test%20description&due_date=12/08/2011&username=nikko&userpass=test1234 Если всё работает, вы должны увидеть в папке data новую папку, в которой будет текстовый документ такого содержания:


Congratulations! Мы успешно создали API server и сделали вызов API.

Шаг 3. Защищаем API server с app id и app secret.
Сейчас наш api server принимает все запросы. Мы должны ограничить доступ к серверу, чтобы гарантировать, что только наши собственные клиенты имеют возможность сделать API запросы. Так же вы можете создать систему в которой пользователи могут создавать свои собственные приложения, которые имеют доступ к API серверу, подобно тому, как работает Facebook и Twtter приложения.
Начнём с создание пары id-key для клиентов, которые буду использовать наш сервер. Так как это демо, мы можем использовать рандомную 32 символьную строку. Для APP ID, скажем ‘app001'.
Откройте index.php и измените его:
<?php
// Define path to data folder
define('DATA_PATH', realpath(dirname(__FILE__).'/data'));
 
//Define our id-key pairs
$applications = array(
   'APP001' => '28e336ac6c9423d946ba02d19c6a2632', //randomly generated app key
);
//include our models
include_once 'models/TodoItem.php';
 
//wrap the whole thing in a try-catch block to catch any wayward exceptions!
try {
   //*UPDATED*
   //get the encrypted request
   $enc_request = $_REQUEST['enc_request'];
 
   //get the provided app id
   $app_id = $_REQUEST['app_id'];
 
   //check first if the app id exists in the list of applications
   if( !isset($applications[$app_id]) ) {
      throw new Exception('Application does not exist!');
   }
 
   //decrypt the request
   $params = json_decode(trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $applications[$app_id], base64_decode($enc_request), MCRYPT_MODE_ECB)));
 
   //check if the request is valid by checking if it's an array and looking for the controller and action
   if( $params == false || isset($params->controller) == false || isset($params->action) == false ) {
      throw new Exception('Request is not valid');
   }
 
   //cast it into an array
   $params = (array) $params;
   ...
   ...
   ...

Мы сделали простую реализацию очень простого способа проверки подлинности пользователей, используя аутентификацию public-private key. Вот как это выглядит:

  • Делается вызов api, в нем предоставляется $ APP_ID и $ enc_request
  • $ enc_request содержит параметры вызова API, зашифрованные с помощью APP KEY. APP KEY никогда не отправляется на сервер, он используется только для шифрования запроса. Кстати, запрос может быть расшифрован с помощью APP KEY .
  • Когда вызов доходит до сервера, то сервер проверяет свой собственный список приложений на наличие APP ID.
  • Когда он его находит, api server пытается расшифровать запрос используя ключ.
  • Если расшифровка произошла успешно, продолжается выполнение программы

Шаг 4. Создание front end
Давайте создадим новую директорию simpletodo_client_browser.
И index.php:
<!DOCTYPE html>
<html>
<head>
   <title>SimpleTODO</title>
   <link rel="stylesheet" href="css/reset.css" type="text/css" />
   <link rel="stylesheet" href="css/bootstrap.min.css" type="text/css" />
   <script src="js/jquery.min.js"></script>
   <script src="js/jquery-ui-1.8.16.custom.min.js"></script>
 
   <style>
   body {
      padding-top: 40px;
   }
   #main {
      margin-top: 80px;
      text-align: center;
   }
   </style>
</head>
<body>
   <div class="topbar">
      <div class="fill">
         <div class="container">
            <a class="brand" href="index.php">SimpleTODO</a>
         </div>
      </div>
   </div>
   <div id="main" class="container">
      <form class="form-stacked" method="POST" action="login.php">
         <div class="row">
            <div class="span5 offset5">
               <label for="login_username">Username:</label>
               <input type="text" id="login_username" name="login_username" placeholder="username" />
 
               <label for="login_password">Password:</label>
               <input type="password" id="login_password" name="login_password" placeholder="password" />
 
            </div>
         </div>
         <div class="actions">
            <button type="submit" name="login_submit" class="btn primary large">Login or Register</button>
         </div>
      </form>
   </div>
</body>
</html>

Это должно выглядеть так:

Кстати говоря мы включили 2 js и 1 css файлов
  • bootstrap.min.css is the Twitter Bootstrap
  • jquery.min.js is последняя jQuery библиотека
  • jquery-ui-1.8.16.custom.min.js последняя jQuery UI библиотека

Далее создадим login.php для сохранения логина и пароля внутри сессии:
<?php
//get the form values
$username = $_POST['login_username'];
$userpass = $_POST['login_password'];
 
session_start();
$_SESSION['username'] = $username;
$_SESSION['userpass'] = $userpass;
header('Location: todo.php');
exit();

Здесь мы просто создаём новую сессию пользователя, основанную на введённом логине и пароле. Это действие является простой комбинацией ключа, по которому мы можем получить доступ к хранящимся TODO спискам. Далее перенаправление на todo.php для начала взаимодействия с api server’ом. Прежде чем создать todo.php давайте сначала определим класс ApiCaller, который инкапсулирует все методы вызова API, которые нам понадобится, включая шифрование запросов.
Создаём apicaller.php:
<?php
class ApiCaller
{
   //some variables for the object
   private $_app_id;
   private $_app_key;
   private $_api_url;
 
   //construct an ApiCaller object, taking an
   //APP ID, APP KEY and API URL parameter
   public function __construct($app_id, $app_key, $api_url)
   {
      $this->_app_id = $app_id;
      $this->_app_key = $app_key;
      $this->_api_url = $api_url;
   }
 
   //send the request to the API server
   //also encrypts the request, then checks
   //if the results are valid
   public function sendRequest($request_params)
   {
      //encrypt the request parameters
      $enc_request = base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $this->_app_key, json_encode($request_params), MCRYPT_MODE_ECB));
 
      //create the params array, which will
      //be the POST parameters
      $params = array();
      $params['enc_request'] = $enc_request;
      $params['app_id'] = $this->_app_id;
 
      //initialize and setup the curl handler
      $ch = curl_init();
      curl_setopt($ch, CURLOPT_URL, $this->_api_url);
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
      curl_setopt($ch, CURLOPT_POST, count($params));
      curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
 
      //execute the request
      $result = curl_exec($ch);
 
      //json_decode the result
      $result = @json_decode($result);
 
      //check if we're able to json_decode the result correctly
      if( $result == false || isset($result['success']) == false ) {
         throw new Exception('Request was not correct');
      }
 
      //if there was an error in the request, throw an exception
      if( $result['success'] == false ) {
         throw new Exception($result['errormsg']);
      }
 
      //if everything went great, return the data
      return $result['data'];
   }
}

Мы будем использовать класс ApiCaller для отправки запросов к нашему API серверу. Таким образом, все необходимое шифрование и инициализация CURL будет в одном месте, а так же не будет повторения кода.
Конструктор содержит 3 параметра:
  • $ app_id — APP ID для клиента (APP001)
  • $ app_key — APP KEY для клиента (28e336ac6c9423d946ba02d19c6a2632)
  • $ api_url — URL сервера API, localhost/simpletodo_api/

А функция sendRequest():
  • шифрует параметры запроса, используя библиотеку mcrypt в том же порядке, что сервер API расшифровывает его
  • генерирует $_POST параметры, которые будут отправлены на api server
  • выполняет вызов API через CURL
  • проверяет результат вызова (успешен или нет)
  • возвращает данные, когда всё было успешно отправлено.

Сейчас, давайте создадим todo.php. А именно создадим код который возвратит list of todo items для юзера anton и пароля habr_hello.
<?php
session_start();
include_once 'apicaller.php';
 
$apicaller = new ApiCaller('APP001', '28e336ac6c9423d946ba02d19c6a2632', 'http://localhost/simpletodo_api/');
 
$todo_items = $apicaller->sendRequest(array(
   'controller' => 'todo',
   'action' => 'read',
   'username' => $_SESSION['username'],
   'userpass' => $_SESSION['userpass']
));
 
echo '';
var_dump($todo_items);

Заходим на главную и логинимся под anton и habr_hello и вы должны увидеть результат var_dump’a. Поздравляю! Вы успешно выполнили api запрос к api server’y. В этом коде мы:
  • создали сессию
  • создали экземпляр класса ApiCaller, передавая app id, app key в todo.php.


Давайте изменим нашу страничку добавив некоторых возможностей.
Кстати не забудьте удалить вар дамп ^_^.
<!DOCTYPE html>
<html>
<head>
   <title>SimpleTODO</title>
 
   <link rel="stylesheet" href="css/reset.css" type="text/css" />
   <link rel="stylesheet" href="css/bootstrap.min.css" type="text/css" />
   <link rel="stylesheet" href="css/flick/jquery-ui-1.8.16.custom.css" type="text/css" />
 
   <script src="js/jquery.min.js"></script>
   <script src="js/jquery-ui-1.8.16.custom.min.js"></script>
 
   <style>
   body {
      padding-top: 40px;
   }
   #main {
      margin-top: 80px;
   }
 
   .textalignright {
      text-align: right;
   }
 
   .marginbottom10 {
      margin-bottom: 10px;
   }
   #newtodo_window {
      text-align: left;
      display: none;
   }
   </style>
 
   <script>
   $(document).ready(function() {
      $("#todolist").accordion({
         collapsible: true
      });
      $(".datepicker").datepicker();
      $('#newtodo_window').dialog({
         autoOpen: false,
         height: 'auto',
         width: 'auto',
         modal: true
      });
      $('#newtodo').click(function() {
         $('#newtodo_window').dialog('open');
      });
   });
   </script>
</head>
<body>
   <div class="topbar">
      <div class="fill">
         <div class="container">
            <a class="brand" href="index.php">SimpleTODO</a>
         </div>
      </div>
   </div>
   <div id="main" class="container">
      <div class="textalignright marginbottom10">
         <span id="newtodo" class="btn info">Create a new TODO item</span>
         <div id="newtodo_window" title="Create a new TODO item">
            <form method="POST" action="new_todo.php">
               <p>Title:<br /><input type="text" class="title" name="title" placeholder="TODO title" /></p>
               <p>Date Due:<br /><input type="text" class="datepicker" name="due_date" placeholder="MM/DD/YYYY" /></p>
               <p>Description:<br /><textarea class="description" name="description"></textarea></p>
               <div class="actions">
                  <input type="submit" value="Create" name="new_submit" class="btn primary" />
               </div>
            </form>
         </div>
      </div>
      <div id="todolist">
         <?php foreach($todo_items as $todo): ?>
         <h3><a href="#"><?php echo $todo->title; ?></a></h3>
         <div>
            <form method="POST" action="update_todo.php">
            <div class="textalignright">
               <a href="delete_todo.php?todo_id=<?php echo $todo->todo_id; ?>">Delete</a>
            </div>
            <div>
               <p>Date Due:<br /><input type="text" id="datepicker_<?php echo $todo->todo_id; ?>" class="datepicker" name="due_date" value="12/09/2011" /></p>
               <p>Description:<br /><textarea class="span8" id="description_<?php echo $todo->todo_id; ?>" class="description" name="description"><?php echo $todo->description; ?></textarea></p>
            </div>
            <div class="textalignright">
               <?php if( $todo->is_done == 'false' ): ?>
               <input type="hidden" value="false" name="is_done" />
               <input type="submit" class="btn" value="Mark as Done?" name="markasdone_button" />
               <?php else: ?>
               <input type="hidden" value="true" name="is_done" />
               <input type="button" class="btn success" value="Done!" name="done_button" />
               <?php endif; ?>
               <input type="hidden" value="<?php echo $todo->todo_id; ?>" name="todo_id" />
               <input type="hidden" value="<?php echo $todo->title; ?>" name="title" />
               <input type="submit" class="btn primary" value="Save Changes" name="update_button" />
            </div>
            </form>
         </div>
         <?php endforeach; ?>
      </div>
   </div>
</body>
</html>

Это должно выглядеть так:

Приятно смотрится, неправда ли? Добавим теперь той самой функциональности, new_todo.php
Который содержит вызов todo/create, создавая новый TODO item элемент. Создание других страниц: (update_todo.php и delete_todo.php) должно быть простым.
Откроем new_todo.php:
<?php
session_start();
include_once 'apicaller.php';
 
$apicaller = new ApiCaller('APP001', '28e336ac6c9423d946ba02d19c6a2632', 'http://localhost/simpletodo_api/');
 
$new_item = $apicaller->sendRequest(array(
   'controller' => 'todo',
   'action' => 'create',
   'title' => $_POST['title'],
   'due_date' => $_POST['due_date'],
   'description' => $_POST['description'],
   'username' => $_SESSION['username'],
   'userpass' => $_SESSION['userpass']
));
header('Location: todo.php');
exit();
?>

Как вы видете new_todo.php использует ApiCaller для отправки todo/create запроса к API server’y.
  • стартует сессия
  • инициализация ApiCaller класса
  • отправка запроса с помощью sendRequest()
  • редирект на todo.php

Поздравляю! Мы успешно создали API-centric web application.

Заключение:
Есть много преимуществ в разработке приложений, сделанных с API. Если вы хотите создать Android версию SimpleTODO, то вся функциональность, которая может вам потребоваться, уже есть в API server, так что все, что вам нужно сделать, это просто создать Android клиент! Хотите, реорганизовать или оптимизировать некоторые классы? Нет проблем — просто убедитесь, что выходные данные будут теми же. Нужно добавить больше функциональности? Вы можете сделать это без изменения кода клиента!

Вдохновением служила статья nettuts
Пишите если где ошибся, и комментарии по поводу где что лучше можно было бы сделать
Tags:
Hubs:
+31
Comments 31
Comments Comments 31

Articles