元数据就是关于数据的信息,比如一张图片的大小、种类、创建时间、尺寸等信息,在 WordPress 中,元数据指的是文章、用户、评论和分类法项目的附加信息。

举个例子,我们创建了类型为 “商品” 的自定义文章类型,其中包含一个价格元数据字段,这个字段存储在 postmeta 文章元数据表中。

WordPress 的主要数据(文章、评论、用户、分类法项目)和他们元数据是一对多关系,一个主要数据可以有很多元数据,所以,我们可以在元数据中存储很多附加数据。这就是 WordPress 数据结构的灵活性所在。

本章节我们将讨论 管理文章元数据, 自定义元数据盒子, 渲染文章元数据

# 9.1 管理文章元数据

# 添加元数据

我们可以使用 add_post_meta() 函数添加元数据,该函数接受一个 post_id、一个 meta_key、一个 meta_value 和一个 unique 标志。

meta_key 是插件在其他地方引用 meta_value 的依据,我们可以使用类似 ‘mymetakeyname’ 或其他任意字符串作为名称,但是为了避免冲突和语义化,建议在名称前面加上前缀,并以下划线分隔名称中的单词,如 mla_featured_menu,需要注意的是,同一个 meta_key 可以被多次使用来存储各种各样的元数据(见下面的唯一标志)。

meta_value 可以是一个字符串、整数、数组或对象。如果 meta_value 是一个数组或对象,WordPress 会在存储到数据库之前自动序列化他们,并在从数据库中获取之前反序列化。

unique 标志可以让我们声明某条元数据相对于一条主数据是否为唯一的,一个非唯一的 meta_key 相对于一条主数据可以有多个值,比如 ‘price’ 这个 meta_key,一个产品的价格是唯一的,所以我们应该为这条元数据添加 unique 标记,以确保一个产品只有一个价格。

# 更新元数据

如果需要更新一条已经存在的元数据,我们可以使用 update_post_meta(), 如果我们使用这个函数更新了一条不存在的元数据,该函数自动调用 add_post_meta() 来帮我们添加这条数据,该函数的参数和 add_post_meta() 一样。

# 删除元数据

如果我们需要删除一条元数据,使用 delete_post_meta() 函数,该函数接受一个 post_id,一个 meta_key 和一个可选的 meta_value。

# 字符转义

WordPress 在存储文章元数据的时候会使用 stripslashes() 转义数据中的某些字符串,因此在传入可能包含 \ 转义字符串的时候(例如 JSON )需要注意。比如一个 JSON 字符串 {key:value with \escaped quotes}:

<?php
$escaped_json = '{key:value with \escaped quotes\}';
update_post_meta($id, 'escaped_json', $escaped_json);
$broken = get_post_meta($id, 'escaped_json', true);
/*
$broken, after stripslashes(), ends up unparsable:
{key:value with escaped quotes}
*/
1
2
3
4
5
6
7
8

解决办法

通过使用函数 wp_slash(),添加一个转义级别,用来补偿 stripslashes() 的转义。

<?php
$escaped_json = '{key:value with \escaped quotes\}';
update_post_meta($id, 'double_escaped_json', wp_slash($escaped_json));
$fixed = get_post_meta($id, 'double_escaped_json', true);
/*
$fixed, after stripslashes(), ends up as desired:
{key:value with \escaped quotes\}
*/
1
2
3
4
5
6
7
8

# 隐藏的自定义字段

如果我们打算在插件或主题中使用自定义字段来存储数据,请务必注意,WordPress 不会在自定义字段编辑界面显示以下划线 “_” 开头的 meta_key。我们可以使用 add_meta_box() 函数来添加元数据盒子以显示这些隐藏的自定义字段。

下面的例子将添加一个 meta_key 为 “_color” 的隐藏自定义字段,该字段的值为 “red”,这个字段不会在 WordPress 默认的自定义字段编辑界面中显示。

<?php
add_post_meta( 68, '_color', 'red', true );
1
2

# 隐藏数组

另外,如果 meta_value 是一个数组,即便 meta_key 没有下划线,这条元数据也不会在自定义字段编辑界面上显示。因为数据库中保存的数据是被转义过的,我们很难去编辑一条被转义过的数据。

# 9.2 自定义元数据盒子

# 什么是元数据盒子

用户编辑文章时,编辑界面由多个默认盒子组成:编辑器、发布、分类目录、标签等,这些都是元数据盒子。主题和插件也可以添加自定义元数据盒子到任何一个文章类型编辑界面。自定义元数据盒子的内容通常是 HTML 表单元素,用户通过表单输入主题或插件需要的数据,除了表单,元数据盒子的内容也可以是任何我们需要的 HTML。

# 为什么要使用元数据盒子

元数据盒子是方便、灵活的模块化文章数据编辑元素,可以让用户很方便的编辑当前文章的相关信息,我们的自定义元数据盒子和默认的文章信息显示在一个界面上,他们之间的从属和相关关系很清晰。

如果用户不需要某个元数据盒子,他们可以很方便的将其隐藏,根据需要,用户也可以对元数据盒子排序,把自己经常使用的元数据盒子放在合适的位置。

# 添加元数据盒子

如果我们需要创建一个元数据盒子,调用 add_meta_box() 函数并将执行该调用的函数挂载到 add_meta_boxes Action 钩子中即可。下面的示例在文章和 mla_cpt 自定义文章类型编辑界面上添加了一个元数据盒子。

<?php
function mla_add_custom_box() {
   $screens = ['post', 'mla_cpt'];
   foreach ($screens as $screen) {
      add_meta_box(
         'mla_box_id',           // Unique ID
         'Custom Meta Box Title',  // Box title
         'mla_custom_box_html',  // Content callback, must be of type callable
         $screen                   // Post type
      );
   }
}
add_action('add_meta_boxes', 'mla_add_custom_box');
1
2
3
4
5
6
7
8
9
10
11
12
13

上面的 mla_custom_box_html 是一个回调函数,用来显示元数据盒子需要的 HTML 表单,该函数示例代码如下:

<?php
function mla_custom_box_html($post) {
   ?>
   <label for=mla_field>Description for this field</label>
   <select name=mla_field id=mla_field class=postbox>
      <option value=>Select something...</option>
      <option value=something>Something</option>
      <option value=else>Else</option>
   </select>
   <?php
}
1
2
3
4
5
6
7
8
9
10
11
请注意,上面的表单中没有提交按钮。元数据盒子的 HTML 包含在编辑界面的表单标签内,当用户点击文章的发布或更新按钮时,所有元数据盒子内表单的数据都会被一起报保存。

上面的示例只包一个下拉列表字段,在实际开发时,我们可以根据需要添加任意类型的表单字段,如果需要添加的字段很多,可以考虑按照数据上的相似性把这些字段分组,这样可以让界面更加清晰和容易使用。

# 获取自定义字段值作为表单默认值

如果我们已经保存了自定义字段,显示表单的时候,我们需要把已经保存的值作为表单的默认值和表单一起显示,我们可以使用 get_post_meta() 函数获取这个值。下面的示例使用自定义表单的值为下拉选择表单添加了已选择的状态。

<?php
function mla_custom_box_html($post) {
   $value = get_post_meta($post->ID, '_mla_meta_key', true);
   ?>
   <label for=mla_field>Description for this field</label>
   <select name=mla_field id=mla_field class=postbox>
      <option value=>Select something...</option>
      <option value=something <?php selected($value, 'something'); ?>>Something</option>
      <option value=else <?php selected($value, 'else'); ?>>Else</option>
   </select>
   <?php
}
1
2
3
4
5
6
7
8
9
10
11
12

上面代码中,selected() 是一个非常有用的辅助函数,用来帮助我们设置已选中的选项数据,关于该函数的详细介绍请参考: selected() 函数文档。

# 保存自定字段值

当我们保存或更新一篇文章的时候,会触发几个 Action,我们可以根据需要在任何一个 Action 被触发时保存自定义字段中输入的值。在这个例子中,我们使用 save_post Action 钩子,根据情况不同,有时候选择其他钩子可能更合适。需要注意的是,更新文章的时候 save_post 可能会被触发多次,在保存数据的时候,我们需要照顾到这种情况。

下面的示例把保存文章时题传递过来的 mla_field 数据存储到了 _mla_meta_key 这个隐藏 post_meta 中。

<?php
function mla_save_postdata($post_id) {
   if (array_key_exists('mla_field', $_POST)) {
      update_post_meta(
         $post_id,
         '_mla_meta_key',
         $_POST['mla_field']
      );
   }
}
add_action('save_post', 'mla_save_postdata');
1
2
3
4
5
6
7
8
9
10
11

上面的示例代码没有做必要的安全检查,在实际的代码中,千万不要忘记。

# 幕后发生的事情

我们通常不需要关心幕后发生的事情,为了内容的完整,这里简单的说明一下。

WordPress 显示文章编辑界面的时候,会调用 do_meta_boxes() 函数遍历所有元数据盒子,并调用元数据盒子中 callback 参数指定的函数来显示表单,显示表单内容之前,会加上需要的标记(如 div titles 等)。

# 移除元数据盒子

如果我们需要从编辑界面移除现有元数据盒子,可以使用 remove_meta_box() 函数,传递的参数需要和添加元数据盒子时候的对应参数完全一致。如果需要删除默认的元数据盒子,可以查看 wp-includes/edit-form-advanced.php 文件中添加默认元数据盒子的相关代码获取需要的参数。

# 面向对象

到目前为止,我们一直在使用面向过程的方法来添加元数据盒子,我们还可以使用其他编程方法来添加。

使用面向对象的方法来添加元数据盒子非常简单,并且我们不用担心在全局命名空间中的命名冲突。为了节省内存并使实现更简单,下面示例中使用具有静态方法的抽象类。

<?php
abstract class Mla_Meta_Box {
   public static function add() {
      $screens = ['post', 'mla_cpt'];
      foreach ($screens as $screen) {
         add_meta_box(
            'mla_box_id',          // Unique ID
            'Custom Meta Box Title', // Box title
            [self::class, 'html'],   // Content callback, must be of type callable
            $screen                  // Post type
         );
      }
   }

   public static function save($post_id) {
      if (array_key_exists('mla_field', $_POST)) {
         update_post_meta(
            $post_id,
            '_mla_meta_key',
            $_POST['mla_field']
         );
      }
   }

   public static function html($post) {
      $value = get_post_meta($post->ID, '_mla_meta_key', true);
      ?>
      <label for=mla_field>Description for this field</label>
      <select name=mla_field id=mla_field class=postbox>
         <option value=>Select something...</option>
         <option value=something <?php selected($value, 'something'); ?>>Something</option>
         <option value=else <?php selected($value, 'else'); ?>>Else</option>
      </select>
      <?php
   }
}

add_action('add_meta_boxes', ['Mla_Meta_Box', 'add']);
add_action('save_post', ['Mla_Meta_Box', 'save']);
1
2
3
4
5
6
7
8
9
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

# Ajax

由于元数据盒子的 HTML 元素位于编辑文章的表单之内,在默认情况下,元数据盒子里面的数据会在保存文章的时候和文章数据一起存储在 $_POST 超级变量中提交。根据需要,我们可以使用 AJAX 来提升用户体验,让元数据盒子里面的数据单独提交,不管用户是否保存了文章。

定义一个触发器

首先,我们需要定义一个触发事情,这个事件可以是点击了每个链接、改变了某个值或者其他任何 JavaScript 事件。在下面的例子中,我们将 change 定义为执行 Ajax 请求的触发事件。

(function ($, window, document) {
   'use strict';
   // execute when the DOM is ready
   $(document).ready(function () {
      // js 'change' event triggered on the mla_field form field
      $('#mla_field').on('change', function () {
         // our code
      });
   });
}(jQuery, window, document));
1
2
3
4
5
6
7
8
9
10

# 客户端代码

接下来,我们需要定义触发器,也就是说,我们需要编写 AJAX 客户端代码。在下面的例子中,我们将发起 POST 请求,服务端响应成功或者失败,来判断 mla_field 是否有效。

(function ($, window, document) {
   'use strict';
   // execute when the DOM is ready
   $(document).ready(function () {
      // js 'change' event triggered on the mla_field form field
      $('#mla_field').on('change', function () {
         // jQuery post method, a shorthand for $.ajax with POST
         $.post(mla_meta_box_obj.url,                        // or ajaxurl
                   {
                       action: 'mla_ajax_change',               // POST data, action
                       mla_field_value: $('#mla_field').val() // POST data, mla_field_value
                   }, function (data) {
            // handle response data
            if (data === 'success') {
               // perform our success logic
            } else if (data === 'failure') {
               // perform our failure logic
            } else {
               // do nothing
            }
         }
            );
        });
   });
}(jQuery, window, document));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

下一步,我们将创建一个名为 mla_meta_box_obj 的自定义 JavaScript 对象,这个对象中包含了用于自定义 WrodPress AJAX 操作的 URL。

如果我们的元数据盒子只需要 WordPress 的 AJAX 文件 URL,我们可以不用创建一个新的自定义 JavaScript 对象,而是使用预定义的 ajaxurl 变量,这个变量仅在 WordPress 后台中可用。在执行任何逻辑之前先检查一下是否存在。

# 插入客户端代码

为了让上一步的代码可以访问到 mla_meta_box_obj 这个JavaScript 对象,我们需要把这个对象加入到前端代码中,在下面的示例中,我们将 AJAX 功能添加到了文章、mla_cpt 这两个文章类型的编辑界面。

JacaScript 文件所在的路径为/plugin-name/admin/meta-boxes/js/admin.js,plugin-name 是插件文件夹。

function mla_meta_box_scripts(){
   // get current admin screen, or null
   $screen = get_current_screen();
   // verify admin screen object
   if (is_object($screen)) {
      // enqueue only for specific post types
      if (in_array($screen->post_type, ['post', 'mla_cpt'])) {
         // enqueue script
         wp_enqueue_script('mla_meta_box_script', plugin_dir_url(__FILE__) . 'admin/meta-boxes/js/admin.js', ['jquery']);
         // localize script, create a custom js object
         wp_localize_script(
            'mla_meta_box_script',
            'mla_meta_box_obj',
            [
               'url' => admin_url('admin-ajax.php'),
            ]
         );
      }
   }
}
add_action('admin_enqueue_scripts', 'mla_meta_box_scripts');

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 服务端代码

最后一步是编写处理 AJAX 请求的服务端代码。

function mla_meta_box_ajax_handler() {
   if (isset($_POST['mla_field_value'])) {
      switch ($_POST['mla_field_value']) {
         case 'something':
            echo 'success';
            break;
         default:
            echo 'failure';
            break;
      }
   }
   // ajax handlers must die
   die;
}
// wp_ajax_ is the prefix, mla_ajax_change is the action we've used in client side code
add_action('wp_ajax_mla_ajax_change', 'mla_meta_box_ajax_handler');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

本页面的示例代码中缺乏足够的安全操作来保证安全,在实际的代码中,一定要把安全代码加进去。

# 9.3 渲染文章元数据

WordPress 提供了两个函数来帮助我们访问存储在 postmeta 数据表中的数据 get_post_meta()get_post_custom()

get_post_meta 函数获取指定文章的某个 meta_key 的数据,使用方法如下。

get_post_meta(
   int $post_id,
   string $key = '',
   bool $single = false
);
1
2
3
4
5

.e.g.

$mla_meta_value = get_post_meta(get_the_ID(), 'mla_meta_key');
1

get_post_custom 获取某个文章的所有元数据,使用方法如下:

get_post_custom(
   int $post_id
);
1
2
3

.e.g.

$meta_array = get_post_custom(get_the_ID());
1