RestAPI для веб-приложения на PHP или познаем дзен в чистоте

от автора

В нынешней разработке все стремятся к чистоте. Чистый код и прозрачный для любого Джуниора паттерн – безусловно залог успешного долгоиграющего проекта, который еще не скоро соберутся переписывать.

В данной статье расскажу, как в течении нескольких лет я пришел к, в моем видении, идеальному решению для реализации Restfull API сервиса на PHP. Конечно, я в курсе, что существует бесчисленное множество фреймворков, которые позволяют за пару минут развернуть своё API. Но, меня всегда одолевали сомнения на их счет. Лично я – никогда не любил использовать чужой код. Сначала: из-за того, что не было уверенности 100% понимания всех происходящих процессов. Позднее: из за сомнений в том, что данный фреймворк – лучшее, что может быть написано для моего проекта.

Если Вы – ярый сторонник фреймворков и не понимаете мой выбор: нашел статью на хабре затрагивающую эту тему.

Итак, если Вы еще не определились какой подход использовать в реализации RestAPI для Вашего сервиса или просто хотите сравнить свой подход с моим – давайте смотреть код!

Безусловно, перед началом Вам стоит настроить Ваш веб-сервер, что-бы все запросы летели в index.php, если Вы не знаете, как это сделать – под спойлером привожу пример настроек для Nginx.

location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

location /css/ {}
location / {
rewrite ^ /index.php last;
}

Начнем, конечно, с index.php:
В котором, как ни странно для MVC паттерна, практически ничего нет.

Для вызова API нам потребуется всего несколько строк:

$action = explode('/', $_SERVER['REQUEST_URI']); if(!isset($action[1]))     $page = NULL; else     $page = $action[1];  if($page == 'api') {     include_once 'api/main/api.class.php';     new api; } else {     //тут можете подгрузить свой клиентский код заинклюдив, например, index.html  } 

Таким образом, теперь, любые обращения на URL sitename.com/api/что_то_там_еще будут способствовать только вызову класса API и ничего лишнего.

Далее, для понимания всей картины, давайте окинем взглядом архитектуру будущего приложения:
image

Скриншот иерархии предоставлен прямо с боевого проекта, а в статье мы затронем лишь папку api и файл index.php.

Файлы api по порядку:

api.class.php
У нас перед глазами основной файл, в котором кроется вся суть моего подхода: базовый класс играет роль роутера – он будет определять обращение к конкретному методу API, подготавливать все данные и осуществлять вызов названного метода.

include_once 'model.class.php'; include_once 'view.class.php'; include_once 'factory.class.php';  class api {     private $db;     private $view;     private $factory;     private $args = array();     private $userID;      function __construct() {          $this->db = new db();$this->view = new view();$this->factory = new factory();         $url = parse_url($_SERVER['REQUEST_URI']);         $action = explode('/', $url['path']);         $action = end($action);          if (!empty($action)) {              if (!empty($_POST) || (substr($action, 0, 4) == 'get_')) {                  if (file_exists('api/methods/' . $action . '/controller.php')) {                      if (file_exists('api/methods/' . $action . '/parameters.inc.php')) {                          $parameters = array();                         $missing_parameters = array();                         $wrong_types = array();                          include_once('api/methods/' . $action . '/parameters.inc.php');                          foreach ($parameters as $param => $value ) {                              if (!empty($_POST[$param])) {                                 if(factory::check_parameter_type($_POST[$param], $value[1]))                                     $this->args[$param] = factory::sanitize(factory::set_parameter_type($_POST[$param], $value[1])); //sanitize arguments from request body and assign argument                                 else                                     $wrong_types[] = $param;                              } elseif ($value[0] == 1)                                 $missing_parameters[] = $param;                              else                                 $this->args[$param] = NULL;                          }                          if (empty($missing_parameters)) {                             if (empty($wrong_types)) {                                  try {                                      call_user_func_array(array($this, $action), $this->args); //request api method                                  } catch (ErrorException $e) {                                      view::error($_POST, 503);                                  }                              } else                                 view::error("Incorrect data type for: " . implode(', ', $wrong_types), 204);                         } else                             view::error("Missing parameters: " . implode(', ', $missing_parameters), 204);                     } else                         view::error("Method in developing.", 503);                 } else                     view::error("The method '" . $action . "' does not exist.", 204);             } else                 view::error("No params received.", 204);         } else             view::error("Method was not received.", 204);     }      public function __call($method, $args) { //create new api method from called arguments         @include_once('api/methods/' . $method . '/controller.php');         return true;     } } 

Сам код давольно тривиален – инициализация, вылавливание из URL названия метода… Интересна лишь часть, где происходит выборка объектов из массива POST и преобразуется в аргументы функции которая, уже в дальнейшем, будет играть роль уникального метода.

factory.class.php
В данном файле я коплю функции, которые больше относятся к серверной стороне. Допустим тут можно сжать картинку, засунуть функцию для санитайза или подключить целую библиотеку.

class factory {    public static function check_parameter_type($var, $type) {         switch($type){             case 'string':                 return true;                 break;             case 'boolean':                 if(($var === 'true') || ($var === true))                     return true;                 elseif(($var === 'false') || ($var === false))                     return true;                 else                     return false;                 break;             case 'integer':                 return is_numeric($var) ? true : false;                 break;             case 'smallint':                 return (is_numeric($var) && (strlen($var) == 1)) ? true : false;                 break;             default:                 return false;         }     }      public static function set_parameter_type($var, $type) {         switch($type){             case 'string':                 return $var;                 break;             case 'boolean':                 if(($var === 'true') || ($var === true))                     return 1;                 elseif(($var === 'false') || ($var === false))                     return 0;                 else                     return false;                 break;             case 'integer':                 return $var;                 break;             case 'smallint':                 return $var;                 break;             default:                 return false;         }     }  } 

Пример использования Вы уже видели в api.class.php

$this->args[$param] = factory::sanitize(factory::set_parameter_type($_POST[$param], $value[1]));

Следующий файл отвечает за работу с БД проекта
model.class.php

class db {     public $current;     private $_db;      function __construct() {         try {             include 'db.config.php';             $this->_db->exec('SET NAMES utf8mb4');         } catch (PDOException $e) {          }     }      function __destruct()     {         $this->_db = NULL;     }      protected function catch_db_error($query) {               $dbh = $this->_db->query($query);          if (!$dbh) {             print $query;             die(json_encode(array("Error" => "Syntax error.")));         }         return $dbh;     }      public function orm($sql, $array, $type){          $this->_db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);          try {             $query = $this->_db->prepare($sql.';', array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));             $query->execute($array);         } catch (Exception $e) {             echo $e->getMessage();             die();         }          switch($type) {             case 'select_one':                 return $query->fetch(PDO::FETCH_ASSOC);                 break;             case 'select':                 $query = $query->fetchAll(PDO::FETCH_ASSOC);                 if(is_array($query))                     return $query;                 else                     return array();                 break;             case 'insert':                 return $this->_db->lastInsertId();                 break;             case 'replace':                 return $this->_db->lastInsertId();                 break;             case 'delete':                 return true;                 break;             case 'update':                 return true;                 break;             default:                 return true;         }     } 

Также, при необходимости, сюда вписываются методы работы с БД, которые часто повторяются в проекте. Например, проверка пользователя, получение общих данных о нем и так далее.

view.class.php
View есть view и отвечает он за ответ API. Ответ, который, увидит запросивший в окне браузера.

class view {     static function encode($array) {          array_walk_recursive($array, function (&$value) {             if (!empty($value))                 $value = is_numeric($value) ? intval($value) : $value;             else {                 if (($value === '') or ($value === NULL))                     $value = NULL;                 elseif (($value === '0') or ($value === 0))                     $value = 0;             }         });          print json_encode($array);     }      static function error($text,$code = 400)     {         $result = array(             'error' => $text,             'status' => $code         );          return print json_encode($result);     }      static function state($state)     {         print $state == true ? json_encode(array('Status' => 'Successful.')) : json_encode(array('Status' => 'Invalid token.'));     }      static function status($text,$code) {         $result = array(             'message' => $text,             'status' => $code         );         return print json_encode($result);     } } 

В классе собраны методы для оформления сообщений и описание вывода словарей на JSON.

Итак, мы добрались до самого интересного – как же устроены методы в проекте?

Рассмотрим, на примере метода для входа
image

И снова – смотрим файлы по порядку.

Описание метода начинается с файла parameters.inc.php – мечта параноика.
Позволит вам почувствовать себя крутым программистом который пишет на серьезном языке со статической типизацией. К тому же вы будете точно знать, какой формат данных будет передан в метод.

$parameters = array(     'login' => array(1, 'string'),     'password' => array(1, 'string') ); 

Все что тут храниться – массив входных аргументов для метода. 0 — не обязательный, 1 — обязательный и далее – тип переменной. Проверку на соответствие типу можете описать в factory.class.php. Если что-то пойдет не так вы увидите одну из ошибок, которые мы описали в api.class.php.

Сердце любого метода – контроллер.
controller.php

require('model.php'); $model = new model(); // load database methods  const SALT = 'ВаЩеОченьКруТаяСоЛь';  if((!empty($this->args['login'])) and (!empty($this->args['password']))) {      if($userID = $model->login($this->args['login'], md5(crypt($this->args['password'],SALT)))) {         @session_start();         $_SESSION['userID'] = $userID;          view::encode(array(                 "userID" => $userID             )         );      } else         view::error("Wrong login or password.", 200); } else {     view::error("Fill all fields.", 200); } 

Тут описана логика, обращение к импровизированной ORM, а также, вызывается необходимый метод из класса view для вывода ответа.

Куда же мы без базы
model.php

class model extends db {     public function login($phone,$password) {          $query = $this->orm('SELECT `UserID` FROM `UserPrivate` WHERE `Password` = :password AND `Phone` = :phone', array(             ':phone' => $phone,             ':password' => $password         ), 'select_one');          return $this->return_field($query, 'UserID');     } } 

В папке метода файл model.php позволяет описывать дополнительные фуникции для работы с БД, которые, будут доступны только в текущем методе.

Таким образом – обращение к sitename.com/api/login вызовет описанный выше метод.

На этом описание моего Restfull API сервиса подходит к своему логическому завершению. Заранее прошу прощения за кучу ошибок, опечатки и форматирование.

ссылка на оригинал статьи https://habrahabr.ru/post/280121/