В нынешней разработке все стремятся к чистоте. Чистый код и прозрачный для любого Джуниора паттерн – безусловно залог успешного долгоиграющего проекта, который еще не скоро соберутся переписывать.
В данной статье расскажу, как в течении нескольких лет я пришел к, в моем видении, идеальному решению для реализации Restfull API сервиса на PHP. Конечно, я в курсе, что существует бесчисленное множество фреймворков, которые позволяют за пару минут развернуть своё API. Но, меня всегда одолевали сомнения на их счет. Лично я – никогда не любил использовать чужой код. Сначала: из-за того, что не было уверенности 100% понимания всех происходящих процессов. Позднее: из за сомнений в том, что данный фреймворк – лучшее, что может быть написано для моего проекта.
Если Вы – ярый сторонник фреймворков и не понимаете мой выбор: нашел статью на хабре затрагивающую эту тему.
Итак, если Вы еще не определились какой подход использовать в реализации RestAPI для Вашего сервиса или просто хотите сравнить свой подход с моим – давайте смотреть код!
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 и ничего лишнего.
Далее, для понимания всей картины, давайте окинем взглядом архитектуру будущего приложения:
Скриншот иерархии предоставлен прямо с боевого проекта, а в статье мы затронем лишь папку 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.
Итак, мы добрались до самого интересного – как же устроены методы в проекте?
Рассмотрим, на примере метода для входа
И снова – смотрим файлы по порядку.
Описание метода начинается с файла 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/
Добавить комментарий