Создаём собственный API – 2

Кирилл Евсеев, February 14, 2011

Всем привет :)

Ну что ж, как я и обещал, сегодня мы создадим класс 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.

Иерархия 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).

api_db

Задача класса 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 будет состоять из следующих функций:

  1. array get_user(int) – возвращаем инфо о пользователе из таблицы data по его уникальному ключу
  2. bool delete_user(int) – удаляем пользователя из таблицы data по его уникальному ключу
  3. bool add_user(string, string) – добавляем пользователя (имя, фамилия) в таблицу data
  4. bool add_users(array) – добавляем несколько пользователей в таблицу data. Аналогично add_user
  5. array get_users(void) – возвращаем всех пользоваталей из таблицы data. Будем считать для однозначности, что функция получает один параметр – void
  6. array get_users_or(array) – возвращаем всех пользователей, удовлетворяющих какому-то условию
  7. 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 и полный набор кода, который мы рассмотрел выше.

Всем удачи ;)

В тему:

0комментариев
Ваше имя
Ваш email*
Ваш сайт
Текст вашего комментария:

Поиск по блогу:
Подписаться:
Популярные:
Облако тегов:
Разное:
Счетчик: