В нынешней разработке все стремятся к чистоте. Чистый код и прозрачный для любого Джуниора паттерн – безусловно залог успешного долгоиграющего проекта, который еще не скоро соберутся переписывать.
В данной статье расскажу, как в течении нескольких лет я пришел к, в моем видении, идеальному решению для реализации Restfull API сервиса на PHP. Конечно, я в курсе, что существует бесчисленное множество фреймворков, которые позволяют за пару минут развернуть своё API. Но, меня всегда одолевали сомнения на их счет. Лично я – никогда не любил использовать чужой код. Сначала: из-за того, что не было уверенности 100% понимания всех происходящих процессов. Позднее: из за сомнений в том, что данный фреймворк – лучшее, что может быть написано для моего проекта.
Если Вы – ярый сторонник фреймворков и не понимаете мой выбор: нашел статью на хабре затрагивающую эту тему.
Итак, если Вы еще не определились какой подход использовать в реализации RestAPI для Вашего сервиса или просто хотите сравнить свой подход с моим – давайте смотреть код!
Перед тем как брать подход к себе на вооружение учтите: код был применен еще в далеком 12 году и написан по старым канонам. Содержит в себе массу шероховатостей. Ни в коем случае не стоит переносить содержимое классов в свои проекты. Польза приведенного ниже кода — максимум показать идею!
Также, по поводу вложенности условий, ценный совет дал комментатор alekciy habrahabr.ru/post/280121/#comment_8821069
Начнем, конечно, с index.php:
В котором, как ни странно для MVC паттерна, практически ничего нет.
Для вызова API нам потребуется всего несколько строк:
Таким образом, теперь, любые обращения на URL sitename.com/api/что_то_там_еще будут способствовать только вызову класса API и ничего лишнего.
Далее, для понимания всей картины, давайте окинем взглядом архитектуру будущего приложения:
Скриншот иерархии предоставлен прямо с боевого проекта, а в статье мы затронем лишь папку api и файл index.php.
api.class.php
У нас перед глазами основной файл, в котором кроется вся суть моего подхода: базовый класс играет роль роутера – он будет определять обращение к конкретному методу API, подготавливать все данные и осуществлять вызов названного метода.
Сам код довольно тривиален – инициализация, вылавливание из URL названия метода… Интересна лишь часть, где происходит выборка объектов из массива POST и преобразуется в аргументы функции которая, уже в дальнейшем, будет играть роль уникального метода.
factory.class.php
В данном файле я коплю функции, которые больше относятся к серверной стороне. Допустим тут можно сжать картинку, засунуть функцию для санитайза или подключить целую библиотеку.
Пример использования Вы уже видели в api.class.php
Следующий файл отвечает за работу с БД проекта
model.class.php
Также, при необходимости, сюда вписываются методы работы с БД, которые часто повторяются в проекте. Например, проверка пользователя, получение общих данных о нем и так далее.
view.class.php
View есть view и отвечает он за ответ API. Ответ, который, увидит запросивший в окне браузера.
В классе собраны методы для оформления сообщений и описание вывода словарей на JSON.
Рассмотрим, на примере метода для входа
И снова – смотрим файлы по порядку.
Описание метода начинается с файла parameters.inc.php – мечта параноика.
Позволит вам почувствовать себя крутым программистом который пишет на серьезном языке со статической типизацией. К тому же вы будете точно знать, какой формат данных будет передан в метод.
Все что тут храниться – массив входных аргументов для метода. 0 — не обязательный, 1 — обязательный и далее – тип переменной. Проверку на соответствие типу можете описать в factory.class.php. Если что-то пойдет не так вы увидите одну из ошибок, которые мы описали в api.class.php.
Сердце любого метода – контроллер.
controller.php
Тут описана логика, обращение к импровизированной ORM, а также, вызывается необходимый метод из класса view для вывода ответа.
Куда же мы без базы
model.php
В папке метода файл model.php позволяет описывать дополнительные фуникции для работы с БД, которые, будут доступны только в текущем методе.
Таким образом – обращение к sitename.com/api/login вызовет описанный выше метод.
На этом описание моего Restfull API сервиса подходит к своему логическому завершению. Заранее прошу прощения за кучу ошибок, опечатки и форматирование.
В данной статье расскажу, как в течении нескольких лет я пришел к, в моем видении, идеальному решению для реализации Restfull API сервиса на PHP. Конечно, я в курсе, что существует бесчисленное множество фреймворков, которые позволяют за пару минут развернуть своё API. Но, меня всегда одолевали сомнения на их счет. Лично я – никогда не любил использовать чужой код. Сначала: из-за того, что не было уверенности 100% понимания всех происходящих процессов. Позднее: из за сомнений в том, что данный фреймворк – лучшее, что может быть написано для моего проекта.
Если Вы – ярый сторонник фреймворков и не понимаете мой выбор: нашел статью на хабре затрагивающую эту тему.
Итак, если Вы еще не определились какой подход использовать в реализации RestAPI для Вашего сервиса или просто хотите сравнить свой подход с моим – давайте смотреть код!
Перед тем как брать подход к себе на вооружение учтите: код был применен еще в далеком 12 году и написан по старым канонам. Содержит в себе массу шероховатостей. Ни в коем случае не стоит переносить содержимое классов в свои проекты. Польза приведенного ниже кода — максимум показать идею!
После освоения принципов переданных в статье обязательно посмотрите ссылки ниже:
www.php-fig.org/psr/psr-2
getcomposer.org
Также, по поводу вложенности условий, ценный совет дал комментатор alekciy habrahabr.ru/post/280121/#comment_8821069
Безусловно, перед началом Вам стоит настроить Ваш веб-сервер, что-бы все запросы летели в 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;
}
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 сервиса подходит к своему логическому завершению. Заранее прошу прощения за кучу ошибок, опечатки и форматирование.