Я часто пишу PHP AJAX коннекторы для простых задач на разных сайтах или в составе сложного API среднего проекта. Со временем у меня выработалась система с типовыми решениями.
Серверные коннекторы я часто пишу на PHP, редко на Node.js. Использую по сути 2 протокола, которые вполне схожи, это: RESTful и JSON-RPC.
Основная разница между ними в типах запросов:
Ввиду своей простоты JSON-RPC использую чаще, хотя честно говоря, ещё чаще получается гибридный подход, простой и понятный:

AJAX коннекторы / API с таким типом запросов пишет я думаю подавляющее большинство.
Типовые задачи
Типовые задачи, которые выполняются в коннекторах почти всегда:
- Принять данные входящего запроса, санитизировать их (вычистить попыти инъекций, взлома)
- Подключиться к базе, сделать выборку данных, что-то обновить, изменить
- Если сервис подразумевает наличие профилей, авторизации — авторизовать пользователя
- Если сервис подразумевает наличие персональных настроек по профилям, то вытащить набор настроек для данного профиля/контекста
- Сформировать ответ успешно/не успешно, возможно отдать порцию данных
- Отформатировать и вывести в ответ
- Неплохо бы засекать время, если на один входящий запрос выполняется пакет действий, ставить ограничения по времени
И как я уже сказал, на эти случаи у меня есть наработанные решения, которые я держу отдельными проектами в GitHub и периодически дописываю, стараясь придерживаться сложившейся структуры, в общем, ссылки на проекты с соответствующими решениями в виде PHP классов (инкапсулируя всю логику):
Номера пунктов — соответственно:
- Принять данные входящего запроса, санитизировать их: https://github.com/ershov-ilya/restful.class.php
- Работа с Базой, решение типовых задач: https://github.com/ershov-ilya/database.class.php
- Авторизация: Это пишем сами, либо ищем готовые решения
- Настройки профилей/контекстов: https://github.com/ershov-ilya/config.class.php
- Успешно/не успешно: решает логика Вашего скрипта, все данные для ответа кидаем в один массив
- Отформатировать в требуемом виде (чаще всего JSON): https://github.com/ershov-ilya/format.class.php
- Засекать время: 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/


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