Pull to refresh

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

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

В данной статье расскажу, как в течении нескольких лет я пришел к, в моем видении, идеальному решению для реализации 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;
}


Начнем, конечно, с 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 сервиса подходит к своему логическому завершению. Заранее прошу прощения за кучу ошибок, опечатки и форматирование.
Tags:
Hubs:
-22
Comments94

Articles