Учебное пособие на примере компонента J4 Mywalks

Компонент, который имеет два представления сайта: список прогулок и детали отдельных прогулок. Без излишеств: без ввода данных со сторонних сайтов, без оценок, без счетчиков посещений, без категорий и без другого функционала Joomla. Компоненту нужны две таблицы базы данных: список прогулок и список индивидуальных посещений. Название компонента com_mywalks и названия таблиц #__mywalks и #__mywalks_dates.

Код компонента можно скачать отсюда:

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

При первой установке в сообщениях об успешной установке указано COM_MYWALKS_XML_DESCRIPTION. Это проблема установщика, и беспокоиться не о чем. Просто выберите пункт Mywalks в меню Компоненты админки, чтобы увидеть пример данных Список Walks.

Таблица mywalks

Во время установки следующие SQL-операторы создают таблицы, используемые компонентом Mywalks:

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;

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

Таблица 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;

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

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

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

com_mywalks.zip
     admin
     site
     mywalks.xml

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

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

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

<?xml version="1.0" encoding="UTF-8"?>
<extension type="component" method="upgrade">
	<name>com_mywalks</name>
	<!-- The following elements are optional and free of formatting conttraints -->
	<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-2023 Clifford E Ford, All rights reserved.</copyright>
	<license>GNU/GPL Version 2 or later - http://www.gnu.org/licenses/gpl-2.0.html</license>
	<!--  The version string is recorded in the components table -->
	<version>0.3.0</version>
	<!-- The description is optional and defaults to the name -->
	<description>COM_MYWALKS_XML_DESCRIPTION</description>
	<namespace path="src">J4xdemos\Component\Mywalks</namespace>

	<install> <!-- Runs on install -->
		<sql>
			<file driver="mysql" charset="utf8">sql/install.mysql.sql</file>
		</sql>
	</install>
	<uninstall> <!-- Runs on uninstall -->
		<sql>
			<file driver="mysql" charset="utf8">sql/uninstall.mysql.sql</file>
		</sql>
	</uninstall>

	<!-- Site Main File Copy Section -->
	<!-- Note the folder attribute: This attribute describes the folder
		to copy FROM in the package to install therefore files copied
		in this section are copied from /site/ in the package -->

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

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

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

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

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

Языковые файлы

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

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

Содержимое файла-GB.com_mywalks.ini:

COM_MYWALKS_ERROR_WALK_NOT_FOUND="Walk not found!"
COM_MYWALKS_LIST_PAGE_HEADING="List of Walks"
COM_MYWALKS_LIST_TITLE="Title"
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_TABLE_CAPTION="List of Walks"
COM_MYWALKS_MYWALKS_FILTER_SEARCH_TITLE_DESC="Search in Title"
COM_MYWALKS_WALK_BACK_TO_WALKS="Back to list of walks"
COM_MYWALKS_WALK_REPORTS="Walk Reports"
COM_MYWALKS_WALK_DATE="Visit date"
COM_MYWALKS_WALK_WEATHER="Weather Report"

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

Языковые файлы админки: делаем!

Файлы сайта

Вы можете заметить, что некоторые имена каталогов и файлов 4.x Joomla! начинаются с заглавных букв, а другие - с строчных. J4 также имеет отличную от J3 структуру. Короче говоря, файлы с именами ведущих символов в верхнем регистре разделяются именами, а другие нет. Другая организация в J4 - это.... [ToD].

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

Файлы шаблона tmpl

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

site
     tmpl
          mywalk
              default.php
          mywalks
              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\Language\Text;
use Joomla\CMS\Router\Route;

?>
<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">
  <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>

<a href="/<?php echo Route::_('index.php?option=com_mywalks'); ?>"><?php echo Text::_('COM_MYWALKS_WALK_BACK_TO_WALKS'); ?></a>

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

Оператор use Joomla\CMS\Language\Text определяет местоположение класса, который преобразует строковые ключи в строковые значения. Через вызов функции Text::_() загружается класс когда он необходим. Операторы use почти всегда размещаются сразу под defined('_JEXEC') оператором.

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

Вид списка прогулок - 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;
use Joomla\CMS\Router\Route;
use J4xdemos\Component\Mywalks\Site\Helper\RouteHelper as MywalksHelperRoute;

$listOrder = $this->escape($this->state->get('list.ordering'));
$listDirn  = $this->escape($this->state->get('list.direction'));

?>

<h1><?php echo Text::_('COM_MYWALKS_LIST_PAGE_HEADING'); ?></h1>

<form action="<?php echo Route::_('index.php?option=com_mywalks'); ?>" method="post" name="adminForm" id="adminForm">

<?php echo LayoutHelper::render('joomla.searchtools.default', array('view' => $this)); ?>

<div class="table-responsive">
  <table class="table table-striped">
  <thead>
    <tr>
 		<th scope="col">
			<?php echo HTMLHelper::_('searchtools.sort', 'JGLOBAL_TITLE', 'a.title', $listDirn, $listOrder); ?>
		</th>
		<th scope="col"><?php echo Text::_('COM_MYWALKS_LIST_DESCRIPTION'); ?></th>
		<th scope="col">
			<?php echo HTMLHelper::_('searchtools.sort', 'COM_MYWALKS_LIST_DISTANCE', 'a.distance', $listDirn, $listOrder); ?>
		</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>

<?php echo $this->pagination->getListFooter(); ?>

<input type="hidden" name="task" value="">
<input type="hidden" name="boxchecked" value="0">
<?php echo HTMLHelper::_('form.token'); ?>

</form>

Примечание:

  • Операторы use определяют расположение дополнительных PHP-файлов, используя их пространства имен.
    • Joomla\CMS\HTML\HTMLHelper предназначен для файлов, используемых при отображении страницы, например, файлов Javascript, необходимых для сортировки таблиц.
    • Joomla\CMS\Layout\LayoutHelper используется для создания любого из множества виджетов Joomla.
  • $this->items представляет собой массив, содержащий список прогулок.
  • Слаг (алиас) создается путем преобразования заголовка в строчные буквы и цифры, заменяя пробелы на знаки минус. Он используется для передачи описательного термина в URL-адрес ссылки на список посещений в качестве помощи для поисковой оптимизации.
  • Статический вызов Route используется для создания URL-адреса ссылки на индивидуальное описание прогулки.

Выдержка из функции getWalkRoute:

public static function getWalkRoute($id, $slug, $language = 0, $layout = null)
    {
        // Create the link
        $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\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;

/**
 * HTML Mywalk View class for the Mywalks component
 *
 * @since  1.5
 */
class HtmlView extends BaseHtmlView
{
	/**
	 * The item model state
	 *
	 * @var    \Joomla\Registry\Registry
	 * @since  1.6
	 */
	protected $state;

	/**
	 * The item object details
	 *
	 * @var    \JObject
	 * @since  1.6
	 */
	protected $item;

	/**
	 * The list of visit reports/visit dates for this walk
	 *
	 * @var    \JObject
	 * @since  1.6
	 */
	protected $reports;

	/**
	 * Execute and display a template script.
	 *
	 * @param   string  $tpl  The name of the template file to parse; automatically searches through the template paths.
	 *
	 * @return  mixed  A string if successful, otherwise an Error object.
	 */
	public function display($tpl = null)
	{
		$this->state      = $this->get('State');
		$this->item       = $this->get('Item');
		$this->reports    = $this->get('Reports');

		// Check for errors.
		if (count($errors = $this->get('Errors')))
		{
			throw new GenericDataException(implode("\n", $errors), 500);
		}

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

Функция отображения очень проста. Она извлекает из модели данные о состоянии, одиночной прогулки и отчетах об этой прогулке. Если какой-либо из шагов извлечения данных возвращает ошибку, он генерирует исключение, что обычно приводит к появлению какой-либо страницы сообщения об ошибке. В противном случае управление передается через Joomla в файл tmpl для создания вывода html. Файлы HtmlView могут быть довольно сложными - посмотрите, например, статью о компонентах контента 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\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;

/**
 * Walks List View class
 *
 * @since  1.6
 */
class HtmlView extends BaseHtmlView
{
	/**
	 * The item model state
	 *
	 * @var    \Joomla\Registry\Registry
	 * @since  1.6.0
	 */
	protected $state;

	/**
	 * The item details
	 *
	 * @var    \JObject
	 * @since  1.6.0
	 */
	protected $items;

	/**
	 * The pagination object
	 *
	 * @var    \JPagination
	 * @since  1.6.0
	 */
	protected $pagination;

	/**
	 * The page parameters
	 *
	 * @var    \Joomla\Registry\Registry|null
	 * @since  4.0.0
	 */
	protected $params = null;

	/**
	 * Method to display the view.
	 *
	 * @param   string  $tpl  The name of the template file to parse; automatically searches through the template paths.

	 * @return  mixed  \Exception on failure, void on success.
	 *
	 * @since   1.6
	 */
	public function display($tpl = null)
	{
		// Get data from the model.
		$this->state      = $this->get('State');
		$this->items      = $this->get('Items');
		$this->filterForm    = $this->get('FilterForm');
		$this->activeFilters = $this->get('ActiveFilters');
		$this->pagination = $this->get('Pagination');
		// Flag indicates to not add limitstart=0 to URL
		$this->pagination->hideEmptyLimitstart = true;

		// Check for errors.
		if (count($errors = $this->get('Errors')))
		{
			throw new GenericDataException(implode("\n", $errors), 500);
		}

		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\Database\ParameterType;
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
{
	/**
	 * Model context string.
	 *
	 * @var        string
	 */
	protected $_context = 'com_mywalks.mywalk';

	/**
	 * Method to auto-populate the model state.
	 *
	 * Note. Calling getState in this method will result in recursion.
	 *
	 * @since   1.6
	 *
	 * @return void
	 */
	protected function populateState()
	{
		$app = Factory::getApplication();

		// Load state from the request.
		$pk = $app->input->getInt('id');
		$this->setState('mywalk.id', $pk);
	}

	/**
	 * Method to get walk data.
	 *
	 * @param   integer  $pk  The id of the walk.
	 *
	 * @return  object|boolean  Menu item data object on success, boolean false
	 */
	public function getItem($pk = null)
	{
		$pk = (!empty($pk)) ? $pk : (int) $this->getState('mywalk.id');

			try
			{
				$db = $this->getDatabase();
				$query = $db->getQuery(true)
					->select(
						$this->getState(
							'item.select', 'a.*'
						)
					);
				$query->from($db->quoteName('#__mywalks') . ' AS a')
					->where($db->quoteName('a.id') . ' = :id')
					->bind(':id', $pk, ParameterType::INTEGER);

				$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)
				{
					// Need to go through the error handler to allow Redirect to work.
					throw new \Exception($e->getMessage(), 404);
				}
				else
				{
					$this->setError($e);
					$this->_item[$pk] = false;
				}
			}

		return $data;
	}
	/**
	 * Method to get walk visit data.
	 *
	 * @param   integer  $pk  The id of the walk.
	 *
	 * @return  object|boolean  Menu item data object on success, boolean false
	 */
	public function getReports($pk = null)
	{
		$pk = (!empty($pk)) ? $pk : (int) $this->getState('mywalk.id');

		try
		{
			$db = $this->getDatabase();
			$query = $db->getQuery(true)
			->select('b.*')
			->from($db->quoteName('#__mywalk_dates') . ' AS b')
			->where($db->quoteName('b.walk_id') . ' = :id')
			->bind(':id', $pk, ParameterType::INTEGER)
			->order($db->quoteName('date') . ' DESC');

			$db->setQuery($query);

			$data = $db->loadObjectList();

			// It is OK to have a walk without visit data - handle it the view.
		}
		catch (\Exception $e)
		{
			if ($e->getCode() == 404)
			{
				// Need to go through the error handler to allow Redirect to work.
				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\MVC\Model\ListModel;
use Joomla\Database\ParameterType;

/**
 * This models supports retrieving lists of articles.
 *
 * @since  1.6
 */
class MywalksModel extends ListModel
{
	/**
	 * Constructor.
	 *
	 * @param   array  $config  An optional associative array of configuration settings.
	 *
	 * @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',
				'distance', 'a.distance',
			);
		}

		parent::__construct($config);
	}

	/**
	 * Method to auto-populate the model state.
	 *
	 * This method should only be called once per instantiation and is designed
	 * to be called on the first call to the getState() method unless the model
	 * configuration flag to ignore the request is set.
	 *
	 * Note. Calling getState in this method will result in recursion.
	 *
	 * @param   string  $ordering   An optional ordering field.
	 * @param   string  $direction  An optional direction (asc|desc).
	 *
	 * @return  void
	 *
	 * @since   3.0.1
	 */
	protected function populateState($ordering = 'a.id', $direction = 'ASC')
	{
		$search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search');
		$this->setState('filter.search', $search);

		// List state information.
		parent::populateState($ordering, $direction);
	}

	/**
	 * Method to get a store id based on model configuration state.
	 *
	 * This is necessary because the model is used by the component and
	 * different modules that might need different sets of data or different
	 * ordering requirements.
	 *
	 * @param   string  $id  A prefix for the store id.
	 *
	 * @return  string  A store id.
	 *
	 * @since   1.6
	 */
	protected function getStoreId($id = '')
	{
		// Compile the store id.
		$id .= ':' . $this->getState('filter.search');

		return parent::getStoreId($id);
	}

	/**
	 * Get the master query for retrieving a list of walks subject to the model state.
	 *
	 * @return  \JDatabaseQuery
	 *
	 * @since   1.6
	 */
	protected function getListQuery()
	{

		// Create a new query object.
		$db    = $this->getDatabase();
		$query = $db->getQuery(true);

		// Select the required fields from the table.
		$query->select(
			$this->getState(
				'list.select',
				'a.*,
				(SELECT MAX(' . $db->quoteName('date') 
				. ') FROM ' . $db->quoteName('#__mywalk_dates') 
				. ' WHERE ' . $db->quoteName('walk_id') . ' = ' . $db->quoteName('a.id') . ') AS last_visit,
				(SELECT count(' . $db->quote('date') . ') FROM ' . $db->quoteName('#__mywalk_dates') 
				. ' WHERE ' . $db->quoteName('walk_id') . ' = ' . $db->quoteName('a.id') . ') AS nvisits'
				)
		);
		$query->from($db->quoteName('#__mywalks') . ' AS a');

		// Filter by search in title.
		$search = $this->getState('filter.search');

		if (!empty($search))
		{
			$search = '%' . trim($search) . '%';
			$query->where($db->quoteName('a.title') . ' LIKE :search')
			->bind(':search', $search, ParameterType::STRING);
		}

		// Add the list ordering clause.
		$orderCol  = $this->state->get('list.ordering', 'a.id');
		$orderDirn = $this->state->get('list.direction', 'ASC');

		if ($orderCol === 'title') {
            $ordering = [
                $db->quoteName('a.title') . ' ' . $db->escape($orderDirn),
            ];
        } else {
            $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn);
        }

        $query->order($ordering);

		return $query;
	}
}

Управление

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

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

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

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   If true, the view output will be cached
     * @param   array    $urlparams  An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}.
     *
     * @return  static  This object to support chaining.
     *
     * @since   1.5
     */
    public function display($cachable = false, $urlparams = array())
    {
        return parent::display();
    }
}

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

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

Services provider файл: 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\Component\Router\RouterFactoryInterface;
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
use Joomla\CMS\Extension\ComponentInterface;
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->setRouterFactory($container->get(RouterFactoryInterface::class));

                    return $component;
        }
        );
    }
};

Загрузочный файл компонента: administrator/components/com_mywalks/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;

    /**
     * Booting the extension. This is the function to set up the environment of the extension like
     * registering new class loaders, etc.
     *
     * If required, some initial set up can be done from services of the container, eg.
     * registering HTML services.
     *
     * @param   ContainerInterface  $container  The container
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public function boot(ContainerInterface $container)
    {
        //$this->getRegistry()->register('mywalksadministrator', new AdministratorService);
    }
}

На данный момент вызов службы администратора закомментирован. Это приводит к ошибке времени выполнения при вызове компонента Mywalks из интерфейса администратора. К чему вернемся в части 2.

Роутер компонента

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

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

На данном этапе компонент com_mywalks работает. Нужен один пункт меню для ссылки на список прогулок.

Есть проблема. В списке прогулок, ссылки на отдельные прогулки выглядят так: /site-root/my-walks.html?view=mywalk&id=1 (где site-root может быть или не быть деревом подпапок).

Пора реализовать роутер с поддержкой SEF URL-адресов в вашем компоненте.

Для компонента mywalks использовать отдельные URL-адреса прогулок, например: /site-root/mywalks/walk-n/walk-title.html

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

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

Файл роутера: component/com_mywalks/Service/Router.php

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 для создания ссылок на отдельные прогулки и функцию parse для преобразования входящего URL-адреса SEF во внутренний маршрут Joomla. Не стоит беспокоиться о ссылке в пункте меню, так как это регулируется правилами MenuRules.

Файл правил роутера: components/my_walks/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;

/**
 * Rule to process URLs without a menu item
 *
 * @since  3.4
 */
class MywalksNomenuRules implements RulesInterface
{
    /**
     * Router this rule belongs to
     *
     * @var RouterView
     * @since 3.4
     */
    protected $router;

    /**
     * Class constructor.
     *
     * @param   RouterView  $router  Router this rule belongs to
     *
     * @since   3.4
     */
    public function __construct(RouterView $router)
    {
        $this->router = $router;
    }

    /**
     * Dummymethod to fullfill the interface requirements
     *
     * @param   array  &$query  The query array to process
     *
     * @return  void
     *
     * @since   3.4
     * @codeCoverageIgnore
     */
    public function preprocess(&$query)
    {
        $test = 'Test';
    }

    /**
     * Parse a menu-less URL
     *
     * @param   array  &$segments  The URL segments to parse
     * @param   array  &$vars      The vars that result from the segments
     *
     * @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;
    }

    /**
     * Build a menu-less URL
     *
     * @param   array  &$query     The vars that should be converted
     * @param   array  &$segments  The URL segments to create
     *
     * @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'];
        // the last part of the url may be missing
        if (isset($query['slug'])) {
            $segments[] = $query['slug'];
            unset($query['slug']);
        }
        unset($query['view']);
        unset($query['id']);
    }
} 

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

И наконец

Вот и всё — готова рабочая фронтенд часть компонента!

Фронтенд часть компонента

Вольный перевод, в редакции 2024,
оригинал: https://docs.joomla.org/J4.x:My_Walks_Part_1:_The_Site_code