Создаём собственный API – 2
Всем привет 🙂
Ну что ж, как я и обещал, сегодня мы создадим класс Executor в первом приближении. Для нормальной работы класса нам понадобится несколько вспомогательных классов-утилит и функций, а также драйвер для проверки того, что получилось. На самом деле, эти вспомогательные классы займут львиную долю сегодняшней статьи, но они необходимы и третья часть будет посвящена API в чистом виде. Но, не будем забегать вперёд.
Итак, для работы нам понадобится следующее:
- 1.vd – функция вывода ошибок на экран. Я использую очень старую функцию-обёртку для var_dump,
автором которой не являюсь, но функция очень удобна и поможет в отладке. Хотя, в нижеприведенном коде она будет вызываться всего лишь один раз, чтобы насладиться репортом о правильном результате =) - 2. rnd_str – я набросал на скорую руку функцию, которая генерирует символьный мусор 🙂 Вы можете использовать нечто более осмысленное, вплоть до нормальных понятных данных.
- 3. simple_db – очень простой класс для работы с MySQL. Имплементирует интерфейс db_itrfc и содержит одно public свойство – массив с ответами MySQL. Класс является уязвимым к SQL-инъекциям и служит исключительно демонстрационным целям. Вы можете использовать более надёжный класс, например, ADOdb http://ru.wikipedia.org/wiki/ADOdb. Для этого создайте обёртку объекта ADOdb, имплементирующую интерфейс db_itrfc.
- 4. db_utility – класс-утилита, который создаёт структуру базы данных. На самом деле я мог использовать обычный *.sql файл, но решил написать утилиту из соображений удобства. Если Вы хотите изменить структуру базы, вам достаточно создать дочерний класс и обработать его напильником. Утилита имеет исполняемый метод void run(void). При повторном запуске утилиты, база уничтожается и создаётся заново.
- 5. api_list_utility – класс-утилита, который заполняет базу необходимыми функциями. Собственно говоря, API функциями. Также имеет исполняемый метод void run(void), но в отличие от db_utility дописывает данные, а не заменяет новые на старые. Так что при необходимости повторно использовать утилиту, нужно ещё раз запустить db_utility::run()
И самое главное – иерархия api.
Следуя традиции, сплошная стрелка заменяет слово “наследует”. Пунктирной стрелкой будем заменять слово “реализует”.
Теперь разберём подробно всё с самого начала и посмотрим на код изнутри.
- 1. Функция vd() получает ссылку на 1 параметр любого типа и возвращает форматированный var_dump.
function vd(&$var) { ob_start(); var_dump($var); $cnt=ob_get_contents(); ob_end_clean(); return nl2br(str_replace(' ',' ', htmlspecialchars($cnt))); }
- 2. Функция rnd_str() получает один числовой параметр, который задаёт длину
возвращаемой строки. Возвращаемая строка состоит из случайного набора символов латинского алфавита.function rnd_str($char_num=0) { $abc = str_shuffle('abcdefghijklmnopqrstuvwxyz'); $res = ''; for($i=0; $i<$char_num; $i++) { $res .= $abc[rand(0, strlen($abc)-1)]; } return $res; }
- 3.1. Интерфейс db_itrfc определяет три метода:
interface db_itrfc { // выполнить произвольный запрос к базе public function query($query); // выполнить произвольный запрос и вернуть результат. // используется для запросов SELECT public function res_query($query); // получить информацию о последнем выполненном запросе public function get_query_info(); }
3.2. Класс simple_db имплементирует интерфейс db_itrfc:
class simple_db implements db_itrfc { protected $db_host = ''; //хост базы данных protected $db_user = ''; // пользователь БД protected $db_pwd = ''; // пароль пользователя protected $db_rsc = null; // ссылка на ресурс protected $query_info = array(); // информация о запросе public $db_messages = array(); // сообщения об ошибках // конструктор принимает параметры - хост, логин и пароль, // присваивает эти параметры свойствам класса // затем пытается соединиться с MySQL // в случае ошибки соединения добавляет информацию в массив // $db_messages и возвращает false, если всё ок, то $db_messages // содержит приятную информацию и конструктор возвращает true public function __construct($host='', $user='', $pwd='') { $this->db_host = $host; $this->db_user = $user; $this->db_pwd = $pwd; $this->db_rsc = @mysql_connect($this->db_host, $this->db_user, $this->db_pwd); if(!$this->db_rsc) { $this->db_messages[] = 'Error #'.mysql_errno().': '. mysql_error(); return false; } else { $this->db_messages[] = 'Connect ok'; return true; } } // деструктор закрывает соединение с MySQL public function __destruct() { return @mysql_close($this->db_rsc); } // void collect_info(void) собирает системную // информацию о запросе protected function collect_info() { $this->query_info['id'] = mysql_insert_id($this->db_rsc); $this->query_info['info'] = mysql_info($this->db_rsc); return; } // false push_error(void) сохраняет номер и текстовое сообщение // об ошибке protected function push_error() { $this->db_messages[] = 'Error #'.mysql_errno($this->db_rsc). ': '.mysql_error($this->db_rsc); return false; } // resource query(string) - выполняем запрос к базе, сохраняем // инфо о запросе, возвращаем ссылку на ресурс public function query($query) { $res = null; $res = mysql_query($query, $this->db_rsc); $this->collect_info(); return $res; } // mixed res_query(string) - выполняем запрос к базе, сохраняем // инфо о запросе. Если попытка выполнения запроса окончилось // ошибкой, сохраняем данные об ошибке и возвращаем false, // если всё ок, то формируем массив результатов // и возвращаем array public function res_query($query) { $result = array(); $result_query = false; $result_query = mysql_query($query, $this->db_rsc); $this->collect_info(); if(!$result_query) { return $this->push_error(); } else { while($row = mysql_fetch_assoc($result_query)) { $result[] = $row; } return $result; } } // array get_query_info(void) - всё предельно просто public function get_query_info() { return $this->query_info; } }
Итак, как было сказанно выше, наш класс simple_db позволяет вполне свободно работать с MySQL. Теперь посмотрим на утилиту db_utility, которая создаёт структуру базы данных. Сама структура крайне проста. Для начала будем использовать три таблицы. В таблице data будут храниться данные (мусор, генерируемый функцией rnd_str) о пользователе – имя (f_name) и фамилия (l_name). В таблице api будет храниться информация об API-функциях, а именно – имя функции (func_name), количество аргументов (arg_num) и тип возвращаемого параметра (ret_type). В таблице api_args будет храниться информация об аргументах, а именно – ссылка на таблицу api (aid), имя аргумента (arg_name) и тип аргумента (arg_type).
Задача класса db_utility создать эту базу и заполнить таблицу data тестовыми данными:
class db_utility { // определяем названия таблиц и базы. protected $db_name = 'api_test'; protected $api_table = 'api'; protected $args_table = 'api_args'; protected $data_table = 'data'; protected $basic_queries = array(); protected $db_handler = null; // Конструктор получает объект для работы с базой данных, // имплементирующий интерфейс db_itrfc и инициализирует // переменную $basic_queries через вызов защищённого метода public function __construct($db_object) { $this->db_handler = $db_object; $this->create_queries(); } // Инициализируем массив $basic_queries набором запросов, // необходимых для содзания структуры базы // и заполнения таблицы data тестовыми данными. protected function create_queries() { $this->basic_queries[] = 'DROP DATABASE IF EXISTS `'.$this->db_name.'`'; $this->basic_queries[] = 'CREATE DATABASE `'.$this->db_name.'`'; $this->basic_queries[] = 'USE `'.$this->db_name.'`'; $this->basic_queries[] = 'DROP TABLE IF EXISTS `'.$this->api_table.'`'; $this->basic_queries[] = 'CREATE TABLE `'.$this->api_table.'`(' . 'id INT not null auto_increment primary key, func_name char(255) binary not null, ' . ' arg_num tinyint not null, ret_type char(255) binary not null)'; $this->basic_queries[] = 'drop table if exists `'.$this->args_table.'`'; $this->basic_queries[] = 'create table `'.$this->args_table.'`(' . 'id int not null auto_increment primary key, aid int not null, arg_name char(255) binary not null, '. 'arg_type char(255) binary not null)'; $this->basic_queries[] = 'drop table if exists `'.$this->data_table.'`'; $this->basic_queries[] = 'create table `'.$this->data_table.'`(' . 'id int not null auto_increment primary key, f_name char(255) binary not null, '. 'l_name char(255) binary not null)'; for($i=0; $i< rand(10, 50); $i++) { $this->basic_queries[] = 'insert into `'.$this->data_table.'` values (' . 'NULL, "'.rnd_str(rand(3, 10)).'", "'.rnd_str(rand(3, 10)).'")'; } return; } // Иполняемая функция утилиты void run(void). Выполняем все запросы один за другим public function run() { for($i=0; $i<sizeof($this->basic_queries); $i++) { $this->db_handler->query($this->basic_queries[$i]); } return; } }
Пришло время подумать над простеньким API, который и будет использовать удалённое приложение. Но для начала изучим базовый класс api, в котором нет абсолютно ничего сложного. Класс просто инициализирует объект для работы с базой данных и устанавливает правильные имена для таблиц.
class api { protected $db_name = 'api_test'; protected $api_table = 'api'; protected $args_table = 'api_args'; protected $data_table = 'data'; protected $db_handler = null; // Отличие конструктора в том, что он использует // запрос USE вместо стандартного вызова mysql_select_db public function __construct($db_object) { $this->db_handler = $db_object; $this->db_handler->query('use `'.$this->db_name.'`'); } }
Утилита api_list_utility по своему принципу ничем не отличается от утилиты db_utility, но имеет более сложную исполняемую функцию void run(void).
Как мы увидем чуть ниже, наш API будет состоять из следующих функций:
- array get_user(int) – возвращаем инфо о пользователе из таблицы data по его уникальному ключу
- bool delete_user(int) – удаляем пользователя из таблицы data по его уникальному ключу
- bool add_user(string, string) – добавляем пользователя (имя, фамилия) в таблицу data
- bool add_users(array) – добавляем несколько пользователей в таблицу data. Аналогично add_user
- array get_users(void) – возвращаем всех пользоваталей из таблицы data. Будем считать для однозначности, что функция получает один параметр – void
- array get_users_or(array) – возвращаем всех пользователей, удовлетворяющих какому-то условию
- array get_users_and(array) – возвращаем всех пользователей, удовлетворяющих какому-то условию
class api_list_utility extends api { protected $basic_queries = array(); public function __construct($db_object) { parent::__construct($db_object); $this->create_functions(); } protected function create_functions() { $this->basic_queries[] = array( 'func' => 'insert into `'.$this->api_table.'` values (NULL, "get_user", "1", "array")', 'args' => array( 'insert into `'.$this->args_table.'` values (NULL, ?, "id", "int")' ) ); $this->basic_queries[] = array( 'func' => 'insert into `'.$this->api_table.'` values (NULL, "delete_user", "1", "bool")', 'args' => array( 'insert into `'.$this->args_table.'` values (NULL, ?, "id", "int")' ) ); $this->basic_queries[] = array( 'func' => 'insert into `'.$this->api_table.'` values (NULL, "add_user", "2", "bool")', 'args' => array( 'insert into `'.$this->args_table.'` values (NULL, ?, "fname", "string")' ,'insert into `'.$this->args_table.'` values (NULL, ?, "lname", "string")' ) ); $this->basic_queries[] = array( 'func' => 'insert into `'.$this->api_table.'` values (NULL, "add_users", "1", "bool")', 'args' => array( 'insert into `'.$this->args_table.'` values (NULL, ?, "udata", "array")' ) ); $this->basic_queries[] = array( 'func' => 'insert into `'.$this->api_table.'` values (NULL, "get_users_or", "1", "array")', 'args' => array( 'insert into `'.$this->args_table.'` values (NULL, ?, "cases", "array")' ) ); $this->basic_queries[] = array( 'func' => 'insert into `'.$this->api_table.'` values (NULL, "get_users_and", "1", "array")', 'args' => array( 'insert into `'.$this->args_table.'` values (NULL, ?, "cases", "array")' ) ); $this->basic_queries[] = array( 'func' => 'insert into `'.$this->api_table.'` values (NULL, "get_users", "1", "array")', 'args' => array( 'insert into `'.$this->args_table.'` values (NULL, ?, "void", "string")' ) ); return; } public function run() { $info = array(); for($i=0; $i<sizeof($this->basic_queries); $i++) { $this->db_handler->query($this->basic_queries[$i]['func']); $info = $this->db_handler->get_query_info(); for($j=0; $j<sizeof($this->basic_queries[$i]['args']); $j++) { //Устанавливаем правильное значение ссылок (aid) в таблице api_args $this->basic_queries[$i]['args'][$j] = str_replace('?', $info['id'], $this->basic_queries[$i]['args'][$j]); $this->db_handler->query($this->basic_queries[$i]['args'][$j]); } } return; } }
Если есть утилиты, значит должен быть и драйвер. Если мы правильно подключим вышеприведенный код в index.php и выполним следующее:
//вместо localhost, root, root укажите ваши //правильные параметры для доступа к базе $db = new simple_db('localhost', 'root', 'root'); $util = new db_utility($db); $util->run(); $api_list_util = new api_list_utility($db); $api_list_util -> run();
то получим необходимую структуру базы, заполненную необходимыми данными. После того, как это произойдёт, мы этот код закомментируем, чтобы он нам не мешал 🙂 И index.php примет следующий вид:
$db = new simple_db('localhost', 'root', 'root'); $util = new db_utility($db); //$util->run(); $api_list_util = new api_list_utility($db); //$api_list_util -> run();
Как я писал выше, вызов $util->run(); может выполняться сколько угодно раз, но вызов $api_list_util -> run(); должен выполниться только один раз. Так что не стоит жмакать рефреш в браузере, а просто загляните в ваш навигатор MySQL. Я использую по-старинке стандартный консольный клиент.
Для нормальной работы с API, нам понадобится класс, который вернёт информацию обо всех API-функциях, хранимых в базе данных. Этот класс будет называться api_list и он будет имплементировать интерфейс api_list_itrfc.
api_list_itrfc определяет два метода:
interface api_list_itrfc { //получаем информацию обо всех api функциях, // либо только об одной, если она передана, как // необязательный параметр public function get_api($func=''); //проверяем существование функции в списке API-функций. public function api_func_exists($func_name=''); }
Класс api_list не будет содержать никаких открытых методов, кроме тех, которые определены интерфейсом api_list_itrfc. Посмотрим на этот класс внимательнее
class api_list extends api implements api_list_itrfc { //вызов родительского конструктора public function __construct($db_object) { parent::__construct($db_object); } // array get_api([string]) возвращаем список API-функций // в виде массива public function get_api($func='') { if(isset($func) && !empty($func)) { //Получаем массив только с одной функцией. return $this->get_api_func($func); } else { //Получаем массив всех функций. Структура массива // одинакова в обоих случаях. return $this->get_all_api(); } } //bool api_func_exists([string]) Проверяем функцию на существование public function api_func_exists($func_name='') { $array = array(); $array = $this->get_api($func=''); // Попробуйте использовать стандартный вызов in_array чтобы // оптимизировать этот метод. if(isset($func_name) && is_string($func_name) && !empty($func_name)) { for($i=0; $i<sizeof($array); $i++) { if($array[$i]['func']['func_name'] == $func_name) { return true; } } return false; } else { return false; } } // array get_all_api(void) - возвращаем массив всех API функций. protected function get_all_api() { $func_data = array(); $arg_data = array(); //данные из таблицы функций. $func_data = $this->db_handler->res_query('SELECT * '. 'FROM `'.$this->api_table.'`' . ''); //данные из таблицы аргументов. $arg_data = $this->db_handler->res_query('SELECT * '. 'FROM `'.$this->args_table.'`' . ''); // компилируем данные в массив одного вида return $this->api_list_array($func_data, $arg_data); } // array get_api_func(string) практически аналог предыдущей функции protected function get_api_func($func) { $func_data = array(); $arg_data = array(); $func_data = $this->db_handler->res_query('SELECT * '. 'FROM `'.$this->api_table.'`' . 'WHERE func_name = "'.$func.'"'); $arg_data = $this->db_handler->res_query('SELECT * '. 'FROM `'.$this->args_table.'`' . 'WHERE aid = "'.$func_data[0]['id'].'" '); return $this->api_list_array($func_data, $arg_data); } //array api_list_array(array, array) компилирует из двух массивов // один такого формата, который мы и будем использовать. protected function api_list_array($func_data, $arg_data) { $res = array(); if((is_array($func_data) && !empty($func_data)) && (is_array($arg_data) && !empty($arg_data))) { for($i=0; $i<sizeof($func_data); $i++) { $res[$i]['func'] = array('func_name'=>$func_data[$i]['func_name'] ,'arg_num'=>$func_data[$i]['arg_num'] ,'ret_type'=>$func_data[$i]['ret_type'] ); for($j=0; $j<sizeof($arg_data); $j++) { if($func_data[$i]['id'] == $arg_data[$j]['aid']) { $res[$i]['args'][] = array('arg_name'=>$arg_data[$j]['arg_name'], 'arg_type'=>$arg_data[$j]['arg_type']); } } } return $res; } else { return false; } } }
Чтобы посмотреть, что получилось, сделайте в вашем index.php следующий вызов:
$api_list = new api_list($db); $x = $api_list->get_api('delete_user'); $y = $api_list->get_api(); echo vd($x).'<hr>'; //массив стандартного формата с одной API-функцией echo vd($y).'<hr>'; //массив стандартного формата со всми API-функциями
Если вы дочитали до этого места, значит вы дочитали до самого интересного =) Всё, что было выше, делалось только ради следующих двух классов. Итак, класс api_functions хранит в себе исполняемые API-функции и имплементирует интерфейс api_functions_itrfc. Интерфейс api_functions_itrfc содержит пока только одну функцию, но мы исправим эту ситуацию в следующих статьях на тему API:
interface api_functions_itrfc { //всё очень просто public function delete_user($id); }
А класс api_functions эту функцию и реализует.
class api_functions extends api implements api_functions_itrfc { public function __construct($db_object) { parent::__construct($db_object); } public function delete_user($id) { return $this->db_handler->query('DELETE FROM `'. $this->data_table.'` WHERE id = "'.$id.'"'); } }
Как вы догадались, мы будем наращивать наш функционал именно в этом классе. Надо отметить, что класс уязвим к SQL-инъекциям. Но об этом мы подумаем завтра (с)
Последний класс на сегодня – это как раз первый обещанный Executor 🙂 Посмотрим на него внимательнее
class api_executor extends api { protected $api_list = null; // в будущем договоримся инициализировать // переменную, которая ожидает объект protected $api_functions = null; // типом null //конструктор зависит от двух соседних объектов public function __construct($db_object) { parent::__construct($db_object); $this->api_list = new api_list($this->db_handler); $this->api_functions = new api_functions($this->db_handler); } // главная функция mixed execute(string [, array]) получает // имя функции API с необязательным набором аргументов // и возвращает либо false в случае проблем, либо результат // работы API функции. public function execute($func_name, $args=array()) { // проверяем, действительно ли сушествует // функция API с именем $func_name // и правильно ли переданны аргументы - по умолчанию массив. if($this->api_list->api_func_exists($func_name) && is_array($args)) { //если всё ок, то подготавливаем коллбэк $callback_array = array($this->api_functions, $func_name); //проверяем, может ли этот коллбэк быть вызванным if(is_callable($callback_array, true, $func_name)) { //и если всё ок, то вызываем с набором аргументов return call_user_func($callback_array, $args[0]); } else { return false; } } else { return false; } } }
Тут нужна небольшая ремарка. Вызов call_user_func с параметром $args[0] будет изменён на нормальное поведение в следующих частях статьи. Возвращаемое значение, кстати, тоже пока-что оставляет желать лучшего. Но, тем не менее, вы можете проверить как работает API совершив в index.php следующий вызов –
$api_executor = new api_executor($db); $x = $api_executor->execute("delete_user", array(3)); echo vd($x);
Только убедитесь, что юзер с id = 3 действительно существует.
Ну что ж, подведём итоги.
Мы реализовали набор вспомогательных классов для реализации собственного API. Среди них два класса-утилиты, один класс для чтения всего API из базы, один класс, который превращает имена API-функций из слов в дело и один класс, который будет использовать API Handler для выполнения API запросов.
В следующей статье мы реализуем до конца класс api_functions и обработаем напильником класс api_executor. А пока буду рад ответить на ваши вопросы и конструктивную критику.
В аттачменте вы найдёте полный набор кода для драйвера index.php и полный набор кода, который мы рассмотрел выше.
Всем удачи 😉
В тему: