Создание системы с контролируемым лицензированием: плагин License Manager

19 января 2018

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

Но как насчет обновлений?

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

Функциональность WordPress Updates была разработана с учетом только официальных плагинов WordPress и тематических каталогов, но не стоит беспокоиться: в WordPress есть действия и фильтры, которые позволяют нам подключиться к этой функции и добавить собственный сервер в смешивание.

Что вы узнаете

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

При создании плагина в этой первой части вы узнаете о следующих методах разработки плагинов WordPress:

Используйте плагин WordPress Plugin для быстрого запуска. Создание нового настраиваемого типа сообщений с мета-полем для хранения настраиваемых метаданных. Создание таблицы поддерживаемых баз данных для вашего плагина. Добавление элементов в эту таблицу базы данных. Удаление элементов с использованием собственной таблицы таблиц WordPress.

Если вы хотите увидеть плагин в действии, вы можете загрузить самую последнюю версию плагина из WordPress.org. Для точного исходного кода, используемого в этом учебнике, ознакомьтесь с связанным репозиторием Tuts + Github (справа). Мы пройдем каждый шаг создания плагина один за другим, но если вы не хотите строить все с нуля, то наряду с исходным кодом это хороший способ организовать ваше обучение.

Теперь давайте работать.

1. Создание плагина Использование плагинов-плагинов WordPress

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

Но есть лучший способ.

На этот раз вместо копирования существующего проекта или начала с нуля мы будем использовать TomPackPlayer WordPress Plugin Boilerplate, отправную точку, предназначенную для того, чтобы вы начали с хорошо структурированного объектно-ориентированного плагина WordPress в кратчайшие сроки.

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

Загрузите Boilerplate и сделайте это самостоятельно

Самый простой способ загрузить плагин-плагин - посетить веб-сайт проекта и загрузить последнюю версию в виде zip-файла.

После того, как вы загрузили и распаковали пакет zip, найдите в нем папку соединительной линии, скопируйте его в предпочтительное место и переименуйте его в wp-license-manager.

Затем пришло время просмотреть файлы шаблонов плагинов и сделать их собственными:

    Все всех файлов в каталоге плагина (и его подкаталогах), переименовав все вхождения имени плагина в wp- лицензионный менеджер и Plugin_Name для Wp_License_Manager. Обязательно переименуйте имена классов и их содержащие файлы. Это займет немного времени и ручную работу, поэтому использование PHP IDE, чтобы помочь в переименовании, является хорошей идеей. NДобавить информацию плагина в файл начальной загрузки плагина, который теперь называется wp-license-manager.php и находится в корневом каталоге плагина. Вы найдете информацию в блоке комментариев в верхней части файла сразу после тега PHPDoc @ wordpress-plugin.

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

Если вы хотите больше узнать об использовании шаблона, см. Учебную серию Tom McFarlin, «Разработка плагинов с помощью Boilerplates WordPress». Это основано на более ранней версии шаблона, но все равно даст вам более глубокое понимание идей, лежащих в основе плагина и работающего с ним.

2. Создание пользовательского типа сообщений для продуктов

Плагин для управления лицензиями довольно прост и будет хранить и использовать только два типа данных: продукты и лицензии. Поскольку продукты необходимы для создания лицензий (а продукты - из них проще), мы начнем с них.

Вот что вы будете строить в этом разделе:

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

Version: номер версии продукта. Это поле будет использоваться для проверки обновлений в частях 2 и 3 этой серии учебников.
Перегрузка: ведро Amazon S3, в котором хранится файл. Имя файла: имя файла zip, хранящегося в Amazon S3. Мы поговорим об этих параметрах файла во второй части серии.
Протестировано с версией WordPress, требует версии WordPress, Последнее обновление, Baer high и Baer low: эти параметры отображаются в интерфейсе обновления плагина в третьей части из серии уроков.

Теперь давайте создадим это, создав пользовательский тип сообщения и добавив в него мета-поле.

Шаг 1: Создать тип сообщения

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

Итак, найдите функцию define_public_hooks внутри класса Wp_License_Manager в include class-wp-license-manager.php и добавьте следующее новое действие сразу после двух уже действующих действий wp_enqueue_scripts:

$this->loader->add_action( 'init', $plugin_public, 'add_products_post_type' );

В этой строке используется действие шаблона и загрузчик фильтра, чтобы добавить обработчик действий в действие init Word. Функция, add_products_post_type, переходит в класс Wp_License_Manager_Public.

Здесь находится вся функция:

/**
 * Register the new "products" post type to use for products that
 * are available for purchase through the license manager.
 */
public function add_products_post_type() {
    register_post_type( 'wplm_product',
        array(
            'labels' => array(
                'name' => __( 'Products', $this->plugin_name ),
                'singular_name' => __( 'Product', $this->plugin_name ),
                'menu_name' => __( 'Products', $this->plugin_name ),
                'name_admin_bar' => __( 'Products', $this->plugin_name ),
                'add_new' => __( 'Add New', $this->plugin_name ),
                'add_new_item' => __( 'Add New Product', $this->plugin_name ),
                'edit_item' => __( 'Edit Product', $this->plugin_name ),
                'new_item' => __( 'New Product', $this->plugin_name ),
                'view_item' => __( 'View Product', $this->plugin_name ),
                'search_item' => __( 'Search Products', $this->plugin_name ),
                'not_found' => __( 'No products found', $this->plugin_name ),
                'not_found_in_trash' => __( 'No products found in trash', $this->plugin_name ),
                'all_items' => __( 'All Products', $this->plugin_name ),
            ),
            'public' => true,
            'has_archive' => true,
            'supports' => array( 'title', 'editor', 'author', 'revisions', 'thumbnail' ),
            'rewrite' => array( 'slug' => 'products' ),
            'menu_icon' => 'dashicons-products',
        )
    );
}

Давайте рассмотрим функцию и что она делает - другими словами, параметры для функции WordPress register_post_type.

Первый параметр $ post_type (строка 6) определяет идентификатор типа сообщения, используемого в ссылках администратора WordPress и запросах сообщений такого типа (я пошел с wplm_product, чтобы он не сталкивался с типами сообщений, созданных другими плагинами и темы).

Второй параметр (строки 7-28) - это массив, который определяет свойства типа сообщения (для полного списка параметров, которые вы можете использовать, проверьте WordPress Codex):

labels определяет набор строк метки, используемые для обращения к типу post в области администрирования WordPress. public определяет видимость сообщений этого нового типа. Я хотел, чтобы продукты просматривались посетителями сайта, поэтому я установил это как истину. Если, с другой стороны, вы хотите, чтобы продукты были закрытыми, просто установите для этого параметра значение false.
has_archive определяет, служит ли WordPress для страницы архива для публикации сообщений этого типа или нет.
supports определяет, какие функции редактирования сообщений отображаются в редакторе сообщений. rewrite определяет, как будут выглядеть постоянные ссылки для этого типа продукта. menu_icon определяет значок, который будет использоваться для типа сообщения в меню администратора. Посетите сайт разработчика WordPress, чтобы получить полный список доступных значков панели инструментов.

Листинг, добавление и редактирование продуктов теперь обрабатываются WordPress. Нам осталось только добавить мета-окно с указанными выше пользовательскими настройками.

Шаг 2. Добавление мета-поля для информации о продукте

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

Вот как будет выглядеть мета-окно:

Сначала давайте выберем действие для добавления мета-поля. add_meta_boxes_ {post_type} - отличный вариант: просто добавьте тип post вместо {post_type}, и ваше действие будет вызвано, когда придет время добавить мета-поля для сообщения этого типа (в нашем случае wplm_product),

Как мы работаем над областью администратора, добавьте следующий код в функцию define_admin_hooks () внутри основного класса плагина Wp_License_Manager:

$this->loader->add_action( 'add_meta_boxes_wplm_product', $plugin_admin, 'add_product_information_meta_box' );

Функция, add_product_information_meta_box () в Wp_License_Manager_Admin, просто определяет мета-окно и функцию рендеринга для отображения его содержимого:

/**
 * Registers a meta box for entering product information. The meta box is
 * shown in the post editor for the "product" post type.
 *
 * @param   $post   WP_Post The post object to apply the meta box to
 */
public function add_product_information_meta_box( $post ) {
    add_meta_box(
        'product-information-meta-box',
        __( 'Product Information', $this->plugin_name ),
        array( $this, 'render_product_information_meta_box' ),
        'wplm_product',
        'side'
    );
}

Функция содержит только вызов функции WordPress add_meta_box, которая будет определять мета-поле. Параметры в порядке появления следующие:

    $ id: значение поля идентификатора HTML для элемента мета-окна.
    $ title: заголовок мета-поля, показанный в верхней части box. $ callback: функция, которая будет выполнять рендеринг метаданных. $ post_type: тип публикации в редакторе страниц, на котором должен отображаться этот мета-ящик. $ context: Where мета-поле должно быть помещено ( 'normal ', 'advanced ' или 'side ').

Затем добавьте функцию рендеринга, которую мы определили в третьем параметре:

/**
 * Renders the product information meta box for the given post (wplm_product).
 *
 * @param $post     WP_Post     The WordPress post object being rendered.
 */
public function render_product_information_meta_box( $post ) {
    $product_meta = get_post_meta( $post->ID, 'wp_license_manager_product_meta', true );
    if ( ! is_array( $product_meta ) ) {
        $product_meta = array(
            'file_bucket' => '',
            'file_name' => '',
            'version' => '',
            'tested' => '',
            'requires' => '',
            'updated' => '',
            'baer_low' => '',
            'baer_high' => ''
        );
    }
    $this->render_nonce_field( 'product_meta_box' );
    require( 'partials/product_meta_box.php' );
}

Строка 7: Функция начинается с чтения в текущих метаданных.

Линии 9-20: Если метаданные пусты (именно в этот момент, фактически...), создайте массив метаданных по умолчанию с пустыми значениями.

Строка 22: Распечатайте поле nonce, которое будет использоваться для некоторой дополнительной безопасности при сохранении метаданных. render_nonce_field - вспомогательная функция, которую я создал, чтобы помочь мне запомнить имя nonce. Мы добавим его через некоторое время.

Строка 24: Включите фактический метабокс HTML. Каталог partials является частью шаблона плагина WordPress и предназначен для разделения HTML-кода на PHP. Вот как выглядит файл шаблона для мета-поля:

<?php
/**
 * The view for the plugin's product meta box. The product meta box is used for
 * entering additional product information (version, file bucket, file name).
 *
 * @package    Wp_License_Manager
 * @subpackage Wp_License_Manager/admin/partials
 */
?>
<p>
    <label for="wp_license_manager_product_version">
        <?php _e( 'Version:', $this->plugin_name ); ?>
    </label>
    <input type="text" id="wp_license_manager_product_version"
           name="wp_license_manager_product_version"
           value="<?php echo esc_attr( $product_meta['version'] ); ?>"
           size="25" >
</p>
<p>
    <label for="wp_license_manager_product_tested">
        <?php _e( 'Tested with WordPress version:', $this->plugin_name ); ?>
    </label>
    <input type="text" id="wp_license_manager_product_tested"
           name="wp_license_manager_product_tested"
           value="<?php echo esc_attr( $product_meta['tested'] ); ?>"
           size="25" >
</p>
<p>
    <label for="wp_license_manager_product_requires">
        <?php _e( 'Requires WordPress version:', $this->plugin_name ); ?>
    </label>
    <input type="text" id="wp_license_manager_product_requires"
           name="wp_license_manager_product_requires"
           value="<?php echo esc_attr( $product_meta['requires'] ); ?>"
           size="25" >
</p>
<p>
    <label for="wp_license_manager_product_updated">
        <?php _e( 'Last updated:', $this->plugin_name ); ?>
    </label>
    <input type="text" id="wp_license_manager_product_updated"
           name="wp_license_manager_product_updated"
           value="<?php echo esc_attr( $product_meta['updated'] ); ?>"
           size="25" >
</p>
<p>
    <label for="wp_license_manager_product_baer_low">
        <?php _e( 'Baer low:', $this->plugin_name ); ?>
    </label>
    <input type="text" id="wp_license_manager_product_baer_low"
           name="wp_license_manager_product_baer_low"
           value="<?php echo esc_attr( $product_meta['baer_low'] ); ?>"
           size="25" >
</p>
<p>
    <label for="wp_license_manager_product_baer_high">
        <?php _e( 'Baer high:', $this->plugin_name ); ?>
    </label>
    <input type="text" id="wp_license_manager_product_baer_high"
           name="wp_license_manager_product_baer_high"
           value="<?php echo esc_attr( $product_meta['baer_high'] ); ?>"
           size="25" >
</p>
<h3>Download</h3>
<p>
    <label for="wp_license_manager_product_bucket">
        <?php _e( 'Amazon S3 Bucket:', $this->plugin_name ); ?>
    </label>
    <input type="text" id="wp_license_manager_product_bucket"
           name="wp_license_manager_product_bucket"
           value="<?php echo esc_attr( $product_meta['file_bucket'] ); ?>"
           size="25" />
</p>
<p>
    <label for="wp_license_manager_product_file_name">
        <?php _e( 'Amazon S3 File Name:', $this->plugin_name ); ?>
    </label>
    <input type="text" id="wp_license_manager_product_file_name"
           name="wp_license_manager_product_file_name"
           value="<?php echo esc_attr( $product_meta['file_name'] ); ?>"
           size="25" />
</p>

Шаблон довольно простой HTML, серия меток, за которыми следуют входные элементы со значениями, считанными из массива $ product_meta, который мы извлекли в вышеприведенной функции.

Теперь, создав мета-поле, давайте позаботимся о сохранении его данных.

Шаг 3: Сохранение данных метаданных

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

Это справедливо и для сохранения метаданных.

На этот раз действие, к которому нужно подключиться, - save_post, действие, которое активируется всякий раз, когда сообщение сохраняется в администраторе WordPress. Опять же, в классе Wp_License_Manager и его функции define_admin_hooks () добавьте новую строку:

$this->loader->add_action( 'save_post', $plugin_admin, 'save_product_information_meta_box' );

Функция save_product_information_meta_box входит в класс Wp_License_Manager_Admin:

/**
 * Saves the product information meta box contents.
 *
 * @param $post_id  int     The id of the post being saved.
 */
public function save_product_information_meta_box( $post_id ) {
    if ( ! $this->is_nonce_ok( 'product_meta_box' ) ) {
        return $post_id;
    }
    // Ignore auto saves
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return $post_id;
    }
    // Check the user's permissions
    if ( !current_user_can( 'edit_posts', $post_id ) ) {
        return $post_id;
    }
    // Read, sanitize, and store user input
    $meta = get_post_meta( $post_id, 'wp_license_manager_product_meta', true );
    if ( $meta == '' ) {
        $meta = array();
    }
    $meta['file_bucket'] = sanitize_text_field( $_POST['wp_license_manager_product_bucket'] );
    $meta['file_name'] = sanitize_text_field( $_POST['wp_license_manager_product_file_name'] );
    $meta['version'] = sanitize_text_field( $_POST['wp_license_manager_product_version'] );
    $meta['tested'] = sanitize_text_field( $_POST['wp_license_manager_product_tested'] );
    $meta['requires'] = sanitize_text_field( $_POST['wp_license_manager_product_requires'] );
    $meta['updated'] = sanitize_text_field( $_POST['wp_license_manager_product_updated'] );
    $meta['baer_low'] = sanitize_text_field( $_POST['wp_license_manager_product_baer_low'] );
    $meta['baer_high'] = sanitize_text_field( $_POST['wp_license_manager_product_baer_high'] );
    // Update the meta field
    update_post_meta( $post_id, 'wp_license_manager_product_meta', $meta );
}

Давайте перейдем через функцию, чтобы увидеть, что она делает:

Лины 7-9: Как вы помните, мы добавили поле nonce прямо перед рендерингом мета-поля продукта. В этой функции мы используем это nonce для проверки того, что человек, который отправляет данные, поступает из этой формы (мы скоро добавим функцию is_nonce_ok).
Lines 11-14: Не сохраняйте мета когда WordPress выполняет периодические автосохранения. Это связано с тем, что по умолчанию WordPress не передает данные метаданных, когда он вызывает вызов AJAX, чтобы сделать автосохранение, и поэтому обновление данных метаданных в этот момент может испортить вещи.
Лины 16-19: Только разрешить сохранение данных, если пользователь может редактировать сообщения. Одна вещь, которую следует учитывать в будущем, - добавить пользовательскую возможность для редактирования продуктов диспетчера лицензий.
Lines 22-25: Прочитать существующие метаданные продукта или создать новый массив данных, если метаданные еще не были сохранены (get_post_meta возвращает пустую строку, когда элемент метаданных не найден, а третий параметр равен true). Лины 27-34: Прочитайте в представленных данных, проделайте некоторую базовую санитарию. Линия 37: Все ОК, сохраните данные.

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

Сначала создайте одно:

/**
 * A helper function for creating and rendering a nonce field.
 *
 * @param   $nonce_label  string  An internal (shorter) nonce name
 */
private function render_nonce_field( $nonce_label ) {
    $nonce_field_name = $this->plugin_name . '_' . $nonce_label . '_nonce';
    $nonce_name = $this->plugin_name . '_' . $nonce_label;
    wp_nonce_field( $nonce_name, $nonce_field_name );
}

Ядром этой функции является вызов wp_nonce_field, который выполняет фактическую работу по созданию одноразового токена, используемого для nonce, и записи его в скрытом поле HTML.

Затем проверка:

/**
 * A helper function for checking the product meta box nonce.
 *
 * @param   $nonce_label string  An internal (shorter) nonce name
 * @return  mixed   False if nonce is not OK. 1 or 2 if nonce is OK (@see wp_verify_nonce)
 */
private function is_nonce_ok( $nonce_label ) {
    $nonce_field_name = $this->plugin_name . '_' . $nonce_label . '_nonce';
    $nonce_name = $this->plugin_name . '_' . $nonce_label;
    if ( ! isset( $_POST[ $nonce_field_name ] ) ) {
        return false;
    }
    $nonce = $_POST[ $nonce_field_name ];
    return wp_verify_nonce( $nonce, $nonce_name );
}

Функция проверки nonce использует те же соглашения об именах из вышеприведенной функции, а затем использует их для извлечения nonce из представленных данных формы (строки 11-15). Затем, если unce найден, он использует функцию WordPress wp_verify_nonce, чтобы проверить, что представленный код соответствует сохраненному (строка 17).

3. Добавить лицензии

Теперь мы находимся примерно на полпути к этому первому учебнику в серии: продукты находятся на месте, и пришло время взглянуть на лицензии.

Лицензия на ее простейшем состоит из двух элементов: лицензионного ключа и идентификатора пользователя (мы будем использовать для него адрес электронной почты). Для большего контроля над лицензией мы также добавим третий элемент: дату истечения срока действия лицензии, дату, после которой лицензия перестает быть действительной и не будет получать обновления с нашего сервера диспетчера лицензий.

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

Шаг 1. Создание таблицы базы данных

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

Плагин Boopplate для плагинов WordPress облегчает это, предоставляя пустую функцию, с которой мы можем начать писать наш код. Функция, активирующая в классе Wp_License_Manager_Activator, вызывается каждый раз, когда плагин активируется, либо нажав кнопку Активировать в плагинах или когда обновление плагина завершается.

Сначала добавьте следующее определение переменной вверху класса:

/**
 * The database version number. Update this every time you make a change to the database structure.
 *
 * @access   protected
 * @var      string    $db_version   The database version number
 */
protected static $db_version = 1;

А затем обновите функцию активации следующим кодом:

/**
 * Code that is run at plugin activation.
 */
public static function activate() {
    // Update database if db version has increased
    $current_db_version = get_option( 'wp-license-manager-db-version' );
    if ( ! $current_db_version ) {
        $current_db_version = 0;
    }
    if ( intval( $current_db_version ) < Wp_License_Manager_Activator::$db_version ) {
        if ( Wp_License_Manager_Activator::create_or_upgrade_db() ) {
            update_option( 'wp-license-manager-db-version', Wp_License_Manager_Activator::$db_version );
        }
    }
}

Вот что делает функция:

Линии 6-9: Прочитайте в опции wp-license-manager-db-version. Мы будем использовать эту опцию для хранения текущей версии базы данных плагина на сайте.

Строка 11: Сравните текущую версию базы данных с той, которая определена в кодовой базе плагина ($ db_version, определенная выше). Если номер версии, установленный в коде плагина, выше, чем тот, который хранится в параметрах WordPress, требуется обновление базы данных.

Строка 12: Запустите обновление базы данных.

Строка 13: Если обновление базы данных прошло успешно, обновите параметр wp-license-manager-db-version, чтобы он соответствовал версии в коде.

Теперь мы создали систему для отслеживания версий баз данных и можем перейти к созданию таблицы базы данных. Добавьте в Wp_License_Manager_Activator следующую функцию:

/**
 * Creates the database tables required for the plugin if
 * they don't exist. Otherwise updates them as needed.
 *
 * @return bool true if update was successful.
 */
private static function create_or_upgrade_db() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'product_licenses';
    $charset_collate = '';
    if ( ! empty( $wpdb->charset ) ) {
        $charset_collate = "DEFAULT CHARACTER SET {$wpdb->charset}";
    }
    if ( ! empty( $wpdb->collate ) ) {
        $charset_collate .= " COLLATE {$wpdb->collate}";
    }
    $sql = "CREATE TABLE " . $table_name . "("
         . "id mediumint(9) NOT NULL AUTO_INCREMENT, "
         . "product_id mediumint(9) DEFAULT 0 NOT NULL,"
         . "license_key varchar(48) NOT NULL, "
         . "email varchar(48) NOT NULL, "
         . "valid_until datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, "
         . "created_at datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, "
         . "updated_at datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, "
         . "UNIQUE KEY id (id)"
         . ")" . $charset_collate. ";";
    require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
    dbDelta( $sql );
    return true;
}

Большая часть этой функции заключается в создании запроса SQL CREATE TABLE, который мы будем использовать для создания таблицы базы данных:

Строка 10: Построить имя для таблицы базы данных.

Строки 12-18: Некоторые определения набора символов, которые будут использоваться в конечном SQL.

Линии 20-29: SQL для запроса CREATE TABLE. Таблица будет иметь следующие столбцы:

id: уникальный идентификатор для строки лицензии.
product_id: идентификатор продукта, к которому связана лицензия.
license_key: Используется строковый лицензионный ключ в качестве пароля. email: адрес электронной почты владельца лицензии, который работает как имя пользователя. valid_until: срок действия лицензии. created_at: отметка времени, когда лицензия была Создано. updated_at: Временная метка самого последнего обновления.

Линии 31-32: Используйте метод обновления базы данных WordPress dbDelta для создания или обновления таблицы базы данных. Обратите внимание, что dbDelta фактически не запускает запрос CREATE TABLE как есть, но анализирует его и сравнивает с текущей структурой таблицы, делая необходимые изменения.

Итак, если вы решите внести изменения в структуру таблицы, вместо написания нового SQL-запроса вы просто отредактируете этот запрос CREATE TABLE, обновите параметр $ db_version и дайте dbDelta обработать остальные.

Дополнительные сведения о создании таблиц базы данных в WordPress и использовании dbDelta см. В статье Создание таблиц с плагинами в коде WordPress.

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

Шаг 2. Создание страниц меню лицензий

Теперь, когда мы создали таблицу базы данных для хранения лицензий, давайте создадим страницы меню: «Лицензии» и «Добавить новый».

Опять же, мы начинаем с привязки к правильному действию в функции Wp_License_Manager define_admin_hooks:

$this->loader->add_action( 'admin_menu', $plugin_admin, 'add_licenses_menu_page' );

Функция action, add_license_menu_page, переходит в Wp_License_Manager_Admin и выглядит следующим образом:

/**
 * Creates the settings menu and sub menus for adding and listing licenses.
 */
public function add_licenses_menu_page() {
    add_menu_page(
        __( 'Licenses', $this->plugin_name ),
        __( 'Licenses', $this->plugin_name ),
        'edit_posts',
        'wp-licenses',
        array( $this, 'render_licenses_menu_list' ),
        'dashicons-lock',
        '26.1'
    );
    add_submenu_page(
        'wp-licenses',
        __( 'Licenses', $this->plugin_name ),
        __( 'Licenses', $this->plugin_name ),
        'edit_posts',
        'wp-licenses',
        array( $this, 'render_licenses_menu_list' )
    );
    add_submenu_page(
        'wp-licenses',
        __( 'Add new', $this->plugin_name ),
        __( 'Add new', $this->plugin_name ),
        'edit_posts',
        'wp-licenses-new',
        array( $this, 'render_licenses_menu_new' )
    );
}

Давайте рассмотрим более подробно:

Строки 5-13: Создайте страницу меню верхнего уровня с названием «Лицензии». Функция add_menu_page принимает следующие параметры:

    $ page_title: заголовок страницы меню (значение тега заголовка HTML). $ menu_title: экранное название страницы меню. $ возможность: возможность просмотра страницы. Я использовал edit_posts, но в будущем я, вероятно, подумаю о добавлении собственной возможности на свое место.
    $ menu_slug: Идентификатор страницы меню, используемый на URL страницы.
    $ function: Функция, которая будет обрабатывать визуализацию этой страницы меню. $ icon_url: Это поле может использоваться по-разному, но я решил пойти с иконками панели управления, как объяснялось ранее в этом уроке. $ position: Размещение пункта меню в меню WordPress. Проблема с использованием этого параметра заключается в том, что если два пункта меню имеют одинаковое значение позиции $, будет отображаться только один из них. Согласно документации WordPress, использование десятичного числа, как мы здесь делали, немного помогает.

Строки 15-22: Добавить подменю для перечисления лицензий. Параметры для add_submenu_page в остальном идентичны параметрам для add_menu_page, но первым параметром должен быть идентификатор верхнего меню, к которому необходимо добавить подменю.

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

Линии 24-31: второе подменю - это тот, с которого мы начнем, страница для добавления новой лицензии.

Чтобы сделать код без ошибок, добавьте две пустые функции:

public function render_licenses_menu_list() {
}
public function render_licenses_menu_new() {
}

Шаг 3: Создайте страницу добавления новой лицензии

Создав две страницы настроек, добавьте функциональность на первую страницу «Добавить новую лицензию».

Страница будет выглядеть так:

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

/**
 * Renders the list add new license menu page using
 * the "licenses_new.php" partial.
 */
public function render_licenses_menu_new() {
    // Used in the "Product" drop-down list in view
    $products = get_posts(
        array(
            'orderby'           => 'post_title',
            'order'             => 'ASC',
            'post_type'         => 'wplm_product',
            'post_status'       => 'publish',
            'nopaging'          => true,
            'suppress_filters'  => true
        )
    );
    require plugin_dir_path( dirname( __FILE__ ) ) . 'admin/partials/licenses_new.php';
}

Строки 7-16: Получить все опубликованные сообщения нашего пользовательского типа wplm_product.

Строка 18: Включите часть, содержащую HTML для этой страницы меню.

И вот что входит в шаблон HTML:

<?php
/**
 * The view for the admin page used for adding a new license.
 *
 * @package    Wp_License_Manager
 * @subpackage Wp_License_Manager/admin/partials
 */
?>
<div class="wrap">
    <div id="icon-edit" class="icon32 icon32-posts-post"></div>
    <h2><?php _e( 'Add New License', $this->plugin_name ); ?></h2>
    <p>
        <?php
            $instructions = 'Use this form to manually add a product license. '
                . 'After completing the process, make sure to pass the license key to the customer.';
            _e( $instructions, $this->plugin_name );
        ?>
    </p>
    <form action="<?php echo admin_url( 'admin-post.php' ); ?>" method="post">
        <?php wp_nonce_field( 'wp-license-manager-add-license', 'wp-license-manager-add-license-nonce' ); ?>
        <input type="hidden" name="action" value="license_manager_add_license">
        <table class="form-table">
            <tr class="form-field form-required">
                <th scope="row">
                    <label for="email">
                        <?php _e( 'Email', $this->plugin_name ); ?>
                        <span class="description"><?php _e( '(required)', $this->plugin_name ); ?></span>
                    </label>
                </th>
                <td>
                    <input name="email" type="text" id="email" aria-required="true">
                </td>
            </tr>
            <tr class="form-field form-required">
                <th scope="row">
                    <label for="email">
                        <?php _e( 'Product', $this->plugin_name ); ?>
                        <span class="description"><?php _e( '(required)', $this->plugin_name ); ?></span>
                    </label>
                </th>
                <td>
                    <select name="product" id="product" aria-required="true">
                        <?php foreach ( $products as $product ) : ?>
                            <option value="<?php echo $product->ID; ?>"><?php echo $product->post_title; ?></option>
                        <?php endforeach; ?>
                    </select>
                </td>
            </tr>
            <tr class="form-field form-required">
                <th scope="row">
                    <label for="valid_until">
                        <?php _e( 'Valid until', $this->plugin_name ); ?>
                    </label>
                </th>
                <td>
                    <input name="valid_until" type="text" id="valid_until" aria-required="false" />
                    <p class="description">
                        <?php _e( '(Format: YYYY-MM-DD HH:MM:SS / Leave empty for infinite)', $this->plugin_name );?>
                    </p>
                </td>
            </tr>
        </table>
        <p class="submit">
            <input type="submit" name="add-license" class="button button-primary"
                   value="<?php _e( 'Add License', $this->plugin_name ); ?>" >
        </p>
    </form>
</div>

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

Некоторые строки, на которые вы должны обратить внимание, следующие:

Строка 22: Когда пользователь отправляет форму, она будет отправлена в WordPress admin-post.php.

Строка 23: так же, как в мета-окне, добавьте nonce, чтобы убедиться, что кто-то опубликовал для добавления лицензии, на самом деле делает это через эту страницу.

Строка 24: Определите скрытое поле с именем action со значением license_manager_add_license . Это поле будет использоваться для добавления функции обработки для представления формы - мы перейдем к следующему.

Шаг 4. Сохраните лицензию

На предыдущем шаге мы создали нашу форму «Добавить новую лицензию». Теперь настало время создать функциональность для его сохранения.

Вернитесь к функции define_admin_hooks в Wp_License_Manager и добавьте новое действие:

$this->loader->add_action( 'admin_post_license_manager_add_license', $plugin_admin, 'handle_add_license' );

На этот раз мы используем admin_post_ {action}, умное действие для обработки настраиваемых форм.

Когда размещена форма со скрытым полевым действием, WordPress просматривает значение поля и использует его для создания соответствующего действия. Значение, которое мы использовали для скрытого поля, было "license_manager_add_license ", и поэтому мы можем добавить действие с именем admin_post_license_manager_add_license.

Функция handle_add_license переходит в Wp_License_Manager_Admin:

/**
 * Handler for the add_license action (submitting
 * the "Add New License" form).
 */
public function handle_add_license() {
    global $wpdb;
    if ( ! empty( $_POST )
        && check_admin_referer( 'wp-license-manager-add-license',
            'wp-license-manager-add-license-nonce' ) ) {
        // Nonce valid, handle data
        $email = sanitize_text_field( $_POST['email'] );
        $valid_until = sanitize_text_field( $_POST['valid_until'] );
        $product_id = intval( $_POST['product'] );
        $license_key = wp_generate_password( 24, true, false );
        // Save data to database
        $table_name = $wpdb->prefix . 'product_licenses';
        $wpdb->insert(
            $table_name,
            array(
                'product_id' => $product_id,
                'email' => $email,
                'license_key' => $license_key,
                'valid_until' => $valid_until,
                'created_at' => current_time( 'mysql' ),
                'updated_at' => current_time( 'mysql' )
            ),
            array(
                '%d',
                '%s',
                '%s',
                '%s',
                '%s',
                '%s'
            )
        );
        // Redirect to the list of licenses for displaying the new license
        wp_redirect( admin_url( 'admin.php?page=wp-licenses' ) );
    }
}

Давайте перейдем к функции, чтобы увидеть, что она делает:

Линии 8-10: Проверьте добавление nonce в HTML-форму.

Строки 14-16: собирать и дезактивировать предоставленные данные лицензии.

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

Линии 21-40: Сохраните данные в нашей таблице базы данных. Первым параметром функции insert в $ wpdb является имя таблицы. Второй - это массив с столбцами базы данных и их значениями. Третий определяет типы параметров для форматирования запроса (% s означает строку и цитируется, числовые значения, указанные в% d, остаются без кавычек).

Строка 43: перенаправить пользователя в список лицензий. Мы его построим дальше.

4. Список лицензий

Теперь, когда мы сохранили некоторые данные для лицензий, настало время для последнего шага в этом руководстве: создание списка для просмотра лицензий.

Мне всегда нравится, когда мои пользовательские интерфейсы администратора выглядят так же, как и администратор WordPress, как я могу, и поэтому при создании этого списка я решил пойти с тем же кодом, который использует ядро ​​WordPress для отображения его собственных списков: класс Wp_List_Table, описанный как класс "[b] ase для отображения списка элементов в ajaxified таблице HTML " в его документации PHPDoc.

Этот класс упрощает создание таблиц списка красивых таблиц, таких как тот, который вы видите выше в WordPress, но он содержит оговорку:

Разработчики WordPress задокументировали класс как @private, что означает, что они оставляют за собой право измените его радикально в любое время. Другими словами, использование класса является риском и означает, что вам придется тщательно протестировать ваш плагин перед выпуском новой версии WordPress, чтобы убедиться, что списки все еще работают.

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

Шаг 1. Создание класса таблицы таблиц

Сначала найдите класс Wp_List_Table (он в wp-admin включает класс-wp-list-table.php) и скопируйте его в свой собственный проект. Обязательно переименуйте класс и файл, чтобы они не вступили в конфликт с исходным классом из ядра WordPress (например, Wp_License_Manager_List_Table).

Затем создайте класс своего собственного класса для расширения этого базового класса. Вызовите класс Licenses_List_Table и поместите его в каталог администратора. Затем добавьте следующие строки require_once в функцию load_dependencies в Wp_License_Manager:

/**
 * The classes responsible for rendering the list of licenses.
 */
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-wp-license-manager-list-table.php';
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-licenses-list-table.php';

Откройте вновь созданный класс Licenses_List_Table и добавьте конструктор, который переносит текстовый домен для локализации в качестве параметра и поле для хранения значения. Этому не нужны объяснения:

class Licenses_List_Table extends Wp_License_Manager_List_Table {
    /**
    * The plugin's text domain.
    *
    * @access  private
    * @var     string  The plugin's text domain. Used for localization.
    */
    private $text_domain;
    /**
     * Initializes the WP_List_Table implementation.
     *
     * @param $text_domain  string  The text domain used for localizing the plugin.
     */
    public function __construct( $text_domain ) {
        parent::__construct();
        $this->text_domain = $text_domain;
    }
}

Шаг 2. Определение столбцов списка

Wp_List_Table поставляется с набором функций, которые мы можем переопределить для определения правил данных и форматирования для списка.

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

/**
 * Defines the database columns shown in the table and a
 * header for each column. The order of the columns in the
 * table define the order in which they are rendered in the list table.
 *
 * @return array    The database columns and their headers for the table.
 */
public function get_columns() {
    return array(
        'license_key' => __( 'License Key', $this->text_domain ),
        'email'       => __( 'Email', $this->text_domain ),
        'product_id'  => __( 'Product', $this->text_domain ),
        'valid_until' => __( 'Valid Until', $this->text_domain ),
        'created_at'  => __( 'Created', $this->text_domain )
    );
}

Обратите внимание, что ключи в массиве должны совпадать с именами столбцов в таблице базы данных.

Если мы не хотим отображать столбец created_at, мы можем скрыть его с помощью функции get_hidden_columns:

/**
 * Returns the names of columns that should be hidden from the list table.
 *
 * @return array    The database columns that should not be shown in the table.
 */
public function get_hidden_columns() {
    return array( 'created_at' );
}

Определение столбцов, которые можно использовать для сортировки данных списка, производится аналогичным образом:

/**
 * Returns the columns that can be used for sorting the list table data.
 *
 * @return array    The database columns that can be used for sorting the table.
 */
public function get_sortable_columns() {
    return array(
        'email' => array( 'email', false ),
        'valid_until' => array( 'valid_until', false )
    );
}

Затем давайте посмотрим на форматирование данных столбца. Форматирование может быть выполнено одним из двух способов: либо путем определения пользовательской функции форматирования с именем column_ {column_name}, где column_name - это имя столбца, как указано выше в get_columns, или с помощью функции рендеринга по умолчанию column_default.

Давайте используем column_default для столбцов, которые просто распечатываются. Функция принимает два параметра: обрабатывается строка базы данных ($ item) и имя столбца ($ column_name).

/**
 * Default rendering for table columns.
 *
 * @param $item         array   The database row being printed out.
 * @param $column_name  string  The column currently processed.
 * @return string       The text or HTML that should be shown for the column.
 */
function column_default( $item, $column_name ) {
    switch( $column_name ) {
        case 'email':
        case 'created_at':
            return $item[$column_name];
        default:
            break;
    }
    return '';
}

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

Вот пример, средство рендеринга для столбца valid_unit:

/**
 * Custom renderer for the valid_until field.
 *
 * @param $item     array   The database row being printed out.
 * @return string   The text or HTML that should be shown for the column.
 */
function column_valid_until( $item ) {
    $valid_until = $item['valid_until'];
    if ($valid_until == '0000-00-00 00:00:00') {
        return __( 'Forever', $this->text_domain );
    }
    return $valid_until;
}

Этот визуализатор отображает «Forever», когда значение поля «0000-00-00 00: 00: 00 », а в противном случае - само значение. Вы понимаете.

Шаг 3: Данные списка запросов

Теперь, когда мы определили параметры рендеринга для списка, давайте добавим некоторые данные в микс. Это делается в функции WP_List_Table prepare_items.

Здесь есть функция:

/**
 * Populates the class fields for displaying the list of licenses.
 */
public function prepare_items() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'product_licenses';
    $columns = $this->get_columns();
    $hidden = $this->get_hidden_columns();
    $sortable = $this->get_sortable_columns();
    $this->_column_headers = array( $columns, $hidden, $sortable );
    // Paginatio
    $licenses_per_page = 20;
    $total_items = $wpdb->get_var( "SELECT COUNT(id) FROM $table_name" );
    $offset = 0;
    if ( isset( $_REQUEST['paged'] ) ) {
        $page = max( 0, intval( $_REQUEST['paged'] ) - 1 );
        $offset = $page * $licenses_per_page;
    }
    $this->set_pagination_args(
        array(
            'total_items' => $total_items,
            'per_page' => $licenses_per_page,
            'total_pages' => ceil( $total_items / $licenses_per_page )
        )
    );
    // Sorting
    $order_by = 'email'; // Default sort key
    if ( isset( $_REQUEST['orderby'] ) ) {
        // If the requested sort key is a valid column, use it for sorting
        if ( in_array( $_REQUEST['orderby'], array_keys( $this->get_sortable_columns() ) ) ) {
            $order_by = $_REQUEST['orderby'];
        }
    }
    $order = 'asc'; // Default sort order
    if ( isset( $_REQUEST['order'] ) ) {
        if ( in_array( $_REQUEST['order'], array( 'asc', 'desc' ) ) ) {
            $order = $_REQUEST['order'];
        }
    }
    // Do the SQL query and populate items
    $this->items = $wpdb->get_results(
        $wpdb->prepare( "SELECT * FROM $table_name ORDER BY $order_by $order LIMIT %d OFFSET %d", $licenses_per_page, $offset ),
        ARRAY_A );
}

Давайте перейдем к функции, чтобы увидеть, что она делает:

Линии 8-12: Соберите заголовок столбца и информацию о сортировке с помощью функций, которые мы создали выше.

После этого оставшаяся часть касается запроса данных.

Строки 15-22: Вычислить информацию разбивки на страницы. Если пользователь запросил страницу, параметр paged содержит номер страницы (начиная с 1), поэтому мы можем использовать его для вычисления смещения для SQL-запроса.

Линии 24-30: Передайте информацию разбивки на страницы в таблицу списка, чтобы она отображала правильные кнопки навигации при визуализации таблицы.

Линии 33-46: Подготовьте столбец сортировки и порядок, основанный на пользовательском вводе.

Линии 49-51: Используйте переменные, определенные ранее в функции, чтобы выполнить запрос к базе данных и заполнить переменную класса $ items.

И вот что: теперь вы создали страничную и сортируемую таблицу для отображения лицензий.

Шаг 4. Использование таблицы списков

Ранее в этом уроке мы создали пустую функцию для показа содержимого страницы «Лицензии». Теперь, когда мы создали таблицу списков, мы можем заполнить эту функцию (render_licenses_menu_list в Wp_License_Manager_Admin) и, наконец, сделать видимыми лицензии:

/**
 * Renders the list of licenses menu page using the "licenses_list.php" partial.
 */
public function render_licenses_menu_list() {
    $list_table = new Licenses_List_Table( $this->plugin_name );
    $list_table->prepare_items();
    require plugin_dir_path( dirname( __FILE__ ) ) . 'admin/partials/licenses_list.php';
}

Сначала функция создает экземпляр нашего класса Licenses_List_Table, затем вызывает его функцию prepare_items (определенную выше) для инициализации данных и, наконец, включает в себя шаблон HTML для отображения страницы.

Этот шаблон также содержит вызов $ list_table-> display (), который отображает список на странице:

<?php
/**
 * The view for the admin page used for listing licenses.
 *
 * @package    Wp_License_Manager
 * @subpackage Wp_License_Manager/admin/partials
 */
?>
<div class="wrap">
    <h2>
        <?php _e( 'Licenses', $this->plugin_name ); ?>
        <a class="add-new-h2" href="<?php echo admin_url( 'admin.php?page=wp-licenses-new' );?>">
            <?php _e( 'Add new', $this->plugin_name ) ?>
        </a>
    </h2>
    <?php $list_table->display(); ?>
</div>

Что дальше?

Теперь мы создали базовый плагин для хранения продуктов и лицензий. Однако это не конец.

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

Увидимся в следующий раз!