Namespace на пальцах

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

Содержание

Теория о nаmespase

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

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

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

Примеры

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

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

<?php class ClassName { // some methods }
Code language: HTML, XML (xml)

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

<?php namespace developer1; class ClassName { // some methods }
Code language: HTML, XML (xml)
<?php namespace developer2; class ClassName { // some methods }
Code language: HTML, XML (xml)

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

<?php $var1 = new developer1\ClassName(); $var2 = new developer2\ClassName();
Code language: HTML, XML (xml)

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

<?php use developer1\ClassName as ClassName1; use developer2\ClassName as ClassName2; $var1 = new ClassName1(); $var2 = new ClassName2();
Code language: HTML, XML (xml)

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

<?php use developer1\ClassName; $var = new ClassName();
Code language: HTML, XML (xml)

Работать это будет так: при обращении к классу 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'); });
Code language: HTML, XML (xml)

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

Заключение

В этой, сравнительно небольшой, статье рассмотрены пространства имен для классов. Наведены 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
Code language: Bash (bash)

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

npm install --save-dev @babel/core @babel/cli
Code language: Bash (bash)

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

npm install @wordpress/babel-preset-default --save-dev
Code language: Bash (bash)

Среда для компиляции 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
Code language: Bash (bash)

После окончания компиляции, будет создан 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');
Code language: HTML, XML (xml)

но, по условию, мне нужно выбрать категорию постов. Для этого передадим список категорий и путь, по которому блок будет брать информацию о постах — конечный 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');
Code language: HTML, XML (xml)

немного о зависимостях — скрипт 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' ]); }
Code language: HTML, XML (xml)

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

<?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; }
Code language: HTML, XML (xml)

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

<?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(); }
Code language: HTML, XML (xml)

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

Frontend блока

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

(function(blocks, element, i18n, editor, data) { // code })( window.wp.blocks, window.wp.element, window.wp.i18n, window.wp.editor, window.glp );
Code language: JavaScript (javascript)

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

const { createElement } = element; const { __ } = i18n; const { registerBlockType } = blocks; const { InspectorControls } = editor; const { BlockControls } = editor;
Code language: JavaScript (javascript)

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

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: [] } } } });
Code language: JavaScript (javascript)

поля 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 );
Code language: HTML, XML (xml)

Отдельно упомяну об атрибутах — из-за строгого отношения к типам данных, чтобы булевое значение не сохранить в БД; поэтому, приходится булевое ставить в зависимость от других типов, значение которых можно сохранить. Так у меня, выбор отображения изображения поста происходит чекбоксом(атрибут 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> ); }
Code language: JavaScript (javascript)

немного о написанном: функции 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> ); }
Code language: JavaScript (javascript)

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

Заключение

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