RESTful коннекторы на PHP. Просто и быстро

от автора

Я часто пишу PHP AJAX коннекторы для простых задач на разных сайтах или в составе сложного API среднего проекта. Со временем у меня выработалась система с типовыми решениями.

Серверные коннекторы я часто пишу на PHP, редко на Node.js. Использую по сути 2 протокола, которые вполне схожи, это: RESTful и JSON-RPC.

Основная разница между ними в типах запросов:

RESTful

JSON-RPC

Ввиду своей простоты JSON-RPC использую чаще, хотя честно говоря, ещё чаще получается гибридный подход, простой и понятный:

GET / POST

AJAX коннекторы / API с таким типом запросов пишет я думаю подавляющее большинство.

Типовые задачи

Типовые задачи, которые выполняются в коннекторах почти всегда:

  1. Принять данные входящего запроса, санитизировать их (вычистить попыти инъекций, взлома)
  2. Подключиться к базе, сделать выборку данных, что-то обновить, изменить
  3. Если сервис подразумевает наличие профилей, авторизации — авторизовать пользователя
  4. Если сервис подразумевает наличие персональных настроек по профилям, то вытащить набор настроек для данного профиля/контекста
  5. Сформировать ответ успешно/не успешно, возможно отдать порцию данных
  6. Отформатировать и вывести в ответ
  7. Неплохо бы засекать время, если на один входящий запрос выполняется пакет действий, ставить ограничения по времени

И как я уже сказал, на эти случаи у меня есть наработанные решения, которые я держу отдельными проектами в GitHub и периодически дописываю, стараясь придерживаться сложившейся структуры, в общем, ссылки на проекты с соответствующими решениями в виде PHP классов (инкапсулируя всю логику):

Номера пунктов — соответственно:

  1. Принять данные входящего запроса, санитизировать их: https://github.com/ershov-ilya/restful.class.php
  2. Работа с Базой, решение типовых задач: https://github.com/ershov-ilya/database.class.php
  3. Авторизация: Это пишем сами, либо ищем готовые решения
  4. Настройки профилей/контекстов: https://github.com/ershov-ilya/config.class.php
  5. Успешно/не успешно: решает логика Вашего скрипта, все данные для ответа кидаем в один массив
  6. Отформатировать в требуемом виде (чаще всего JSON): https://github.com/ershov-ilya/format.class.php
  7. Засекать время: https://github.com/ershov-ilya/timer.class.php

Если ваш проект под системой контроля версий Git, то вы легко можете добавить любой из этих проектов к себе, как подмодуль:

В корне своего проекта пишите:

git submodule add https://github.com/ershov-ilya/restful.class.php.git папка/от/корня/проекта/куда/класть 

Обновить потом все подмодули проекта, в корне проекта пишем (в консоли):

git submodule foreach git pull 

Подробнее об остальных действиях команды submodule:

git submodule --help 

или в гугле.
Если вы не умеете пользоваться git’ом, нажимайте кнопку «Download ZIP», скачивайте архив и распаковывайте к себе в проект.

Теперь детально

Образец кода готового решения на базе этих классов

<?php header('Content-Type: text/plain; charset=utf-8'); require_once('../core/config/api.config.private.php'); session_start();  if(DEBUG){     error_reporting(E_ERROR | E_WARNING);     ini_set("display_errors", 1); }  $response=array(     'response' => '204 No Content' ); $format='json';  try {     // API includes     require_once(API_CORE_PATH.'/class/restful/restful.class.php');     require_once(API_CORE_PATH.'/class/auth/auth.class.php');     require_once(API_CORE_PATH.'/class/permissions/permissions.class.php');     require_once(API_CORE_PATH.'/class/database/database.class.php');     require_once(API_CORE_PATH.'/class/config/config.class.php');     require_once(API_CORE_PATH.'/modules/smsc/send.func.php');     require_once(API_CORE_PATH.'/config/smsc.config.private.php');      // restful     $rest=new RESTful('lead', array('id', 'name', 'email', 'phone', 'card_id', 'site', 'sum', 'message', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_content'), array(         // Опции санитизации (чистки) данных входящего запроса         // Варианты: ключ для функции filter_var, регулярка или Closure функция (можно объект с методом __invoke)         'id'    => FILTER_SANITIZE_NUMBER_INT,         'sum'    => FILTER_SANITIZE_NUMBER_FLOAT,         'utm_source'  => '/[^0-9a-zA-Z_\-]/',         'utm_medium'  => '/[^0-9a-zA-Z_\-]/',         'utm_campaign'  => '/[^0-9a-zA-Z_\-]/',         'utm_content'  => '/[^0-9a-zA-Z_\-]/',         'card_id' =>  function($val=null){             if(!empty($val) && $val>10000) return $val;             return '';         }     ));      // Модуль авторизации - здесь используется класс из конкретного проекта     if(empty($rest->scope['scope'])) throw new Exception('403 Auth needed');     $auth=new Auth($rest->scope); // Получение из БД настроек пространства      // Модуль прав доступа - здесь используется класс из конкретного проекта     $permissions=new Permissions($rest->scope, $auth);     $permissions->CORS();      // API определения города     $ip=$rest->scope['ip'];     $memcache = new Memcache;     $memcache->addServer("127.0.0.1");     $city=$memcache->get($ip);     if(empty($city)) {         $link='http://api.sypexgeo.net/json/';         $curl=curl_init();         curl_setopt($curl,CURLOPT_RETURNTRANSFER,true);         curl_setopt($curl,CURLOPT_URL,$link.$ip);         curl_setopt($curl,CURLOPT_HEADER,false);         $out=curl_exec($curl); // Инициируем запрос к API и сохраняем ответ в переменную         $code=curl_getinfo($curl,CURLINFO_HTTP_CODE);         curl_close($curl); // Завершаем сеанс cURL         $res_arr=json_decode($out);         $city=$res_arr->city->name_ru.', '.$res_arr->country->name_ru;         $memcache->set($ip, $city, false, 2592000);     }       if(DEBUG){         // При включённой константе DEBUG         // Здесь можно посмотреть 2 массива объекта класса RESTful         // $rest->scope - данные для авторизации         // $rest->data - данные запроса         print "Scope:\n";         print_r($rest->scope);         print "Data:\n";         print_r($rest->data);     }      // В принципе можно выполнить     // extract($rest->data);      // Образец массива с настройками подключения есть в проекте database.class.php     $pdoconfig = array(         'dbhost'    =>  'localhost',         'dbuser'    =>  'username',         'dbpass'    =>  'password',         'dbname'    =>  'database',         'dbtype'    =>  'mysql'     );     // Модуль подключения к БД     $db = new Database($pdoconfig);     if(!$db) throw new Exception('500 DB connect error');      // Объект класса Config     $config=new Config($db);      if ($rest->scope['METHOD'] == 'POST') {         // Данные для записи в БД         $data=$rest->data;         $data['city']=$city;          // Проверка прав         if(!$permissions->ask('append')) throw new Exception('500 Not allowed');          // Составление переменного набора полей         $fields = array();         $placeholders = array();         foreach ($data as $key => $val) {             $fields[] = $key;             $placeholders[] = ':' . $key;         }         $fields = implode(', ', $fields);         $placeholders = implode(', ', $placeholders);          // Подготовка Запроса         $sql = "INSERT INTO leads ($fields) VALUES ($placeholders)";          $stmt = $db->dbh->prepare($sql);         if(!$stmt) throw new Exception('500 DB statement error');         foreach ($data as $key => $val) {             $stmt->bindParam(':' . $key, $data[$key]);         }          //  Запись в БД         $res = $stmt->execute();         $lastInsertId=$db->dbh->lastInsertId('id');          if($res){             $response=array(                 'response' => '200 Done'             );         }         if(DEBUG) {             $response['Запись строки в scopes'] = $res;         }          $send_lead_notify_sms=$config('send_lead_notify_sms', 0, true);         if(!empty($send_lead_notify_sms)) {             // Отправка СМС             $sms = array(                 'mes' => 'Новая заявка',                 'phones' => $config('report_sms_phone', '79008887766', true),                 'sender' => 'MY SERVICE'             );             if (!empty($rest->data['name'])) $sms['mes'] .= ':' . PHP_EOL . $rest->data['name'];             if (!empty($rest->data['phone'])) $sms['mes'] .= PHP_EOL . $rest->data['phone'];             if (!empty($rest->data['sum'])) $sms['mes'] .= PHP_EOL . 'Сумма: ' . $rest->data['sum'];             if (!empty($rest->data['site'])) $sms['mes'] .= PHP_EOL . 'Сайт: ' . $rest->data['site'];             if (!empty($rest->data['campaign'])) $sms['mes'] .= PHP_EOL . '"' . $rest->data['campaign'] . '"';             if (!empty($rest->data['utm_source'])) $sms['mes'] .= PHP_EOL . 'Источник: ' . $rest->data['utm_source'];             if (!empty($rest->data['message'])) $sms['mes'] .= PHP_EOL . 'Сообщение: ' . $rest->data['message'];             $res = send_sms($sms, $smsc_config);             $response['sms'] = $res['response']['cnt'];         }     }  } catch(Exception $e) {     // При ошибке     $response['response']=$e->getMessage(); }  // Форматирование и вывод ответа require_once(API_CORE_PATH . '/class/format/format.class.php'); print Format::parse($response, $format); ?> 

Санитизация входящего запроса restful.class.php

Инициализация:

<?php $rest=new RESTful('promocode', 'id,code,order', array(     // Sanitize options     'id'    => FILTER_SANITIZE_NUMBER_INT,     'code'  => '/[^0-9a-zA-Z]/',     'order' =>  function($val=null){         if(!empty($val)) return $val;         return null;     } )); ?> 

Конструктор класса принимает входящий запрос, санитизирует его (чистит от инъекций) и разбивает на 2 массива: $rest->scope (информация о запросе: IP источника, user-agent и информация для авторизации) и $rest->data (сами параметры запроса).

Первый параметр — это по сути имя коннектора / действия, будет доступно в $rest->scope[‘ACTION’], может быть полезен для определения прав и разрешений.

Список полей, которые нужно принять в массив $rest->data — строка через запятую, либо массив.

Третий необязательный параметр — массив с определениями методов санитизации в формате:. Default is FILTER_SANITIZE_STRING for the filter_var().

«имя поля» => метод санитизации (Флаг для filter_var, строка с регуляркой, либо Closure функция или объект с методом __invoke($raw_value)).

Четвёртый необязательный параметр: Список полей которые нужно принять в массив $rest->scope.

print_r($rest->scope); // Смотрим массив информации о запросе print_r($rest->data); // Смотрим параметры запроса print $rest('code'); // Смотрим значение конкретного входящего параметра (будет доступен, только если указан в списке полей во втором параметре конструктора) 

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

database.class.php

Класс Database — обёртка для PDO, с реализацией типовых задач:

  • Вытащить / добавить строку в таблицу
  • Сделать выборку нескольких строк, по условию
  • Добавить несколько строк (загрузить массив данных в БД)

Инициализация:

$db = new Database($pdoconfig); 

В качестве параметра конструктор принимает массив вида:

    $pdoconfig = array(         'dbhost'    =>  'localhost',         'dbuser'    =>  'username',         'dbpass'    =>  'password',         'dbname'    =>  'database',         'dbtype'    =>  'mysql'     ); 

или путь к файлу содержащему такой массив (с именем $pdoconfig).

Имеет методы:

$db->getOneSQL($sql) // Вытащить одну (первую) строку из ответа на запрос $sql, возвращает ассоциированный массив "имя столбца" => "значение" $db->getOneWhere($table, $where='1', $filter='')  // Вытащить одну (первую) строку из ответа на запрос $sql, возвращает ассоциированный массив "имя столбца" => "значение" // От предыдущей отличается только тем, что можно написать простое условие поиска, например $db->getOneWhere("task", "status='new'");  $db->exec($sql) // Исполнить запрос $sql, вернуть количество затронутых строк $db->getOne($table, $id, $id_field_name='id', $filter='') // Вытащить одну строку с айдишником равным $id, возвращает ассоциированный массив "имя столбца" => "значение" 

Первый параметр "$table" — имя таблицы для выборки. Если столбец с айдишниками называется не «id», третьим параметром можно указать имя столбца по которому искать. Четвёртый параметр "$filter" — набор столбцов, который нужно вытащить. Возвращает ассоциированный PHP массив «имя столбца» => «значение».

Остальные функции работают по аналогии, смыслы параметров повторяются.

$db->get($sql) // Произвольная SQL выборка из базы $db->getTable($table, $columns='', $from=0, $limit=-1) // Получить целую таблицу, возвращает двумерный ассоциативный запрос $db->getTableWhere($table, $columns='', $where='1')  // Получить таблицу с условием WHERE, возвращает двумерный ассоциативный запрос $db->getTableByKey($table, $key, $keyColumnName='scope')  // Получить таблицу, где поле $keyColumnName равно значению $key, возвращает двумерный ассоциативный запрос $db->Count($table, $where) // Получить количество строк в таблице $table, удовлетворяющих условию $where $db->putOne($table, $data, $flags=0) // Добавить одну строку в таблицу, принимает ассоциативный массив $db->put($table, $fields, $data, $flags=0, $overlay=array(), $default=NULL) // Добавить много строк в таблицу, принимает двумерный ассоциативный массив $db->updateOne($table, $id, $data, $id_field_name='id') // Обновить одну строку в таблице, принимает ассоциативный массив $db->updateMass($table, $data, $props=array()) // Обновить много строк в таблице, принимает двумерный ассоциативный массив 

Все функции класса Database сохраняют полученный ответ в массиве $this->last[$storage], где $storage совпадает с именем метода:

$db->toKeyValue($storage='getTable') // Получить ассоциативный массив "ключ" => "значение" из хранилища последних результатов, принимает параметр имя выполненного метода, например 'getTable' или 'getTableByKey' $db->toValueArray($storage='getTable', $column=1) // Получить простой массив занчений из хранилища (результат последнего запроса), принимает параметр имя выполненного метода, например 'getTable' или 'getTableByKey' $db->pick($columns=NULL, $map=array(), $storage='getOne') // Вытащить несколько колонок  из хранилища (результат последнего однострочного запроса) $db->pickLine($field, $key, $map=array(), $filter=array(), $storage='getTableByKey') // Вытащить одну строку из хранилища (результат последнего многострочного запроса) $db->transpose($storage='getTable') // Транспонирование (поворот) таблицы, также принимает в качестве параметра имя метода класса например (результат последнего многострочного запроса) 'getTable' 

config.class.php

Инициализация:

$config=new Config($db); // Принимает в качестве параметра вышеупомянутый массив $pdoconfig, объект класса Database или объект класса PDO // Либо можно указать путь к текстовому файлу вида: ключ значение $config=new Config($db); 

Работает только с одной таблицей, имя которой можно указать с помощью второго необязательного параметра, который представляет собой конфигруационный массив, например:

$config=array(     'preload'       =>  true,     'table'         =>  'config', // имя таблицы     'keyfield'      =>  'key',    // имя столбца с именами ключей     'valuefield'    =>  'value',  // имя столбца со значениями ключей     'file_delimiter'     =>  ' ', // разделитель в файле, который разделяет строку на ключ и значение     'file_strip_first_line'  =>  false ); $config=new Config($db, $config); 

format.class.php

Класс, все методы которого статичные. Умеет форматировать массивы PHP в форматы:

  • JSON
  • plain
  • строки GET/POST запроса
  • phpArray
  • SQL

Следующим образом:

print Format::parse($array, 'json'); print Format::parse($array, 'php'); print Format::parse($array, 'get'); print Format::parse($array, 'plain'); 

Мультитаймер timer.class.php

Пример кода: простой

require_once('timer.class.php'); $timer=new Timer(); $timer->start('mysql'); sleep(1); print $timer('mysql'); $timer->stop('mysql'); print_r($timer->data()); // Массив с результатами print $timer; // Вывести отчёт 

Этот класс может создавать таймеры с зависимостями, счётчики будут храниться в структуре в виде дерева. Стартовать и останавливать таймеры можно сколько угодно раз, в итоге будет показана сумма отрезков времени. __invoke функция показывает текущее время по конкретному счётчику НЕ останавливая его, например: print $timer(‘mysql’);.

Соответственно имеются методы.

$timer->start('mysql:sql:query:response:parsing', true); // Второй параметр true говорит, что нужно стартануть все таймеры-родители $timer->stopTree('mysql'); // Остановить всё дерево mysql 

Пример кода: посложнее

<?php require_once('timer.class.php'); $timer=new Timer(array( 'debug'=>true )); $timer->start('mysql:sql:query:response:parsing',true); $timer->start('postgres:sql:query:response:parsing'); sleep(1); print_r($timer->data()); print $timer; $timer->start('file:read'); sleep(1); $timer->stopTree('mysql'); print_r($timer->data()); ?> 

Разделитель родительский: дочерний можно заменить с помощью массива конфигурации параметром конструтора.

<?php $config=array(     'query_delimiter'   =>  ':',     'output_delimiter'  =>  '=',     'add_children_time' => true,     'debug'             => false ); $timer=new Timer($config); ?> 

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


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *