Namespace на пальцах

Всем привет и здравствуйте! Сегодня речь пойдет об пространстве имен(nаmespaсe). Эта фича чисто объектно-ориентированного программирования; несмотря на то, что пространство имен есть во многих языках программирования(C++, python, java и т.д.), код, приведенный в примерах, будет на php.

Содержание

Теория о nаmespase

Пространство имён (англ. namespace) — некоторое множество, под которым подразумевается модель, абстрактное хранилище или окружение, созданное для логической группировки уникальных идентификаторов (то есть имён).

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

Такое определение дает нам википедия, однако оно, как по мне, достаточно сложное для понимания. Я же определю более просто – это дополнительная координата для обращения к классу. Рассмотрим примеры для большего понимания…

Примеры

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

Теперь применительно к коду: 2 программиста написали по одинаково названному классу

<?php
class ClassName {
    // some methods
}

и отправили третьему. Тот, должен использовать эти классы для сборки приложения – но как к ним обращаться? Вариант №1 – на берегу договориться о наименовании классов – это подойдет для небольшого их количества, но если их десятки и сотни? Вариант №2 -правильный вариант – договориться об использовании пространств имен

<?php
namespace developer1;

class ClassName {
    // some methods
}
<?php
namespace developer2;

class ClassName {
    // some methods
}

вот так это будет выглядеть в php. Далее, третьему нужно использовать эти классы в своем коде. Чтоб обратиться к такому классу, нужно сначала указать пространство имен, а затем имя класса

<?php
$var1 = new developer1ClassName();
$var2 = new developer2ClassName();

это годится при единичном вызове каждого класса, но что если ссылок на каждый класс множество? каждый раз писать пространство имени класса? По меньшей мере, это не удобно. Для этого есть директива use as (для php, в других языках есть аналоги)

<?php
use developer1ClassName as ClassName1;
use developer2ClassName as ClassName2;

$var1 = new ClassName1();
$var2 = new ClassName2();

По сути, это работает как “локальное переименование”, т.е классы ClassName1 и ClassName2 будут существовать только в пределах файла с этими директивами. В случаи если начальные классы имеют разные названия, директиву use as можно сократить до use

<?php
use developer1ClassName;

$var = new ClassName();

Работать это будет так: при обращении к классу ClassName будет проверены все директивы use и если есть та, которая на конце имеет такое же имя класса, будет обращение к пространству этого имени.

Автозагрузка классов

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

пьяная плесень - монгольский

т.е. обязательно) это неудобно. В этом случаи удобно использовать “автозагрузку классов”. Дело в том, что при обращении к незнакомому классу, возбуждается функция, которой передается название требуемого класса(если известно его имя пространства – будет передано и оно).

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

Сначала приведем структуру директорий и названий классов в соответствие с namespace и именем класса. Есть две функции для подобной работы – __autoload()(устаревшая с php 7.2) и spl_autoload_register()(появившаяся в php 5.0). Также, с php 5.3 добавлены анонимные функции и код автозагрузчика будет выглядеть так

<?php
spl_autoload_register(function($class)
{
  require($class . '.php');
});

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

Заключение

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

Создаем блок редактора Gutenberg

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

Содержание

Постановка задачи

  • Задача: разработать плагин, добавляющий блок последних записей в редактор.
  • В блоке можно редактировать:
    • Количество выводимых постов;
    • Категорию постов(из существующих, не пустых)
    • Статус отображения изображения записи

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

Теоретические выкладки

Начнем из далека – нужно же статье придать объёма!

Редактор Gutenberg – это новый(относительно, частью wp он стал в версии движка 5, до этого был плагином) визуальный редактор WordPress для записей и страниц. Проект назван именем Иогана Гутерберга, презентовавшего Европе печатный станок и начавшего печатную революцию. Его работа сделала знание и информацию доступнее и запустила социальную революцию. Аналогично этому, разработчики хотят сделать доступным создание продвинутый макетов страниц для всех пользователей WordPress(источник).

Основная особенность редактора Gutenberg – это преставление всего контента в виде блоков и определение макета записи прямо в редакторе.

Ну все, хватит копи-паста, мой внутренний борец с плагиатом не позволит более. Да, я знаю, что все мои статейки – это, всего лишь, пере озвучка документаций и других источников… но, иногда, пишу и свои наблюдения.

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

Опишу свои мысли о том, что я буду делать – дабы выполнить поставленную задачу. Первым делом, учитывая, что по правилам хорошего тона react общается с приложениями по средствам REST API, и нужного пути вордпресс по умолчанию не имеет – создам этот путь в WP REST API. Второе – я знаю, что в таблицу БД движок сохраняет запись о блоке; поскольку, наш блок во фронте должен быть динамичен – то запись должна хранить элемент, который вордпресс ассоциирует с функцией. Кто ранее знакомился с этим движком, понял о чем я – о шоркоде, который мне также предстоит создать. И последнее, несколько слов о javascript – внутри функции регистрации блока нужно будет определить 4 вещи: мета инфу о блоке, его атрибуты, сохраняемую строку и вид в редакторе.

Установка рабочей среды

Это действие нужно чисто для удобства и скорости разработки. Но, в принципе, код можно писать и в блокноте – но все используют специальные редакторы. Так-что, советую эти операции проделать тоже…

Первое, при любой разработке на PHP(а язык бэка wordpress именно php) понадобится локальный сервер. Я использую OpenServer: просто скачай с официального сайта и установи его.

Второе – это компилятор JSX в JS, SCSS в CSS. В качестве такого компилятора, я использую NodeJS с пакетами; далее, подробно об установке.

С официального сайта грузим и устанавливаем NodeJS. После успешной установки, зашел в командную строку, у нас появиться менеджер пакетов npm. Первое, что нужно сделать – это сформировать package.json запуском команды

npm init

и дальнейшими ответами на поставленные вопросы. После чего, в командной строке пишем

npm install --save-dev @babel/core @babel/cli

это установи транскриптор babel в зависимости для ноды. И, для полного понимания при компиляции, нужен пресет @wordpress/babel-preset-default. Запустим

npm install @wordpress/babel-preset-default --save-dev

Среда для компиляции JSX готова. Немного о нахождении компилируемого файла и самого запуска процесса компиляции. Пусть компилируемый файл называется script.js и скомпилированный script-compiled.js; как я понял наблюдением, файл script.js должен лежать на одном уровне с package.json и папкой node_modules, так как ругается на указанные пути(по крайней мере, так было у меня). Сам процесс компиляции запускается так

npx babel --presets @wordpress/default script.js --watch --out-file script-compiled.js

После окончания компиляции, будет создан script-compiled.js

Backend блока

Бэк блока, как было ранее сказано, заключается в подключении скрипта и стилей, а также, нового rest-пути и шорткода. Делается это следующим кодом

<?php
function blocks_scripts()
{
    wp_enqueue_script(
        'gutenberg-latest-posts',
        plugins_url('/block/js/gutenberg-latest-posts-block.js'),
        ['wp-blocks', 'wp-element', 'wp-editor', 'wp-i18n'],
        true
    );
}
add_action('enqueue_block_editor_assets', 'blocks_scripts');

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

<?php
function blocks_scripts()
{
    $categories = get_categories();

    if (!empty($categories)) {
        foreach ($categories as $key => $value) {
            $cats[$value->term_id] = $value->name;
        }
    }
    // скрываем блок если нет категорий
    if (!empty($cats)) {
        // подключаем блок
        wp_enqueue_script(
            'gutenberg-latest-posts',
            plugins_url('/block/js/gutenberg-latest-posts-block.js'),
            ['wp-blocks', 'wp-element', 'wp-editor', 'wp-i18n'],
            true
        );
        // создаем обьект glp, содержащий
        // список категорий и ссылку на WP REST API постов
        wp_localize_script(
            'gutenberg-latest-posts',
            'glp',
            [
            'categories' => $cats,
            'restURL' => esc_url(get_rest_url(null, 'block/v1/latest-posts'))
          ]
        );
    }
}
add_action('enqueue_block_editor_assets', 'blocks_scripts');

немного о зависимостях – скрипт wp-blocks содержит функции, необходимые для регистрации блока и образует js объект wp.blocks; wp-element содержит функции react с ворпрессовской “прокладкой”, объект wp.element; wp-i18n функции для создания translate ready, wp.18n. Зависимость wp-editor можно пропустить, но она содержит готовые функции для размещения элементов управления атрибутами блока.

Следующий шаг – создание пути ‘wp/v2/latest-posts‘ – ведь его еще не существует. Этот код его зарегистрирует в системе

<?php
add_action('rest_api_init', 'blocks_rest_endpoint');
function blocks_rest_endpoint()
{
  register_rest_route('block/v1', '/latest-posts', [
    'method' => 'GET',
    'callback' => 'blocks_return_data'
  ]);
}

Теперь то, что он будет выводить(имеется в виду, что получит перешедший по этому пути)

<?php
function blocks_return_data()
{
    $category = isset($_GET['cat']) ? absint($_GET['cat']) : 0;
    $count = isset($_GET['count']) ? absint($_GET['count']) : 5;

    return json_encode(blocks_get_latest_posts($category, $count));
}

function blocks_get_latest_posts($category, $count)
{
    $result = [];
    $posts = get_posts([
      'numberposts' => $count,
      'category' => $category
    ]);

    foreach ($posts as $key => $value) {
        $result[] = [
          'title' => get_the_title($value->ID),
          'excerpt' => get_the_excerpt($value->ID),
          'link' => get_permalink($value->ID),
          'src' => get_the_post_thumbnail_url($value->ID, 'full'),
          'id'  => $value->ID
        ];
    }

    return $result;
}

и в конце, зарегистрируем шорткод

<?php
add_shortcode('gutenberg-block-latest-posts', 'blocks_shortcode');
function blocks_shortcode($atts)
{
    $atts = shortcode_atts([
      'count' => 5,
      'cat' => array_shift(array_keys(blocks_get_categories())),
      'thumb' => 1
    ], $atts);

    $posts = blocks_get_latest_posts(absint($atts['cat']), absint($atts['count']));

    ob_start(); ?>
    <div class="latest-posts">
      <?php if (!empty($posts)): ?>
        <?php foreach ($posts as $key => $value): ?>
          <div class="post-<?php echo $value['id']; ?> row">
            <?php if ($atts['thumb'] && !empty($value['src'])): ?>
              <div class="thumbnail">
                <img src="<?php echo $value['src'] ?>" alt="<?php echo $value['title'] ?>">
              </div>
            <?php endif; ?>
            <div class="post-content">
              <a href="<?php echo $value['link'] ?>">
                <h2><?php echo $value['title'] ?></h2>
              </a>
              <p><?php echo $value['excerpt'] ?></p>
            </div>
          </div>
        <?php endforeach; ?>
      <?php endif; ?>
    </div>
    <?php
    wp_reset_postdata();

    return ob_get_clean();
}

Вот и весь бэк-энд блока

Frontend блока

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

(function(blocks, element, i18n, editor, data) {
    // code
})(
  window.wp.blocks,
  window.wp.element,
  window.wp.i18n,
  window.wp.editor,
  window.glp
);

В нижних скобках указаны те объекты, которые нужно “впустить”, сверху – под какими именами их использовать в порядке соответствия. Далее, для удобства(поймете после компиляции файла), нужно “вытянуть” некоторые методы с объектов

  const { createElement } = element;
  const { __ } = i18n;
  const { registerBlockType } = blocks;
  const { InspectorControls } = editor;
  const { BlockControls } = editor;

Далее, зарегистрируем блок редактора, его описания и атрибуты, коими будем манипулировать

  registerBlockType('block/gutenberg-latest-posts', {
      title: __('Latest Posts'),
      icon: 'dashicons-excerpt-view',
      category: 'common',
      attributes: {
        thumb: {
          type: 'integer',
          default: 1
        },
        count: {
          type: 'integer',
          default: 5
        },
        category: {
          type: 'integer',
          default: Object.keys(data.categories)[0]
        },
        checked: {
          type: 'bool',
          default: true
        },
        posts:  {
          type: 'array',
          default: []
        }
      }
  }
});

поля title и icon интуитивно понятные(есть еще и другие, необязательные), скажу только, что category это блок, где будет отображен блок(блоки по умолчанию ). Чтобы создать свою категорию добавь следующий PHP-код

<?php
function my_plugin_block_categories( $categories, $post ) {
    if ( $post->post_type !== 'post' ) {
        return $categories;
    }
    return array_merge(
        $categories,
        array(
            array(
                'slug' => 'my-category',
                'title' => __( 'My category', 'my-plugin' ),
                'icon'  => 'wordpress',
            )
    );
}
add_filter( 'block_categories', 'my_plugin_block_categories', 10, 2 );

Отдельно упомяну об атрибутах – из-за строгого отношения к типам данных, чтобы булевое значение не сохранить в БД; поэтому, приходится булевое ставить в зависимость от других типов, значение которых можно сохранить. Так у меня, выбор отображения изображения поста происходит чекбоксом(атрибут checked), состояние которого возвращается в типе bool. Чтобы сохранять это состояние, я ввел доп.атрибут в типе integer(thumb), значение которого в полной зависимостти от checked.

Осталось определить 2 метода, которые должны возвращать HTML-разметку имеющую по контейнеру. Первый(edit) отвечает за разметку в редакторе блоков, второй(save) – за то, что будет сохранено в БД. Начнем с первого(полный код, написанный мною)

      edit: ({attributes, setAttributes}) => {
        function Item(props) {
          return (
            <div class={ 'row post-' + props.props.id }>
              { (() => {
                if (attributes.thumb && props.props.src.length) {
                  return (
                    <div class="thumbnail">
                      <img src={ props.props.src } alt={ props.props.title }/>
                    </div>
                  );
                }
              })() }
              <div class="post-excerpt">
                <a href={ props.props.link } onClick={ abort }><h3>{ props.props.title }</h3></a>
                <p>{ props.props.excerpt }</p>
              </div>
            </div>
          );
        }
        function setCount(event) {
          setAttributes({count:parseInt(event.target.value)});
          setPosts(attributes.category, event.target.value);
        }
        function setCategory(event) {
          setAttributes({category:parseInt(event.target.value)});
          setPosts(event.target.value, attributes.count);
        }
        function setThumb(event) {
          if (event.target.checked) {
            setAttributes({
              thumb: 1,
              checked: true
            });
          } else {
            setAttributes({
              thumb: 0,
              checked: false
            });
          }
        }
        function abort(event) {
          event.preventDefault();
        }
        function setPosts(category, count) {
          var xhr = new XMLHttpRequest();
          xhr.open('GET', data.restURL + '?cat=' + category + '&count=' + count);

          xhr.onload = function () {
            setAttributes({posts:JSON.parse(xhr.responseText)});
          };

          xhr.send();
        }
        return (
          <div className={ attributes.className }>
            <BlockControls>
              <h3>{ __('Latest Posts') }</h3>
            </BlockControls>
            <InspectorControls>
              <p>
                <label>{ __('Posts Count') }</label><br/>
                <input type="number" value={ attributes.count } onChange={ setCount } min="1"/>
              </p>
              <p>
                <label>{ __('Posts Category') }</label><br/>
                  { (() => {
                    let option = Object.keys(data.categories).map((i) => {
                      return (
                        <option value={ i }>{ data.categories[i] }</option>
                      );
                    });
                    return (
                      <select value={ attributes.category } onChange={ setCategory }>
                        { option }
                      </select>
                    )
                  })() }
              </p>
              <p>
                <label>
                  <input type="checkbox" checked={ attributes.checked } onChange={ setThumb }/>
                  { __('Show Thumbnail') }
                </label>
              </p>
            </InspectorControls>
              { (() => {
                if (!attributes.posts.length) {
                  setPosts(attributes.category, attributes.count);
                }
                let items = attributes.posts.map((post) => {
                  return (
                    <Item props={ post }/>
                  );
                });
                return (
                  <div class="preview">
                    { items }
                  </div>
                );
              })() }
          </div>
        );
      }

немного о написанном: функции setCount, setCategory и setThumb – для изменения состояния атрибутов; Item и ее вызов кодом <Item /> – чисто реактовский прикол; setPosts получает список постов по нашему маршруту, и устанавливает его в атрибут posts. BlockControls и InspectorControls – методы объекта wp.editor: первый размещает внутреннюю разметку в верхней панели(toolbar) блока, второй – в боковой панели.

И, наконец-то, edit. Он очень прост, если бы разрабатываемый блок был стационарен – он возвращал бы такую же разметку, как и основное поле блока

      save: ({attributes}) => {
        return (
          <div class="latest-posts-block">
            [gutenberg-block-latest-posts cat="{ attributes.category }" count="{ attributes.count }" thumb="{ attributes.thumb }"]
          </div>
        );
      }

но поскольку он динамичен – содержит мой шорткод. Обратите внимание, поскольку результатом должна быть HTML-разметка – я обернул шорткод в тег.

Заключение

После компиляции js был получен код регистрации нового блока. Забыл сказать в основном теле статьи – особенностью react есть то, что он рендерит по фреймам, а не по клику. Из этого следует, что любое действие следует “запирать” в функцию. Весь код выполненного тестового лежит здесь.