Я часто пишу PHP AJAX коннекторы для простых задач на разных сайтах или в составе сложного API среднего проекта. Со временем у меня выработалась система с типовыми решениями.
Серверные коннекторы я часто пишу на PHP, редко на Node.js. Использую по сути 2 протокола, которые вполне схожи, это: RESTful и JSON-RPC.
Основная разница между ними в типах запросов:
Ввиду своей простоты JSON-RPC использую чаще, хотя честно говоря, ещё чаще получается гибридный подход, простой и понятный:
AJAX коннекторы / API с таким типом запросов пишет я думаю подавляющее большинство.
Типовые задачи, которые выполняются в коннекторах почти всегда:
И как я уже сказал, на эти случаи у меня есть наработанные решения, которые я держу отдельными проектами в GitHub и периодически дописываю, стараясь придерживаться сложившейся структуры, в общем, ссылки на проекты с соответствующими решениями в виде PHP классов (инкапсулируя всю логику):
Номера пунктов — соответственно:
Если ваш проект под системой контроля версий Git, то вы легко можете добавить любой из этих проектов к себе, как подмодуль:
В корне своего проекта пишите:
Обновить потом все подмодули проекта, в корне проекта пишем (в консоли):
Подробнее об остальных действиях команды submodule:
или в гугле.
Если вы не умеете пользоваться git'ом, нажимайте кнопку «Download ZIP», скачивайте архив и распаковывайте к себе в проект.
Инициализация:
Конструктор класса принимает входящий запрос, санитизирует его (чистит от инъекций) и разбивает на 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.
В итоге получаем очищеный от всяких возможных гадостей запрос, дальше можно спокойно работать с Базой Данных.
Класс Database — обёртка для PDO, с реализацией типовых задач:
Инициализация:
В качестве параметра конструктор принимает массив вида:
или путь к файлу содержащему такой массив (с именем $pdoconfig).
Имеет методы:
Первый параметр "$table" — имя таблицы для выборки. Если столбец с айдишниками называется не «id», третьим параметром можно указать имя столбца по которому искать. Четвёртый параметр "$filter" — набор столбцов, который нужно вытащить. Возвращает ассоциированный PHP массив «имя столбца» => «значение».
Остальные функции работают по аналогии, смыслы параметров повторяются.
Все функции класса Database сохраняют полученный ответ в массиве $this->last[$storage], где $storage совпадает с именем метода:
Инициализация:
Работает только с одной таблицей, имя которой можно указать с помощью второго необязательного параметра, который представляет собой конфигруационный массив, например:
Класс, все методы которого статичные. Умеет форматировать массивы PHP в форматы:
Следующим образом:
Этот класс может создавать таймеры с зависимостями, счётчики будут храниться в структуре в виде дерева. Стартовать и останавливать таймеры можно сколько угодно раз, в итоге будет показана сумма отрезков времени. __invoke функция показывает текущее время по конкретному счётчику НЕ останавливая его, например: print $timer('mysql');.
Соответственно имеются методы.
Разделитель родительский: дочерний можно заменить с помощью массива конфигурации параметром конструтора.
Серверные коннекторы я часто пишу на 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);
?>
Borro
У вас не хватает:
А вообще, мой вам совет: уберите этот пост, сделайте изменения описанные мной, а потом посмотрите в сторону микрофреймворков с поддержкой REST или компонент Symfony
ershov-ilya
Вот именно таких подсказок я и ждал от этого поста. Спасибо :)