Учебное пособие на примере компонента 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.
<?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