Статьи

Пользовательские таблицы базы данных: создание API

В первой части этой серии мы рассмотрели недостатки использования пользовательской таблицы. Одним из основных является отсутствие API: поэтому в этой статье мы рассмотрим, как его создать. API действует как слой между обработкой данных в вашем плагине и фактическим взаимодействием с таблицей базы данных — и в первую очередь предназначен для обеспечения безопасности таких взаимодействий и обеспечения «дружественной для человека» оболочки для вашей таблицы. Поэтому нам потребуются функции-оболочки для вставки, обновления, удаления и запроса данных.


Существует несколько причин, по которым рекомендуется API, но большинство сводится к двум связанным принципам: уменьшение дублирования кода и разделение задач.

С вышеупомянутыми четырьмя упомянутыми функциями-оболочками вам нужно только обеспечить безопасность запросов к базе данных в четырех местах — тогда вы можете полностью забыть о санации. Если вы уверены, что ваши функции-оболочки безопасно обрабатывают базу данных, вам не нужно беспокоиться о данных, которые вы им предоставляете. Вы также можете проверить данные — вернуть ошибку, если что-то не так.

Идея состоит в том, что без этих функций вам необходимо убедиться, что каждый экземпляр взаимодействия с вашей базой данных делает это безопасно. Это просто увеличивает вероятность того, что в одном из этих случаев вы что-то упустите и создадите уязвимость в вашем плагине.

Это связано с первым пунктом (и оба связаны с дублированием кода). Благодаря дублированию кода появляется больше возможностей для сканирования ошибок. И наоборот, используя функции-оболочки — если есть ошибка с обновлением или запросом таблицы базы данных — вы точно знаете, где искать.

Это может показаться «мягкой» причиной — но читабельность кода невероятно важна. Читаемость заключается в том, чтобы сделать логику и действия кода понятными для читателя. Это не просто важно, когда вы работаете в команде или когда кто-то может унаследовать вашу работу: вы можете знать, что ваш код должен делать сейчас, но через шесть месяцев вы, вероятно, забудете. А если за вашим кодом трудно следовать, проще ввести ошибку.

Функции Wrapper очищают ваш код, буквально отделяя внутреннюю работу какой-либо операции (скажем, создание сообщения) от контекста этой операции (например, обработку отправки формы). Просто представьте, что вместо каждого экземпляра, в котором вы используете wp_insert_post() все содержимое wp_insert_post() .

Добавление слоев абстракции не всегда хорошо, но, несомненно, так оно и есть. Эти обертки предоставляют не только удобный для пользователя способ обновления или запроса к таблице (представьте, что вам нужно использовать SQL для запроса сообщений, а не гораздо более краткий WP_Query() — и всю SQL-формулировку и очистку, которые идут с ним), но также помогает защитить вас и других разработчиков от изменений в базовой структуре базы данных.

Используя функции-оболочки, вы, как и третьи лица, можете использовать их, не опасаясь, что они небезопасны или сломаются. Если вы решите переименовать столбец, переместить столбец в другое место или даже удалить его, вы можете быть уверены, что остальная часть вашего плагина не сломается, потому что вы просто вносите необходимые изменения в свои функции-оболочки. (Между прочим, это веская причина избегать прямых SQL-запросов к таблицам WordPress: если они изменятся и будут, это сломает ваш плагин.) С другой стороны, API помогает стабильно расширять ваш плагин.

Возможно, я виновен в том, что разделил здесь одну точку на две, но я чувствую, что это важное преимущество. При разработке плагинов есть немного хуже, чем несогласованность: это только поощряет грязный код. Функции Wrapper обеспечивают согласованное взаимодействие с базой данных: вы предоставляете данные, и они возвращают true (или ID) или false (или объект WP_Error , если вы предпочитаете).


Надеюсь, теперь я убежден в необходимости API для вашей таблицы. Но прежде чем мы пойдем дальше, мы сначала определим вспомогательную функцию, которая облегчит дезинфекцию.

Мы определим функцию, которая возвращает столбцы таблицы вместе с ожидаемым форматом данных. Делая это, мы можем легко внести в белый список разрешенные столбцы и соответствующим образом отформатировать ввод. Кроме того, если мы вносим какие-либо изменения в столбцы, нам просто нужно внести изменения здесь

01
02
03
04
05
06
07
08
09
10
function wptuts_get_log_table_columns(){
    return array(
        ‘log_id’=> ‘%d’,
        ‘user_id’=> ‘%d’,
        ‘activity’=>’%s’,
        ‘object_id’=>’%d’,
        ‘object_type’=>’%s’,
        ‘activity_date’=>’%s’,
    );
}

Самая базовая функция-обертка ‘insert’ просто берет массив пар столбец-значение и вставляет их в базу данных. Это не обязательно должно быть так: вы можете решить предоставить более «дружественные человеку» ключи, которые вы затем сопоставите с именами столбцов. Вы также можете решить, что некоторые значения генерируются автоматически или переопределяются на основе переданных значений (например: статус wp_insert_post() в wp_insert_post() ).

Это может быть * значения *, которые требуют сопоставления. Формат, в котором лучше всего хранятся данные, не всегда является самым удобным форматом для использования. Например, для дат может быть проще обработать объект DateTime или метку времени, а затем преобразовать его в желаемый формат даты.

Функция-обертка может быть простой или сложной, но минимум, который она должна сделать, — это очистить входные данные. Я также рекомендую белый список для распознанных столбцов, поскольку попытка вставить данные в несуществующий столбец может вызвать ошибку.

В этом примере идентификатор пользователя по умолчанию совпадает с идентификатором текущего пользователя, и все поля задаются именем столбца, что является исключением из даты действия, которая передается как «дата». Дата в этом примере должна быть локальной меткой времени, которая преобразуется перед добавлением ее в базу данных.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
 * Inserts a log into the database
 *
 *@param $data array An array of key => value pairs to be inserted
 *@return int The log ID of the created activity log.
*/
function wptuts_insert_log( $data=array() ){
    global $wpdb;
 
    //Set default values
    $data = wp_parse_args($data, array(
                 ‘user_id’=> get_current_user_id(),
                 ‘date’=> current_time(‘timestamp’),
    ));
 
    //Check date validity
    if( !is_float($data[‘date’]) || $data[‘date’] <= 0 )
        return 0;
 
    //Convert activity date from local timestamp to GMT mysql format
    $data[‘activity_date’] = date_i18n( ‘Ymd H:i:s’, $data[‘date’], true );
 
    //Initialise column format array
    $column_formats = wptuts_get_log_table_columns();
 
    //Force fields to lower case
    $data = array_change_key_case ( $data );
 
    //White list columns
    $data = array_intersect_key($data, $column_formats);
 
    //Reorder $column_formats to match the order of columns given in $data
    $data_keys = array_keys($data);
    $column_formats = array_merge(array_flip($data_keys), $column_formats);
 
    $wpdb->insert($wpdb->wptuts_activity_log, $data, $column_formats);
 
    return $wpdb->insert_id;
}
Совет. Также рекомендуется проверить достоверность данных. Какие проверки вы должны выполнить и как API реагирует, полностью зависит от вашего контекста. wp_insert_post() требует определенной степени уникальности для публикации слагов — если есть конфликты, он автоматически генерирует уникальный. wp_insert_term другой стороны, wp_insert_term возвращает ошибку, если термин уже существует. Это связано с тем, как WordPress обрабатывает эти объекты, и семантикой.

Обновление данных обычно имитирует вставку данных — за исключением того, что указывается идентификатор строки (обычно это просто первичный ключ) вместе с данными, которые необходимо обновить. В общем случае аргументы должны соответствовать функции вставки (для согласованности) — поэтому в этом примере вместо date_date используется «date»

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
 * Updates an activity log with supplied data
 *
 *@param $log_id int ID of the activity log to be updated
 *@param $data array An array of column=>value pairs to be updated
 *@return bool Whether the log was successfully updated.
*/
function wptuts_update_log( $log_id, $data=array() ){
    global $wpdb;
 
    //Log ID must be positive integer
    $log_id = absint($log_id);
    if( empty($log_id) )
         return false;
 
    //Convert activity date from local timestamp to GMT mysql format
    if( isset($data[‘activity_date’]) )
         $data[‘activity_date’] = date_i18n( ‘Ymd H:i:s’, $data[‘date’], true );
 
 
    //Initialise column format array
    $column_formats = wptuts_get_log_table_columns();
 
    //Force fields to lower case
    $data = array_change_key_case ( $data );
 
    //White list columns
    $data = array_intersect_key($data, $column_formats);
 
    //Reorder $column_formats to match the order of columns given in $data
    $data_keys = array_keys($data);
    $column_formats = array_merge(array_flip($data_keys), $column_formats);
 
    if ( false === $wpdb->update($wpdb->wptuts_activity_log, $data, array(‘log_id’=>$log_id), $column_formats) ) {
         return false;
    }
 
    return true;
}

Функция-обертка для запроса данных часто бывает довольно сложной — особенно потому, что вы можете поддерживать все типы запросов, которые выбирают только определенные поля, ограничивают с помощью операторов AND или OR, упорядочивают по одному из нескольких возможных столбцов и т. Д. (Просто смотрите класс WP_Query ).

Основной принцип функции-оболочки для запроса данных заключается в том, что если он должен взять «массив запросов», интерпретировать его и сформировать соответствующий оператор SQL.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/**
 * Retrieves activity logs from the database matching $query.
 * $query is an array which can contain the following keys:
 *
 * ‘fields’ — an array of columns to include in returned roles.
 * ‘orderby’ — datetime, user_id or log_id.
 * ‘order’ — asc or desc
 * ‘user_id’ — user ID to match, or an array of user IDs
 * ‘since’ — timestamp.
 * ‘until’ — timestamp.
 *
 *@param $query Query array
 *@return array Array of matching logs.
*/
function wptuts_get_logs( $query=array() ){
 
     global $wpdb;
     /* Parse defaults */
     $defaults = array(
       ‘fields’=>array(),’orderby’=>’datetime’,’order’=>’desc’, ‘user_id’=>false,
       ‘since’=>false,’until’=>false,’number’=>10,’offset’=>0
     );
 
    $query = wp_parse_args($query, $defaults);
 
    /* Form a cache key from the query */
    $cache_key = ‘wptuts_logs:’.md5( serialize($query));
    $cache = wp_cache_get( $cache_key );
 
    if ( false !== $cache ) {
            $cache = apply_filters(‘wptuts_get_logs’, $cache, $query);
            return $cache;
    }
 
     extract($query);
 
    /* SQL Select */
    //Whitelist of allowed fields
    $allowed_fields = wptuts_get_log_table_columns();
     
    if( is_array($fields) ){
        //Convert fields to lowercase (as our column names are all lower case — see part 1)
        $fields = array_map(‘strtolower’,$fields);
 
            //Sanitize by white listing
           $fields = array_intersect($fields, $allowed_fields);
    }else{
        $fields = strtolower($fields);
    }
 
    //Return only selected fields.
    if( empty($fields) ){
        $select_sql = «SELECT* FROM {$wpdb->wptuts_activity_log}»;
    }elseif( ‘count’ == $fields ) {
        $select_sql = «SELECT COUNT(*) FROM {$wpdb->wptuts_activity_log}»;
    }else{
        $select_sql = «SELECT «.implode(‘,’,$fields).» FROM {$wpdb->wptuts_activity_log}»;
    }
 
     /*SQL Join */
     //We don’t need this, but we’ll allow it be filtered (see ‘wptuts_logs_clauses’ )
     $join_sql=»;
 
    /* SQL Where */
    //Initialise WHERE
    $where_sql = ‘WHERE 1=1’;
 
    if( !empty($log_id) )
       $where_sql .= $wpdb->prepare(‘ AND log_id=%d’, $log_id);
 
    if( !empty($user_id) ){
 
       //Force $user_id to be an array
       if( !is_array( $user_id) )
           $user_id = array($user_id);
 
       $user_id = array_map(‘absint’,$user_id);
       $user_id__in = implode(‘,’,$user_id);
       $where_sql .= » AND user_id IN($user_id__in)»;
    }
 
    $since = absint($since);
    $until = absint($until);
 
    if( !empty($since) )
       $where_sql .= $wpdb->prepare(‘ AND activity_date >= %s’, date_i18n( ‘Ymd H:i:s’, $since, true));
 
    if( !empty($until) )
       $where_sql .= $wpdb->prepare(‘ AND activity_date <= %s’, date_i18n( ‘Ymd H:i:s’, $until, true));
 
    /* SQL Order */
    //Whitelist order
    $order = strtoupper($order);
    $order = ( ‘ASC’ == $order ? ‘ASC’ : ‘DESC’ );
 
    switch( $orderby ){
       case ‘log_id’:
            $order_sql = «ORDER BY log_id $order»;
       break;
       case ‘user_id’:
            $order_sql = «ORDER BY user_id $order»;
       break;
       case ‘datetime’:
             $order_sql = «ORDER BY activity_date $order»;
       default:
       break;
    }
 
    /* SQL Limit */
    $offset = absint($offset);
    if( $number == -1 ){
         $limit_sql = «»;
    }else{
         $number = absint($number);
         $limit_sql = «LIMIT $offset, $number»;
    }
 
    /* Filter SQL */
    $pieces = array( ‘select_sql’, ‘join_sql’, ‘where_sql’, ‘order_sql’, ‘limit_sql’ );
    $clauses = apply_filters( ‘wptuts_logs_clauses’, compact( $pieces ), $query );
    foreach ( $pieces as $piece )
          $$piece = isset( $clauses[ $piece ] ) ?
 
    /* Form SQL statement */
    $sql = «$select_sql $where_sql $order_sql $limit_sql»;
 
    if( ‘count’ == $fields ){
        return $wpdb->get_var($sql);
    }
 
    /* Perform query */
    $logs = $wpdb->get_results($sql);
 
    /* Add to cache and filter */
    wp_cache_add( $cache_key, $logs, 24*60*60 );
    $logs = apply_filters(‘wptuts_get_logs’, $logs, $query);
    return $logs;
 }

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

Вы можете считать ваши запросы достаточно сложными или повторяться регулярно, чтобы иметь смысл кэшировать результаты. Поскольку разные запросы будут давать разные результаты, мы, очевидно, не хотим использовать общий ключ кэша — нам нужен ключ, который уникален для этого запроса. Это именно то, что делает следующее. Он сериализует массив запроса, а затем хэширует его, создавая ключ, уникальный для $query :

1
$cache_key = ‘wptuts_logs:’.md5( serialize($query));

Затем мы проверяем, есть ли у нас что-то для этого ключа кеша — если так, отлично, мы просто возвращаем его содержимое. Если нет, мы генерируем SQL, выполняем запрос, а затем добавляем результаты в кэш (не более 24 часов) и возвращаем их. Нам нужно помнить, что записи в результатах этой функции могут занять до 24 часов. Обычно существуют контексты, в которых кэш очищается автоматически, но нам необходимо реализовать их.

Крюки были широко освещены на WPTuts + недавно Томом Макфарлином и Пиппином Уильямсоном . В своей статье Пиппин рассказывает о причинах, по которым вы должны сделать свой код расширяемым с помощью хуков, а обертки, такие как wptuts_get_logs() служат отличными примерами того, где их можно использовать.

Мы использовали два фильтра в вышеуказанной функции:

  • wptuts_get_logs — фильтрует результат функции
  • wptuts_logs_clauses — фильтрует массив компонентов SQL

Это позволяет сторонним разработчикам или даже нам самим использовать предоставленный API. Если мы избегаем использования прямого SQL в нашем плагине и используем только те функции-оболочки, которые мы создали, то это сразу же позволяет расширить наш плагин. В wptuts_logs_clauses фильтр wptuts_logs_clauses позволит разработчикам изменять каждую часть SQL — и, таким образом, выполнять сложные запросы. Отметим, что работа любого плагина, использующего эти фильтры, чтобы убедиться, что возвращаемые данные должным образом очищены.

Хуки также полезны при выполнении трех других «операций»: вставка, обновление и удаление данных. Действия позволяют подключаемым модулям знать, когда они выполняются, поэтому они выполняют некоторые действия. В нашем контексте это может означать отправку электронного письма администратору, когда конкретный пользователь выполняет определенное действие. Фильтры в контексте этих операций полезны для изменения данных перед их вставкой.

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

  • Сообщается, когда вызывается ловушка или что она делает (например, вы можете догадаться, что могут делать pre_get_posts и user_has_cap .
  • Быть уникальным Рекомендуется использовать префикс ловушки с именем вашего плагина. В отличие от функций, не будет ошибки, если есть конфликт между именами хуков — вместо этого он, вероятно, просто «тихо» сломает один или несколько плагинов.
  • Экспонаты какой-то структуры. Сделайте ваши хуки предсказуемыми и избегайте именования хуков «на лету», так как это иногда может привести к случайным именам хуков. Вместо этого спланируйте как можно раньше возможные крючки и придумайте соответствующее соглашение об именах — и придерживайтесь его.
Подсказка. Как правило, рекомендуется имитировать те же соглашения, что и WordPress, поскольку разработчики быстрее поймут, что делает этот хук. Что касается использования имени плагина в качестве префикса: если ваше имя плагина является общим, этого может быть недостаточно для обеспечения уникальности. Наконец, не присваивайте действие и фильтр с тем же именем.

Удаление данных часто является самой простой из оболочек, хотя может потребоваться выполнить некоторые операции «очистки», а также просто удалить данные. wp_delete_post() не только удаляет запись из таблицы *_posts но также удаляет соответствующую мета-запись, отношения таксономии, комментарии и ревизии и т. д.

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

  • _delete_ запускается перед удалением
  • _deleted_ запускается после удаления
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * Deletes an activity log from the database
 *
 *@param $log_id int ID of the activity log to be deleted
 *@return bool Whether the log was successfully deleted.
*/
function wptuts_delete_log( $log_id ){
    global $wpdb;
 
    //Log ID must be positive integer
    $log_id = absint($log_id);
    if( empty($log_id) )
         return false;
 
    do_action(‘wptuts_delete_log’,$log_id);
    $sql = $wpdb->prepare(«DELETE from {$wpdb->wptuts_activity_log} WHERE log_id = %d», $log_id);
 
    if( !$wpdb->query( $sql ) )
         return false;
 
    do_action(‘wptuts_deleted_log’,$log_id);
 
    return true;
}

Я немного ленив с исходной документацией вышеупомянутого API. В этой серии Том Макфарлин объясняет, почему вы не должны быть. Возможно, вы потратили много времени на разработку своих функций API, но если другие разработчики не знают, как их использовать — они не будут. Вы также будете помогать себе, когда через 6 месяцев вы забудете, как следует предоставлять данные или что вы ожидаете получить.


Оболочки для вашей таблицы базы данных могут варьироваться от относительно простых (например, get_terms() ) до чрезвычайно сложных (например, класс WP_Query ). В совокупности они должны стремиться служить воротами к вашему столу: позволяя вам сосредоточиться на контексте, в котором они используются, и по существу забыть, что они на самом деле делают. API, который вы создаете, является лишь небольшим примером понятия «разделения интересов», которое часто приписывают Эдсгеру В. Дейкстре в его статье « О роли научной мысли :

Это то, что я иногда называю «разделением интересов», которое, хотя и не совсем возможно, но пока является единственным доступным методом эффективного упорядочения своих мыслей, о котором я знаю. Это то, что я имею в виду под «сосредоточением своего внимания на каком-либо аспекте»: это не означает игнорирование других аспектов, это просто воздание должного тому факту, что с точки зрения этого аспекта другой не имеет значения. Это одно- и многодорожечный одновременно.

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