Учебное пособие по компонентам Joomla 4



От автора

Будучи опытным разработчиком на Joomla 3.x мне нужно было узнать о Joomla 4.x, и реально было действительно трудно начать работу. Несмотря на опыт, мои знания о внутреннем устройстве Joomla ограничены. Я начал работать с Joomla на стадии версии 1.6, и многие новые функции прошли мимо меня. Таким образом, это руководство может быть примером того, как слепой ведет слепого. Мне потребовалось около 10 дней, чтобы заставить хоть что-то работать, прочитав код, запустив отладчик и прочитав ограниченное количество доступной документации по Joomla 4. Это руководство написано для Joomla 4.x на этапе Alpha 10 и было обновлено для этапа Beta 4. Даже в этом случае оно может слишком скоро устареть.

Текст руководства был подготовлен как статья, написанная с помощью Joomla 4.0 Alpha 10, преобразованная в MediaWiki, а затем в форматы Github Markdown с помощью Pandoc. И вот что в итоге вышло.

Назначение компонента и его схема данных (Data Schema)

Последние несколько лет я гуляю с семьей, иногда один раз в неделю, иногда два раза, но только в хорошую погоду. Я вел список - всего около 50 прогулок, и каждая отличалась от других. Все это было частью попытки поддерживать форму в старости. Поэтому для самообучения я решил разработать компонент на CMS Joomla, который имеет два представления: список прогулок и детали отдельных прогулок. Чтобы не усложнять, я не хочу никаких излишеств: ни ввода данных на стороне сайта, ни оценок, ни счетчиков посещений, ни категорий, ни других плюшек Joomla. Для целей тестирования часть данных была введена непосредственно в базу данных с помощью phpMyAdmin. Компоненту необходимы две таблицы базы данных: список прогулок и список индивидуальных посещений. Я решил назвать компонент com_mywalks и таблицы #__mywalks и #__mywalks_dates.

Весь код для этого учебного компонента можно получить по этой ссылке.

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

Таблица mywalks

На момент написания скрипт установки в папке admin/sql не вызывается. Итак, если вы устанавливаете рабочую версию кода этого руководства, сначала запустите следующие сценарии вручную. Это ошибка в Joomla или в учебном коде? Скрипт удаления работает - таблицы успешно пропадают.

Файлы sql в рабочем примере zip-файла включают образцы данных.

CREATE TABLE IF NOT EXISTS `#__mywalks` (
  `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `title` varchar(64) NOT NULL,
  `description` text NOT NULL,
  `distance` decimal(10,0) NOT NULL,
  `toilets` tinyint(1) NOT NULL DEFAULT '0',
  `cafe` tinyint(1) NOT NULL DEFAULT '0',
  `hills` int(11) NOT NULL DEFAULT '0',
  `bogs` int(11) NOT NULL DEFAULT '0',
  `picture` varchar(128) DEFAULT NULL,
  `width` int(11) DEFAULT NULL,
  `height` int(11) DEFAULT NULL,
  `alt` varchar(64) DEFAULT NULL,
  `state` TINYINT NOT NULL DEFAULT '1'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

Таблица mywalks_dates

CREATE TABLE IF NOT EXISTS `#__mywalk_dates` (
  `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `walk_id` int(11) NOT NULL,
  `date` date NOT NULL,
  `weather` varchar(256) DEFAULT NULL,
  `state` TINYINT NOT NULL DEFAULT '1',
  KEY `idx_walk` (`walk_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

Если бы это был реальный компонент, было бы очевидно, что до окончания разработки схемы еще далеко! Посмотрите на сайт WalkHighlands, чтобы узнать, как далеко. Однако этого достаточно для учебных целей.

Структура файлов манифеста и компонентов

Zip-файл компонента, используемый для установки, должен содержать файл манифеста с именем mywalks.xml (без уведомления com_) вместе с папками администратора и сайта, примерно так:

com_mywalks.zip
     admin
     site
     mywalks.xml

При установке файл манифеста копируется в папку site_root/administrator/components/com_mywalks, где он нужен для удаления. Его не должно быть в исходном коде! Записи также делаются в site_root/administrator/cache/autoload_psr4.php [Это нововведения в Joomla 4].

Файл манифеста

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

<?xml version="1.0" encoding="UTF-8"?>
<extension type="component" method="upgrade">
	<name>com_mywalks</name>
	<!-- Следующие элементы являются необязательными и не содержат ограничений форматирования. -->
	<creationDate>August 2019</creationDate>
	<author>Clifford E Ford</author>
	<authorEmail>cliff@ford.myzen.co.uk</authorEmail>
	<authorUrl>http://www.fford.me.uk/</authorUrl>
	<copyright>Copyright (C) 2019 Clifford E Ford, All rights reserved.</copyright>
	<license>GNU/GPL Version 2 or later - http://www.gnu.org/licenses/gpl-2.0.html</license>
	<!-- Строка версии записывается в таблице компонентов. -->
	<version>0.2.0</version>
	<!-- Описание является необязательным и по умолчанию используется name -->
	<description>COM_MYWALKS_XML_DESCRIPTION</description>
	<namespace path="src">J4xdemos\Component\Mywalks</namespace>

	<install> <!-- Запускается при установке -->
		<sql>
			<file driver="mysql" charset="utf8">sql/install.mysql.sql</file>
		</sql>
	</install>
	<uninstall> <!-- Запускается при удалении -->
		<sql>
			<file driver="mysql" charset="utf8">sql/uninstall.mysql.sql</file>
		</sql>
	</uninstall>

	<!-- Раздел копирования основных файлов фронтенда сайта -->
	<!-- Обратите внимание на атрибут folder: этот атрибут описывает папку,
		из которой нужно скопировать в пакете для установки, поэтому
		файлы, скопированные в этом разделе, копируются из /site/ в пакете. -->

	<files folder="site">
		<folder>src</folder>
		<folder>tmpl</folder>
	</files>
	
	<languages folder="site">
		<language tag="en-GB">language/en-GB/com_mywalks.ini</language>
	</languages>
	
	<administration>
		<files folder="admin">
			<file>access.xml</file>
			<file>config.xml</file>
			<folder>forms</folder>
			<folder>services</folder>
			<folder>sql</folder>
			<folder>src</folder>
			<folder>tmpl</folder>
		</files>
		<languages folder="admin">
			<language tag="en-GB">language/en-GB/com_mywalks.ini</language>
			<language tag="en-GB">language/en-GB/com_mywalks.sys.ini</language>
		</languages>
		<menu img="class:default" link="option=com_mywalks">com_mywalks</menu>
	</administration>
</extension>

Пространство имен

Обратите внимание на тег namespace (пространства имен) в файле манифеста. Первым элементом должно быть название компании. У меня его нет, поэтому я использовал J4xdemos. Пространство имен используется в расширении, чтобы отличать его код от кода в других расширениях, которые могут иметь идентичные имена классов. Пространство имен используется для регистрации поставщика услуг - см. Ниже.

Второй элемент - это тип расширения:

  • Компонент (Component),
  • Модуль (Module),
  • Плагин (Plugin),
  • Шаблон (Template).

Третий элемент - это имя расширения без добавления com_, mod_ и т. Д., В данном случае Mywalks.

Атрибут пространства имен path="src" указывает, что все файлы, содержащие код пространства имен, будут найдены в каталоге src.

Файлы языковых констант

Если вы не знакомы с расширениями Joomla, языковая папка исходного сайта содержит один файл: en-GB.com_mywalks.ini, который содержит переведенные значения фиксированных строк, используемых для перевода с английского на другие языки. Структура папок проста:

site                               - папка, содержащая файлы сайта
     language                      - папка, содержащая файл языкового перевода сайта
          en-GB                    - папка с английскими переводами
               com_mywalks.ini     - файл языковых констант

И com_mywalks.ini имеет такое содержание:

COM_MYWALKS_LIST_DESCRIPTION="Description"
COM_MYWALKS_LIST_DISTANCE="Distance in Km"
COM_MYWALKS_LIST_LAST_VISIT="Last Visit"
COM_MYWALKS_LIST_NVISITS="nVisits"
COM_MYWALKS_LIST_PAGE_HEADING="List of Walks"
COM_MYWALKS_LIST_TABLE_CAPTION="List of Walks"
COM_MYWALKS_LIST_TITLE="Title"
COM_MYWALKS_ERROR_WALK_NOT_FOUND="Walk not found!"

COM_MYWALKS_WALK_DATE="Visit date"
COM_MYWALKS_WALK_REPORTS="Walk Reports"
COM_MYWALKS_WALK_WEATHER="Weather Report"

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

Языковые файлы администратора: Смотри в следующей статье!

Файлы фронтенда сайта

Вы можете заметить, что некоторые имена папок и файлов Joomla 4.x начинаются с заглавных букв, а другие начинаются с строчных букв. J4 также имеет отличную от J3 структуру. Возможно, в продакшен версии Joomla 4 все имена будут приведены к единому виду.

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

tmpl файлы (Файлы вида)

Файлы tmpl содержат код, отображающий виды страниц. Их должно быть проще всего объяснить и понять. Структура файла tmpl в исходном коде выглядит так:

site
     tmpl
          mywalk
              default.php
          mywalks
              default-items.php
              default.php
              default.xml

Вид отображения одной прогулки - tmpl/mywalk/default.php:

<?php
/**
 * @package     Mywalks.Site
 * @subpackage  com_mywalks
 *
 * @copyright   Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

defined('_JEXEC') or die;

//use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;

?>
<div class="page-header">
    <h1><?php echo $this->item->title; ?></h1>
</div>

<p><?php echo $this->item->description; ?>!</p>

<h2><?php echo Text::_('COM_MYWALKS_WALK_REPORTS'); ?></h2>

<div class="table-responsive">
  <table class="table table-striped">
  <caption><?php echo Text::_('COM_MYWALKS_WALK_REPORTS'); ?></caption>
  <thead>
    <tr>
        <th scope="col"><?php echo Text::_('COM_MYWALKS_WALK_DATE'); ?></th>
        <th scope="col"><?php echo Text::_('COM_MYWALKS_WALK_WEATHER'); ?></th>
    </tr>
    </thead>
    <tbody>
    <?php foreach ($this->reports as $id => $report) : ?>
    <tr>
        <td><?php echo $report->date; ?></td>
        <td><?php echo $report->weather; ?></td>
    </tr>
    <?php endforeach; ?><?php //endif; ?>
    </tbody>
  </table>
</div>

Для новичков в Joomla: каждый php-файл начинается с DocBlock, используемого в автоматизированной документации; в файлах с пространством имен следующий оператор - это пространство имен, которое не используется в файлах tmpl; первый исполняемый оператор должен быть всегда определён ('_ JEXEC') или дальше скрипт не работает; что гарантирует, что файл загружен Joomla, а не вызывается напрямую через веб-адрес.

Остальные строки выводят название прогулки, описание и список посещений, извлеченных из базы данных. Оператор use Joomla\CMS\Language\Text загружает класс, который преобразует строковые ключи в строковые значения. Оператор use Joomla\CMS\HTML\HTMLHelper; закомментирован, потому что в этом файле не используется ни одно из множества украшений HTML. Посмотрите ради интереса на файл, чтобы узнать, что он делает: site_root/libraries/src/HTML/HTMLHelper.php.

Вид списка прогулок - tmpl/mywalks/default.php

<?php
/**
 * @package     Mywalks.Site
 * @subpackage  com_mywalks
 *
 * @copyright   Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

defined('_JEXEC') or die;

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
//use Joomla\CMS\Layout\LayoutHelper;

HTMLHelper::_('behavior.core');

?>
<h1><?php echo Text::_('COM_MYWALKS_LIST_PAGE_HEADING'); ?></h1>
<div class="com-contact-categories categories-list">
    <?php
        echo $this->loadTemplate('items');
    ?>
</div>

Обратите внимание, что операторы use загружают дополнительные файлы php, используя их пространства имен. Joomla\CMS\HTML\HTMLHelper добавляет файлы, используемые при отображении страницы, например файлы Javascript, необходимые для сортировки таблиц. Joomla\CMS\Language\Text добавляет файл, используемый для преобразования фиксированных строковых ключей в их английские значения. Joomla\CMS\Layout\LayoutHelper был скопирован сюда, когда во время разработки использовалось копирование и вставка из другого места. Он оставлен, но закомментирован, чтобы проиллюстрировать, что у меня может быть много случаев случайного кода, который ничего не делает, кроме использования ресурсов сервера.

Этот файл выводит заголовок страницы, а затем загружает другой файл, default-items.php, который отображает список прогулок. $this->loadTemplate('items') использует код библиотеки, чтобы найти файл default_items.php в том же каталоге, в котором он был вызван.

Список items - tmpl/mywalks/default_items.php

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

<?php
/**
 * @package     Mywalks.Site
 * @subpackage  com_mywalks
 *
 * @copyright   Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

defined('_JEXEC') or die;

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use J4xdemos\Component\Mywalks\Site\Helper\RouteHelper as MywalksHelperRoute;

?>
<div class="table-responsive">
  <table class="table table-striped">
  <caption><?php echo Text::_('COM_MYWALKS_LIST_TABLE_CAPTION'); ?></caption>
  <thead>
    <tr>
        <th scope="col"><?php echo Text::_('COM_MYWALKS_LIST_TITLE'); ?></th>
        <th scope="col"><?php echo Text::_('COM_MYWALKS_LIST_DESCRIPTION'); ?></th>
        <th scope="col"><?php echo Text::_('COM_MYWALKS_LIST_DISTANCE'); ?></th>
        <th scope="col"><?php echo Text::_('COM_MYWALKS_LIST_LAST_VISIT'); ?></th>
        <th scope="col"><?php echo Text::_('COM_MYWALKS_LIST_NVISITS'); ?></th>
    </tr>
    </thead>
    <tbody>
    <?php foreach ($this->items as $id => $item) :
        $slug = preg_replace('/[^a-z\d]/i', '-', $item->title);
        $slug = strtolower(str_replace(' ', '-', $slug));
    ?>
    <tr>
        <td><a href="/<?php echo Route::_(MywalksHelperRoute::getWalkRoute($item->id, $slug)); ?>">
        <?php echo $item->title; ?></a></td>
        <td><?php echo $item->description; ?></td>
        <td><?php echo $item->distance; ?></td>
        <td><?php echo $item->last_visit //$item->lastvisit; ?></td>
        <td><?php echo $item->nvisits; ?></td>
    </tr>
    <?php endforeach; ?><?php //endif; ?>
    </tbody>
  </table>
</div>

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

Это отрывок из функции getWalkRoute:

    public static function getWalkRoute($id, $slug, $language = 0, $layout = null)
    {
        // Создание URL ссылки
        $link = 'index.php?option=com_mywalks&view=mywalk&id=' . $id . '&slug=' . $slug;

        if ($language && $language !== '*' && Multilanguage::isEnabled())
        {
            $link .= '&lang=' . $language;
        }

        if ($layout)
        {
            $link .= '&layout=' . $layout;
        }

        return $link;
    }

Получение данных - файлы HtmlView

Предполагается, что файлы tmpl имеют дело исключительно с html. Любые данные, необходимые для создания html, такие как список прогулок, должны храниться в переменных в файлах HtmlView, где они становятся доступными в объекте $this.

Файл HtmlView.php для просмотра одиночной прогулки

<?php
/**
 * @package     Mywalks.Site
 * @subpackage  com_mywalks
 *
 * @copyright   Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace J4xdemos\Component\Mywalks\Site\View\Mywalk;

defined('_JEXEC') or die;

//use Joomla\CMS\HTML\HTMLHelper;
//use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;

/**
 * HTML Mywalk View class для компонента Mywalks
 *
 * @since  1.5
 */
class HtmlView extends BaseHtmlView
{
    /**
     * Состояние модели item
     *
     * @var    \Joomla\Registry\Registry
     * @since  1.6
     */
    protected $state;

    /**
     * Детали объекта item
     *
     * @var    \JObject
     * @since  1.6
     */
    protected $item;
    protected $reports;

    /**
     * Выполнить и отобразить шаблон по сценарию.
     *
     * @param   string  $tpl  Имя анализируемого файла шаблона; автоматически просматривает пути к шаблонам.
     *
     * @return  mixed  Строка в случае успеха, в противном случае - объект ошибки.
     */
    public function display($tpl = null)
    {
        $state      = $this->get('State');
        $item       = $this->get('Item');
        $reports    = $this->get('Reports');

        $this->state       = &$state;
        $this->item        = &$item;
        $this->reports     = &$reports;

        // Проверка на ошибки.
        if (count($errors = $this->get('Errors')))
        {
            throw new GenericDataException(implode("\n", $errors), 500);
        }

        return parent::display($tpl);
    }
}

Функция display очень проста. Он извлекает из модели данные о состоянии, одиночной прогулке и отчетах об этой прогулке. Если какой-либо из шагов извлечения данных возвращает ошибку, он генерирует исключение, что обычно приводит к появлению какой-либо страницы с сообщением об ошибке. В противном случае управление передается через Joomla в файл tmpl для создания вывода html. Файлы HtmlView могут быть довольно сложными.

Файл HtmlView для списка прогулок

<?php
/**
 * @package     Mywalks.Site
 * @subpackage  com_mywalks
 *
 * @copyright   Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace J4xdemos\Component\Mywalks\Site\View\Mywalks;

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
//use Joomla\CMS\HTML\HTMLHelper;
//use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
//use Joomla\CMS\Router\Route;

/**
 * Список прогулок View class
 *
 * @since  1.6
 */
class HtmlView extends BaseHtmlView
{
    /**
     * Состояние модели элемента item
     *
     * @var    \Joomla\Registry\Registry
     * @since  1.6.0
     */
    protected $state;

    /**
     * Детали элемента item
     *
     * @var    \JObject
     * @since  1.6.0
     */
    protected $items;

    /**
     * Объект разбивки на страницы
     *
     * @var    \JPagination
     * @since  1.6.0
     */
    protected $pagination;

    /**
     * Параметры страницы
     *
     * @var    \Joomla\Registry\Registry|null
     * @since  4.0.0
     */
    protected $params = null;

    /**
     * Метод отображения представления.
     *
     * @param   string  $tpl  Имя анализируемого файла шаблона; автоматически просматривает пути к шаблонам.
     *
     * @return  mixed  \Исключение при неудаче, недействительно при успехе.
     *
     * @since   1.6
     */
    public function display($tpl = null)
    {
        $app    = Factory::getApplication();
        $params = $app->getParams();

        // Get some data from the models
        $state      = $this->get('State');
        $items      = $this->get('Items');
        $pagination = $this->get('Pagination');

        // Флаг указывает на то, что не следует добавлять limitstart=0 в URL
        $pagination->hideEmptyLimitstart = true;

        // Проверка на ошибки. 
        if (count($errors = $this->get('Errors')))
        {
            throw new GenericDataException(implode("\n", $errors), 500);
        }

        $this->state      = &$state;
        $this->items      = &$items;
        $this->params     = &$params;
        $this->pagination = &$pagination;

        return parent::display($tpl);
    }
}

Готовы к моделям?

Получение данных - файлы модели

Для модели одиночной прогулки нам нужен файл модели, который реализует populateState, getItem и getVisits. Для списка прогулок нам нужны populateState, getListQuery, getItems и некоторые другие для сортировки по столбцам и разбивки на страницы длинных списков, ни один из которых не реализован в этом руководстве.

Файл модели: MywalkModel

<?php
/**
 * @package     Mywalks.Site
 * @subpackage  com_mywalks
 *
 * @copyright   Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace J4xdemos\Component\Mywalks\Site\Model;

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\ItemModel;

/**
 * Mywalk Component Mywalk Model
 *
 * @since  1.5
 */
class MywalkModel extends ItemModel
{
    /**
     * Строка контекста модели.
     *
     * @var        string
     */
    protected $_context = 'com_mywalks.mywalk';

    /**
     * Метод автоматического заполнения состояния модели.
     *
     * Примечание. Вызов getState в этом методе приведет к рекурсии.
     *
     * @since   1.6
     *
     * @return void
     */
    protected function populateState()
    {
        $app = Factory::getApplication();

        // Состояние загрузки из запроса.
        $pk = $app->input->getInt('id');
        $this->setState('mywalk.id', $pk);

        $offset = $app->input->getUInt('limitstart');
        $this->setState('list.offset', $offset);

        // Загрузка параметров.
        $params = $app->getParams();
        $this->setState('params', $params);
    }

    /**
     * Метод получения данных о прогулке.
     *
     * @param   integer  $pk  Идентификатор прогулки.
     *
     * @return  object|boolean  Объект данных пункта меню в случае успеха, false
     */
    public function getItem($pk = null)
    {
        $pk = (!empty($pk)) ? $pk : (int) $this->getState('mywalk.id');

            try
            {
                $db = $this->getDbo();
                $query = $db->getQuery(true)
                    ->select(
                        $this->getState(
                            'item.select', 'a.*'
                        )
                    );
                $query->from('#__mywalks AS a')
                    ->where('a.id = ' . (int) $pk);

                $db->setQuery($query);

                $data = $db->loadObject();

                if (empty($data))
                {
                    throw new \Exception(Text::_('COM_MYWALKS_ERROR_WALK_NOT_FOUND'), 404);
                }
            }
            catch (\Exception $e)
            {
                if ($e->getCode() == 404)
                {
                    // Чтобы перенаправление отработало, необходимо пройти через обработчик ошибок.
                    throw new \Exception($e->getMessage(), 404);
                }
                else
                {
                    $this->setError($e);
                    $this->_item[$pk] = false;
                }
            }

        return $data;
    }
    /**
     * Метод получения данных о посещениях с прогулкой.
     *
     * @param   integer  $pk  The id of the walk.
     *
     * @return  object|boolean  Объект данных пункта меню в случае успеха, false
     */
    public function getReports($pk = null)
    {
        $pk = (!empty($pk)) ? $pk : (int) $this->getState('mywalk.id');

        try
        {
            $db = $this->getDbo();
            $query = $db->getQuery(true)
            ->select('b.*');
            $query->from('#__mywalk_dates AS b')
            ->where('b.walk_id = ' . (int) $pk);
            $query->order('`date` DESC');

            $db->setQuery($query);

            $data = $db->loadObjectList();

            // Совершенно нормально прогуляться без данных о посещениях - обработка вид.
        }
        catch (\Exception $e)
        {
            if ($e->getCode() == 404)
            {
                // Чтобы перенаправление работало, необходимо пройти через обработчик ошибок.
                throw new \Exception($e->getMessage(), 404);
            }
            else
            {
                $this->setError($e);
                $this->_item[$pk] = false;
            }
        }

        return $data;
    }
}

Файл модели: MywalksModel

<?php
/**
 * @package     Mywalks.Site
 * @subpackage  com_mywalks
 *
 * @copyright   Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace J4xdemos\Component\Mywalks\Site\Model;

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\ListModel;

/**
 * Эта модель поддерживает получение списков статей.
 *
 * @since  1.6
 */
class MywalksModel extends ListModel
{
    /**
     * Constructor.
     *
     * @param   array  $config  Необязательный ассоциативный массив настроек конфигурации.
     *
     * @see     \JController
     * @since   1.6
     */
    public function __construct($config = array())
    {
        if (empty($config['filter_fields']))
        {
            $config['filter_fields'] = array(
                'id', 'a.id',
                'title', 'a.title',
            );
        }

        parent::__construct($config);
    }

    /**
     * Метод автоматического заполнения состояния модели.
     *
     * Этот метод следует вызывать только один раз для каждого экземпляра и
     * предназначен для вызова при первом вызове метода getState(),
     * если не установлен флаг конфигурации модели для игнорирования запроса.
     *
     * Примечание. Вызов getState в этом методе приведет к рекурсии.
     *
     * @param   string  $ordering   Необязательное поле для сортировки.
     * @param   string  $direction  Необязательное направление сортировки (asc | desc).
     *
     * @return  void
     *
     * @since   3.0.1
     */
    protected function populateState($ordering = 'ordering', $direction = 'ASC')
    {
        $app = Factory::getApplication();

        // Информация о состоянии списка
        $value = $app->input->get('limit', $app->get('list_limit', 0), 'uint');
        $this->setState('list.limit', $value);

        $value = $app->input->get('limitstart', 0, 'uint');
        $this->setState('list.start', $value);

        $orderCol = $app->input->get('filter_order', 'a.id');

        if (!in_array($orderCol, $this->filter_fields))
        {
            $orderCol = 'a.id';
        }

        $this->setState('list.ordering', $orderCol);

        $listOrder = $app->input->get('filter_order_Dir', 'ASC');

        if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', '')))
        {
            $listOrder = 'ASC';
        }

        $this->setState('list.direction', $listOrder);

        $params = $app->getParams();
        $this->setState('params', $params);

        //$this->setState('layout', $app->input->getString('layout'));
    }

    /**
     * Метод получения идентификатора на основе состояния конфигурации модели.
     *
     * Это необходимо, потому что модель используется компонентом
     * и разными модулями, которым могут потребоваться
     * разные наборы данных или разные требования к порядку.
     *
     * @param   string  $id  Префикс для идентификатора id.
     *
     * @return  string  Идентификатор id.
     *
     * @since   1.6
     */
    protected function getStoreId($id = '')
    {
        // Получение id.

        return parent::getStoreId($id);
    }

    /**
     * Получить главный запрос для получения списка прогулок в зависимости от состояния модели.
     *
     * @return  \JDatabaseQuery
     *
     * @since   1.6
     */
    protected function getListQuery()
    {
        // Получить текущего пользователя для проверки авторизации
        $user = Factory::getUser();

        // Создать новый объект запроса.
        $db    = $this->getDbo();
        $query = $db->getQuery(true);

        // Выберать необходимые поля из таблицы.
        $query->select(
            $this->getState(
                'list.select',
                'a.*,
                (SELECT MAX(`date`) from #__mywalk_dates WHERE walk_id = a.id) AS last_visit,
                (SELECT count(`date`) from #__mywalk_dates WHERE walk_id = a.id) AS nvisits
                ')
        );
        $query->from('#__mywalks AS a');

        $params      = $this->getState('params');

        // Добавить данные о порядке списка.
        $query->order($this->getState('list.ordering', 'a.id') . ' ' . $this->getState('list.direction', 'ASC'));

        return $query;
    }

    /**
     * Метод получения списка прогулок.
     *
     * Переопределение для вставки преобразования поля attribs в объект \JParameter.
     *
     * @return  mixed  Массив объектов в случае успеха, false в случае неудачи.
     *
     * @since   1.6
     */
    public function getItems()
    {
        $items  = parent::getItems();
        return $items;
    }

    /**
     * Метод получения начального количества элементов для набора данных.
     *
     * @return  integer  Начальное количество элементов, доступных в наборе данных.
     *
     * @since   3.0.1
     */
    public function getStart()
    {
        return $this->getState('list.start');
    }
}

Control Flow

Запуск компонента - Контроллер

Стоит помнить, что URL-адрес страницы со списком прогулок, не относящийся к SEF, - это index.php?option=com_mywalks&task=display&view=mywalks. Часть task часто не учитывается, и в этом случае устанавливается task по умолчанию для вида. Если часть вида не указана, компонент должен установить вид по умолчанию.

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

DisplayController

<?php
/**
 * @package     Mywalks.Site
 * @subpackage  com_mywalks
 *
 * @copyright   Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace J4xdemos\Component\Mywalks\Site\Controller;

defined('_JEXEC') or die;

use Joomla\CMS\MVC\Controller\BaseController;

/**
 * Mywalks Component Controller
 *
 * @since  1.5
 */
class DisplayController extends BaseController
{
    /**
     * Method to display a view.
     *
     * @param   boolean  $cachable   Если true, вывод представления будет кэширован.
     * @param   array    $urlparams  Массив безопасных параметров URL и их типов переменных, допустимые значения см. {@link \JFilterInput::clean()}.
     *
     * @return  static  Этот объект поддерживает chaining.
     *
     * @since   1.5
     */
    public function display($cachable = false, $urlparams = array())
    {
        return parent::display();
    }
}

Основные файлы администратора

Хотя мы все еще разрабатываем код для отображения сайта, необходим некоторый код администратора. Файл services/provider.php используется для загрузки компонента, либо для отображения его собственных представлений сайта, либо для использования модулем меню для создания пунктов меню.

The services provider file: administrator/components/com_mywalks/services/provider.php

Обратите особое внимание на строки, начинающиеся с $container->registerServiceProvider, поскольку именно здесь ваш код регистрируется в контейнере для использования позже.

<?php
/**
 * @package     Mywalks.Administrator
 * @subpackage  com_mywalks
 *
 * @copyright   Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

defined('_JEXEC') or die;

//use Joomla\CMS\Categories\CategoryFactoryInterface;
use Joomla\CMS\Component\Router\RouterFactoryInterface;
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
use Joomla\CMS\Extension\ComponentInterface;
//use Joomla\CMS\Extension\MVCComponent;
use Joomla\CMS\Extension\Service\Provider\CategoryFactory;
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
use Joomla\CMS\Extension\Service\Provider\MVCFactory;
use Joomla\CMS\Extension\Service\Provider\RouterFactory;
use Joomla\CMS\HTML\Registry;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use J4xdemos\Component\Mywalks\Administrator\Extension\MywalksComponent;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;

/**
 * The mywalks service provider.
 *
 * @since  4.0.0
 */
return new class implements ServiceProviderInterface
{
    /**
     * Registers the service provider with a DI container.
     *
     * @param   Container  $container  The DI container.
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public function register(Container $container)
    {
        $container->registerServiceProvider(new CategoryFactory('\\J4xdemos\\Component\\Mywalks'));
        $container->registerServiceProvider(new MVCFactory('\\J4xdemos\\Component\\Mywalks'));
        $container->registerServiceProvider(new ComponentDispatcherFactory('\\J4xdemos\\Component\\Mywalks'));
        $container->registerServiceProvider(new RouterFactory('\\J4xdemos\\Component\\Mywalks'));
        $container->set(
                ComponentInterface::class,
                function (Container $container)
                {
                    $component = new MywalksComponent($container->get(ComponentDispatcherFactoryInterface::class));

                    $component->setRegistry($container->get(Registry::class));
                    $component->setMVCFactory($container->get(MVCFactoryInterface::class));
//                  $component->setCategoryFactory($container->get(CategoryFactoryInterface::class));
                    $component->setRouterFactory($container->get(RouterFactoryInterface::class));

                    return $component;
        }
        );
    }
};

The component boot file: administrator/components/com_mywalks/src/Extension/MywalksComponent.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_mywalks
 *
 * @copyright   Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace J4xdemos\Component\Mywalks\Administrator\Extension;

defined('JPATH_PLATFORM') or die;

use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Component\Router\RouterServiceInterface;
use Joomla\CMS\Component\Router\RouterServiceTrait;
use Joomla\CMS\Extension\BootableExtensionInterface;
use Joomla\CMS\Extension\MVCComponent;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLRegistryAwareTrait;
use Psr\Container\ContainerInterface;

/**
 * Component class for com_mywalks
 *
 * @since  4.0.0
 */
class MywalksComponent extends MVCComponent implements
BootableExtensionInterface, RouterServiceInterface
{
    use RouterServiceTrait;
    use HTMLRegistryAwareTrait;

    /**
     * Загрузка расширения. Это функция для настройки среды расширения,
     * например, для регистрации новых загрузчиков классов и т.д.
     *
     * При необходимости можно выполнить некоторую первоначальную
     * настройку из служб контейнера, например. регистрация HTML-сервисов. 
     *
     * @param   ContainerInterface  $container  The container
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public function boot(ContainerInterface $container)
    {
        //$this->getRegistry()->register('mywalksadministrator', new AdministratorService);
    }
}

На данный момент обращение вызова к регистрации Administrator Service закомментирован. Это приводит к ошибке времени выполнения при вызове компонента Mywalks из интерфейса администратора. См. часть 2.

The Component Router

На этом этапе работает компонент com_mywalks. Для перехода к списку прогулок нужен один пункт меню. Есть загвоздка: в списке прогулок ссылки на отдельные прогулки примерно такие:

/site-root/my-walks.html?view=mywalk&id=1

(где корень сайта мой или не может быть деревом вложенных папок). Пришло время сделать собственный роутер SEF? И сделайте перерыв, чтобы прочитать Поддержка URL-адресов SEF в вашем компоненте. У меня есть другой пакет Joomla, который использует URL-адреса SEF в форме [domain]/XXX/YY/page-title.html, где XXX - это код филиала организации, а YY - код языка. Некоторые ветки используют несколько языков. Нестандартно! Да, но именно об этом и просил заказчик.

Для компонента mywalks я хочу использовать отдельные URL-адреса прогулки, например:

/site-root/mywalks/walk-n/walk-title.html

Где n - индивидуальный идентификатор прогулки, а название прогулки автоматически генерируется из фактического названия. На самом деле ни walk-title, ни .html не нужны. Первое - за дружелюбие, второе - за то, что я старомоден.

Нет пунктов меню для индивидуальных прогулок. Они никому не нужны, и их невозможно создать. Требуется настраиваемый маршрутизатор, состоящий из двух файлов: Router.php и MywalksNomenuRules.php.

The Router File: component/com_mywalks/src/Service/Router.php

<?php
/**
 * @package     Mywalks.Site
 * @subpackage  com_mywalks
 *
 * @copyright   Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace J4xdemos\Component\Mywalks\Site\Service;

defined('_JEXEC') or die;

use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Categories\CategoryFactoryInterface;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Component\Router\RouterView;
use Joomla\CMS\Component\Router\RouterViewConfiguration;
use Joomla\CMS\Component\Router\Rules\MenuRules;
//use Joomla\CMS\Component\Router\Rules\NomenuRules;
use J4xdemos\Component\Mywalks\Site\Service\MywalksNomenuRules as NomenuRules;
use Joomla\CMS\Component\Router\Rules\StandardRules;
use Joomla\CMS\Menu\AbstractMenu;
use Joomla\Database\DatabaseInterface;

/**
 * Routing class of com_mywalks
 *
 * @since  3.3
 */
class Router extends RouterView
{
    protected $noIDs = false;

    /**
     * The category factory
     *
     * @var CategoryFactoryInterface
     *
     * @since  4.0.0
     */
    private $categoryFactory;

    /**
     * The db
     *
     * @var DatabaseInterface
     *
     * @since  4.0.0
     */
    private $db;

    /**
     * Mywalks Component router constructor
     *
     * @param   SiteApplication           $app              Объект приложения
     * @param   AbstractMenu              $menu             Объект меню для работы
     * @param   CategoryFactoryInterface  $categoryFactory  Объект категории
     * @param   DatabaseInterface         $db               Объект базы данных
     */
    public function __construct(SiteApplication $app, AbstractMenu $menu,
            CategoryFactoryInterface $categoryFactory, DatabaseInterface $db)
    {
        $this->categoryFactory = $categoryFactory;
        $this->db              = $db;

        $params = ComponentHelper::getParams('com_mywalks');
        $this->noIDs = (bool) $params->get('sef_ids');

        $mywalks = new RouterViewConfiguration('mywalks');
        $mywalks->setKey('id');
        $this->registerView($mywalks);

        $mywalk = new RouterViewConfiguration('mywalk');
        $mywalk->setKey('id');
        $this->registerView($mywalk);

        parent::__construct($app, $menu);

        $this->attachRule(new MenuRules($this));
        $this->attachRule(new StandardRules($this));
        $this->attachRule(new NomenuRules($this));
    }
}

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

    use Joomla\Component\Mywalks\Site\Service\MywalksNomenuRules as NomenuRules;
    ...
            $this->attachRule(new NomenuRules($this));

Правила включают функцию build для создания ссылок на отдельные прогулки и функцию синтаксического анализа для преобразования входящего URL-адреса SEF во внутренний маршрут Joomla. Не нужно беспокоиться о ссылке в пункте меню, так как это регулируется правилами MenuRules.

The Router Rules file: components/my_walks/src/Service/MywalksNomenuRules.php

<?php
/**
 * Joomla! Content Management System
 *
 * @copyright  Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace J4xdemos\Component\Mywalks\Site\Service;

defined('JPATH_PLATFORM') or die;

use Joomla\CMS\Component\Router\RouterView;
use Joomla\CMS\Component\Router\Rules\RulesInterface;

/**
 * Правило обработки URL без пункта меню
 *
 * @since  3.4
 */
class MywalksNomenuRules implements RulesInterface
{
    /**
     * Маршрутизатор, которому принадлежит это правило
     *
     * @var RouterView
     * @since 3.4
     */
    protected $router;

    /**
     * Class constructor.
     *
     * @param   RouterView  $router  Маршрутизатор, которому принадлежит это правило
     *
     * @since   3.4
     */
    public function __construct(RouterView $router)
    {
        $this->router = $router;
    }

    /**
     * Dummymethod для выполнения требований интерфейса
     *
     * @param   array  &$query  Массив запросов для обработки
     *
     * @return  void
     *
     * @since   3.4
     * @codeCoverageIgnore
     */
    public function preprocess(&$query)
    {
        $test = 'Test';
    }

    /**
     * Parse a menu-less URL
     *
     * @param   array  &$segments  Сегменты URL для анализа
     * @param   array  &$vars      Части, полученные в результате сегментации
     *
     * @return  void
     *
     * @since   3.4
     */
    public function parse(&$segments, &$vars)
    {
        //with this url: http://localhost/j4x/my-walks/mywalk-n/walk-title.html
        // segments: [[0] => mywalk-n, [1] => walk-title]
        // vars: [[option] => com_mywalks, [view] => mywalks, [id] => 0]

        $vars['view'] = 'mywalk';
        $vars['id'] = substr($segments[0], strpos($segments[0], '-') + 1);
        array_shift($segments);
        array_shift($segments);
        return;
    }

    /**
     * Создание URL-адреса без меню
     *
     * @param   array  &$query     Части, которые нужно преобразовать
     * @param   array  &$segments  Сегменты URL для сборки
     *
     * @return  void
     *
     * @since   3.4
     */
    public function build(&$query, &$segments)
    {
        // content of $query ($segments is empty or [[0] => mywalk-3])
        // when called by the menu: [[option] => com_mywalks, [Itemid] => 126]
        // when called by the component: [[option] => com_mywalks, [view] => mywalk, [id] => 1, [Itemid] => 126]
        // when called from a module: [[option] => com_mywalks, [view] => mywalks, [format] => html, [Itemid] => 126]
        // when called from breadcrumbs: [[option] => com_mywalks, [view] => mywalks, [Itemid] => 126]

        // the url should look like this: /site-root/mywalks/walk-n/walk-title.html

        // if the view is not mywalk - the single walk view
        if (!isset($query['view']) || (isset($query['view']) && $query['view'] !== 'mywalk') || isset($query['format']))
        {
            return;
        }
        $segments[] = $query['view'] . '-' . $query['id'];
        // последняя часть URL-адреса может отсутствовать
        if (isset($query['slug'])) {
            $segments[] = $query['slug'];
            unset($query['slug']);
        }
        unset($query['view']);
        unset($query['id']);
    }
}

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

И наконец

То есть он - рабочий компонент, но работает пока только на стороне фронтенда сайта!

Joomla 4: Mywalks, Часть 1 - Код сайта

Читать дальше >> «Учебное пособие по компонентам Joomla 4: Mywalks, Часть 2 - Код админки»

Перевод с аглицкого:
https://docs.joomla.org/Part_1:_The_Site_code

Заберите ссылку на статью к себе, чтобы потом легко её найти!
Раз уж досюда дочитали, то может может есть желание рассказать об этом месте своим друзьям, знакомым и просто мимо проходящим?
Не надо себя сдерживать! ;)

Старт! Горячий старт на просторы интернета
Старт! Горячий старт на просторы интернета
Старт! Меню