- Структура файлов сайта
DisplayContoller.php
src/View/Countries/HtmlView.php
Model/CountriesModel.php
tmpl/countries/default.php
tmpl/countries/default.xml
forms/filter_countries.xml
language/en-GB/com_countrybase.ini
src/Service/Router.php
- Анатомия MVC Joomla 4
Структура файлов сайта.
В части компонента "Сайт" (Site) меньше файлов, чем в части "Администратор" (Administrator), поэтому это кажется хорошим местом для начала. Будут рассмотрены только те части каждого файла, которые нуждаются в объяснении. Лучше всего, если вы откроете каждый файл, о котором идет речь, посмотрите на общее содержание, а затем найдете те части, которые нужно объяснить. Структура файлов в алфавитном порядке выглядит следующим образом:
Site
|- forms
|- filter_countries.xml
|- language
|- en-GB
|- com_countrybase.ini
|- src
|- Controller
|- DisplayController
|- Model
|- CountriesModel.php
|- Service
|- Router.php
|- View
|- Countries
|- HtmlView.php
|- tmpl
|- countries
|- default.php
|- default.xml
DisplayContoller.php
<?php
/**
* @package Countrybase.Site
* @subpackage com_countrybase
*
* @copyright (C) 2022 Clifford E Ford
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace J4xdemos\Component\Countrybase\Site\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
/**
* Контроллер компонента Countrybase
*
* @since 4.0.0
*/
class DisplayController extends BaseController
{
/**
* Вид по умолчанию.
*
* @var string
* @since 1.6
*/
protected $default_view = 'countries';
protected $app;
}
Части этого файла можно пояснить следующим образом:
Уведомление об авторских правах.
Каждый php-файл должен начинаться с уведомления об авторских правах, как показано ниже:
<?php
/**
* @package Countrybase.Site
* @subpackage com_countrybase
*
* @copyright (C) 2022 Clifford E Ford
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
Если вы часто создаете новые файлы и копируете/вставляете этот раздел, не забудьте обновить название компонента и уведомление об авторских правах.
Пространство имен и проверка определений.
После предупреждения об авторских правах каждый php-файл должен содержать строку defined('_JEXEC') or die;
за исключением того, что файлы с пространством имен должны объявлять пространство имен (namespace
) перед любым другим php-кодом, то есть перед проверкой defined
. Файлы php с пространством имен - это файлы, содержащие компонентные php-классы в папке src или ее подпапках.
namespace J4xdemos\Component\Countrybase\Site\Controller;
defined('_JEXEC') or die;
Данная проверка предотвращает выполнение php-файла при прямом вызове через его url. Константа _JEXEC
определяется при запуске приложения Joomla 4 через корневой или администраторский файл index.php
. Это важное средство обеспечения безопасности.
В сочетании с пространством имен, объявленным в файле манифеста countrybase.xml
, Joomla будет искать любой класс, объявленный в текущем файле в root/components/com_countrybase/src/Controller
- в данном случае добавляя имя этого файла, DisplayController.php
use Statements
Правила use
обычно следуют за проверкой defined
и часто перечисляются в алфавитном порядке. Декларации use
определяют местоположение классов, используемых данным php-файлом. Иногда операторы use
появляются по ошибке, будучи объявленными, но не используемыми. Это не причиняет вреда, но это следует исправить. Здесь есть две неиспользуемые декларации:
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
Класс контроллера
Контроллер отображения практически ничего не делает, поскольку вся работа выполняется в родительском классе. Единственное, что он делает, это устанавливает представление по умолчанию, в данном случае страны. Это заставит представление компонента по умолчанию использовать файлы Countries/HtmlView.php
и tmpl/countries/default.php
для отображения данных о странах.
/**
* Контроллер компонента Countrybase
*
* @since 4.0.0
*/
class DisplayController extends BaseController
{
/**
* Вид по умолчанию.
*
* @var string
* @since 1.6
*/
protected $default_view = 'countries';
protected $app;
}
Для разметки кода используется стандартная разметка Joomla для php (https://developer.joomla.org/coding-standards/basic-guidelines.html). Блоки документации предназначены для автоматического документирования кода. Значения параметра since
, показанные здесь, предназначены для кода Joomla 4.
Не забудьте установить $default_view
для этого контроллера. Без него DisplayController
будет использовать представление по умолчанию, определенное в конфигурационном файле компонента или, если такового не существует, в имени компонента.
src/View/Countries/HtmlView.php
Контроллер установил представление по умолчанию для countries
, поэтому следующим шагом будет загрузка соответствующего кода HtmlView.php. Части этого файла требуют некоторых пояснений.
Переменные класса.
class HtmlView extends BaseHtmlView
{
/**
* Состояние Модели
*
* @var \Joomla\CMS\Object\CMSObject
*/
protected $state = null;
...
protected $items = null;
...
protected $pagination = null;
...
public $filterForm;
...
public $activeFilters;
Переменные класса используются для хранения информации об отображаемой странице:
$state
- состояние модели, часто устанавливается при вводе формы или из строки запроса.$items
- список данных о стране, полученный из базы данных.$pagination
- объект, используемый для отображения механизма постраничной навигации, если страниц больше, чем лимит списка, обычно 20 элементов.$filterForm
- обычно не встречается на страницах сайта, но используется в com_countrybase для фильтрации по названию страны или опубликованному состоянию.$activeFilters
- используется для отслеживания того, какие фильтры используются.
Функция display.
Это довольно стандартное представление для сайта, за исключением частей filterForm
и activeFilters
:
public function display($tpl = null)
{
$this->state = $this->get('State');
$this->items = $this->get('Items');
$this->pagination = $this->get('Pagination');
$this->filterForm = $this->get('FilterForm');
$this->activeFilters = $this->get('ActiveFilters');
// Флаг указывает не добавлять limitstart=0 к URL
$this->pagination->hideEmptyLimitstart = true;
// Проверка на наличие ошибок.
if (count($errors = $this->get('Errors')))
{
throw new GenericDataException(implode("\n", $errors), 500);
}
parent::display($tpl);
}
Команды вида $this->get('Xxxx')
заставляют Joomla искать в CountriesModel.php
функцию с именем getXxxx()
и возвращать любые данные, выполненные этим кодом, для хранения и использования в представлении. Часто функция находится в родительском CountriesModel
. Например, функция getItems
отсутствует в CountriesModel.php
, но присутствует в ListModel
, которую она расширяет.
В итоге класс представления получает данные из модели, а затем вызывает свой родительский класс для отображения данных.
Model/CountriesModel.php
В Model
есть небольшое количество функций, которые обычно требуют самостоятельного выполнения. Другие наследуются от родительской ListModel
.
constructor
Нормальной практикой является включение в конструктор любых полей фильтра, которые требуется использовать. Без них может показаться, что фильтры не имеют никакого эффекта. О них часто забывают, когда позднее потребуется добавить фильтр. Каждое поле задается как имя поля и псевдоним имени таблицы, обычно a, b, c и т.д., но может быть любым по собственному выбору, согласующимся с кодом в других местах.
public function __construct($config = array())
{
if (empty($config['filter_fields']))
{
$config['filter_fields'] = array(
'id', 'a.id',
'title', 'a.title',
'iso_2', 'a.iso_2',
'iso_3', 'a.iso_3',
'country_code', 'a.country_code',
'region_code', 'a.region_code',
'state', 'a.state',
'subregion_code', 'a.subregion_code',
'phone_prefix', 'a.phone_prefix',
'currency_code', 'a.currency_code',
);
}
parent::__construct($config);
}
populateState
Эта функция принимает входные параметры и подготавливает их для использования в запросе к базе данных. Некоторые параметры обрабатываются в родительской функции, поэтому здесь ничего делать не нужно. Например, если полем поиска является заголовок, он обрабатывается родительской функцией, так же как и поля состояния начала и конца пагинации.
protected function populateState($ordering = 'title', $direction = 'ASC')
{
// Список сведений о состоянии.
parent::populateState($ordering, $direction);
}
getStoreId
Эта функция создает хэш для хранения запроса для использования в других местах.
protected function getStoreId($id = '')
{
// Сборка id хранилища.
$id .= ':' . $this->getState('filter.search');
$id .= ':' . $this->getState('filter.published');
return parent::getStoreId($id);
}
getListQuery
Здесь создается запрос для извлечения данных из базы данных. это требует некоторого понимания того, как строятся запросы.
protected function getListQuery()
{
// Создание нового объекта запроса.
$db = $this->getDbo();
$query = $db->getQuery(true);
// Выбор необходимых полей из таблицы.
$query->select(
$this->getState(
'list.select',
[
$db->quoteName('a.title'),
$db->quoteName('a.iso_2'),
$db->quoteName('a.iso_3'),
$db->quoteName('a.country_code'),
$db->quoteName('a.region_code'),
$db->quoteName('a.subregion_code'),
$db->quoteName('a.phone_prefix'),
$db->quoteName('a.currency_code'),
$db->quoteName('a.state'),
$db->quoteName('b.title') . ' AS currency_title',
$db->quoteName('b.symbol'),
$db->quoteName('b.dollar_exchange_rate'),
]
)
)
->from($db->quoteName('#__countrybase_countries', 'a'))
->leftjoin($db->quoteName('#__countrybase_currencies', 'b') . 'ON a.currency_code = b.currency_code');
// Фильтр по поиску в заголовке.
$search = $this->getState('filter.search');
if (!empty($search))
{
$search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%'));
$query->where('(a.title LIKE ' . $search . ')');
}
// Фильтр по состоянию публикации
$published = (string) $this->getState('filter.published');
if ($published !== '*')
{
if (is_numeric($published))
{
$state = (int) $published;
$query->where($db->quoteName('a.state') . ' = :state')
->bind(':state', $state, ParameterType::INTEGER);
}
}
// Добавление пункта упорядочивания списка.
$orderCol = $this->state->get('list.ordering', 'a.title');
$orderDirn = $this->state->get('list.direction', 'ASC');
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDirn));
return $query;
}
Пояснение
getQuery(true)
получает новый пустой объект запроса.$query->select()
добавляет операторSELECT
. Операторовselect
может быть несколько - Joomla объединяет их.- Псевдонимы таблиц
a
иb
показывают, что некоторые столбцы берутся из разных таблиц. ->from()
определяет, какая таблица является таблицейa
. Это может быть в отдельном операторе:$query->from();
->leftjoin()
определяет таблицуb
и то, как она должна быть присоединена к таблицеa
.$query->where()
использует все заданные фильтры, один для поиска, другой для состояния.return $query
здесь нет родительского вызова, всё в запросе должно быть установлено здесь.
tmpl/countries/default.php
Это часть кода, в которой создается html-содержимое. В самом крайнем случае он может содержать только <h1>Hello World</h1>. Для списка стран необходима таблица с заголовком и одной строкой для данных о каждой стране. Поскольку существует 250 стран, необходим механизм пагинации для отображения подмножества стран по нескольку за раз. Для этого нужна форма. И в этом случае пригодится стандартная панель инструментов поиска Joomla. Вот она:
<?php
/**
* @package Countrybase.Site
* @subpackage com_countrybase
*
* @copyright (C) 2022 Clifford E Ford
* @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;
$listOrder = $this->escape($this->state->get('list.ordering'));
$listDirn = $this->escape($this->state->get('list.direction'));
?>
<h1><?php echo Text::_('COM_COUNTRYBASE_COUNTRIES'); ?></h1>
<form action="<?php echo Route::_('index.php?option=com_countrybase'); ?>" 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">
<caption><?php echo Text::_('COM_COUNTRYBASE_COUNTRIES_TABLE_CAPTION'); ?></caption>
<thead>
<tr>
<th scope="col">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_COUNTRYBASE_COUNTRIES_COUNTRY', 'a.title', $listDirn, $listOrder); ?>
</th>
<th scope="col"><?php echo Text::_('COM_COUNTRYBASE_COUNTRIES_ISO_2'); ?></th>
<th scope="col"><?php echo Text::_('COM_COUNTRYBASE_COUNTRIES_ISO_3'); ?></th>
<th scope="col"><?php echo Text::_('COM_COUNTRYBASE_COUNTRIES_CURRENCY_TITLE'); ?></th>
<th scope="col"><?php echo Text::_('COM_COUNTRYBASE_COUNTRIES_CURRENCY_SYMBOL'); ?></th>
<th scope="col"><?php echo Text::_('COM_COUNTRYBASE_COUNTRIES_CURRENCY_CODE'); ?></th>
<th scope="col"><?php echo Text::_('COM_COUNTRYBASE_COUNTRIES_XRATE'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($this->items as $id => $item) : ?>
<tr>
<td><?php echo $item->title; ?></td>
<td><?php echo $item->iso_2; ?></td>
<td><?php echo $item->iso_3; ?></td>
<td><?php echo $item->currency_title; ?></td>
<td><?php echo $item->symbol; ?></td>
<td><?php echo $item->currency_code; ?></td>
<td><?php echo $item->dollar_exchange_rate; ?></td>
</tr>
<?php endforeach; ?>
</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>
Обратите внимание:
$listOrder
и$listDirection
используются для упорядочивания по заголовку столбца. Лишь заголовок (title) настроен для этого.action
формы обычно настраивается так, чтобы ссылаться на себя.LayoutHelper::render('joomla.searchtools.default',...)
создает строку поиска того типа, который можно увидеть на страницах списков администратора. Ей нужна форма фильтра!$this->pagination->getListFooter()
извлекает html-код для виджета пагинации.task
это скрытое поле (hidden) заполняется javascript при отправке формы.boxchecked
это скрытое поле (hidden) используется, когда один или несколько флажков в строке выбраны для пакетной операции. Здесь оно не очень нужно!HTMLHelper::_('form.token');
получает код для токена формы, используемого в качестве средства защиты при отправке формы, предполагающей ввод данных. Здесь не очень нужен!
tmpl/countries/default.xml
Этот файл используется для создания пункта меню. Он имеет то же имя, что и php-файл, поэтому в данном случае default.xml
.
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<layout title="COM_COUNTRYBASE_VIEW_DEFAULT_MENU_LABEL"
option="COM_COUNTRYBASE_VIEW_DEFAULT_OPTION">
<help
url="components/com_countrybase/help/en-GB/countrybase.html"
/>
<message>
<![CDATA[COM_COUNTRYBASE_VIEW_DEFAULT_MENU_DESC]]>
</message>
</layout>
<!-- Добавление полей в объект параметров для макета. -->
<fields name="params">
<!-- Параметры -->
<fieldset name="options">
</fieldset>
</fields>
</metadata>
Примечания:
help url
указывает на файл справки в папке администратора. Это позволяет создавать собственные внутренние файлы справки, вызываемые из кнопки Help формы редактирования меню после выбора типа меню Countrybase Default View.params
позволяет использовать параметры, например, показывать или нет определенную колонку в списке стран. Пока никаких параметров не указано.- Ключевые фразы переводов должны находиться в файле
administrator/language/en-GB/countrybase.sys.ini
.
forms/filter_countries.xml
Этот файл необходим для строки поиска. Без него Joomla выдаст фатальную ошибку. Имя файла должно быть точно таким, как показано здесь: имя представления (view name), которому предшествует filter_
. Его содержание простое, здесь только определения для поля поиска и любых других фильтров, которые вы можете использовать.
<?xml version="1.0" encoding="utf-8"?>
<form>
<fields name="filter">
<field
name="search"
type="text"
label="COM_COUNTRYBASE_COUNTRIES_FILTER_SEARCH_LABEL"
description="COM_COUNTRYBASE_COUNTRIES_FILTER_SEARCH_DESC"
hint="JSEARCH_FILTER"
/>
<field
name="published"
type="status"
label="JOPTION_SELECT_PUBLISHED"
onchange="this.form.submit();"
>
<option value="">JOPTION_SELECT_PUBLISHED</option>
</field>
</fields>
Обратите внимание, что любые строковые ключи, начинающиеся с J
, определены Joomla, и их не следует включать в языковые файлы.
language/en-GB/com_countrybase.ini
Joomla всегда загружает ключи английского языка перед любым другим языком. Это гарантирует, что ключи не появятся в выводе, если язык переведен не полностью. Поскольку языковые ключи представляют собой длинные слова на псевдоанглийском языке, считается, что лучше иметь смесь английского и другого языка, чем смесь ключей и другого языка. Если используется другой язык, Joomla перезаписывает английские строки строками другого языка.
Обратите внимание, что общепринятой практикой является перечисление строк в алфавитном порядке ключей:
; Joomla! Project
; (C) 2005 Open Source Matters, Inc. <https://www.joomla.org>
; License GNU General Public License version 2 or later; see LICENSE.txt
; Note : All ini files need to be saved as UTF-8
COM_COUNTRYBASE_COUNTRIES_COUNTRY="Country"
COM_COUNTRYBASE_COUNTRIES_CURRENCY_CODE="Code"
COM_COUNTRYBASE_COUNTRIES_CURRENCY_SYMBOL="Symbol"
COM_COUNTRYBASE_COUNTRIES_CURRENCY_TITLE="Currency"
COM_COUNTRYBASE_COUNTRIES_FILTER_COUNTRY_ASC="Country ASC"
COM_COUNTRYBASE_COUNTRIES_FILTER_COUNTRY_DESC="Country DESC"
COM_COUNTRYBASE_COUNTRIES_FILTER_CURRENCY_CODE_ASC="Currency code ASC"
COM_COUNTRYBASE_COUNTRIES_FILTER_CURRENCY_CODE_DESC="Currency code DESC"
COM_COUNTRYBASE_COUNTRIES_FILTER_SEARCH_DESC="Search in Country Name"
COM_COUNTRYBASE_COUNTRIES_FILTER_SEARCH_LABEL="Search"
COM_COUNTRYBASE_COUNTRIES_ISO_2="ISO2"
COM_COUNTRYBASE_COUNTRIES_ISO_3="ISO3"
COM_COUNTRYBASE_COUNTRIES_TABLE_CAPTION="Table of Country Currencies"
COM_COUNTRYBASE_COUNTRIES_XRATE="Exchange Rate"
COM_COUNTRYBASE_COUNTRIES="Countries"
src/Service/Router.php
Router необходим для SEO-урлов. Без него ссылка в меню может выглядеть как option=com_countrybase&view=countries
. С ним ссылка будет отображаться как country-base.html
или любое другое имя, которое будет выбрано для псевдонима заголовка ссылки.
public function __construct(SiteApplication $app, AbstractMenu $menu,
CategoryFactoryInterface $categoryFactory, DatabaseInterface $db)
{
$countries = new RouterViewConfiguration('countries');
$countries->setKey('id');
$this->registerView($countries);
parent::__construct($app, $menu);
$this->attachRule(new MenuRules($this));
$this->attachRule(new StandardRules($this));
$this->attachRule(new NomenuRules($this));
}
Если представлений больше, например, таблица валют, вы определите каждое представление здесь перед оператором parent::__construct()
.
Анатомия MVC Joomla 4.
- «Joomla 4 MVC Anatomy: Начало работы над созданием компонента»
- «Joomla 4 MVC Anatomy: Файловая структура компонента»
- «Joomla 4 MVC Anatomy: Файл манифеста компонента (Manifest File)»
- «Joomla 4 MVC Anatomy: Файлы сайта компонента»
- «Joomla 4 MVC Anatomy: Загрузочные файлы админки»
- «Joomla 4 MVC Anatomy: Файлы для правки админки»
Перевод с английского официальной документации CMS Joomla 4:
https://docs.joomla.org/J4.x:MVC_Anatomy:_Site_Files
Заберите ссылку на статью к себе, чтобы потом легко её найти!
Раз уж досюда дочитали, то может может есть желание рассказать об этом месте своим друзьям, знакомым и просто мимо проходящим?
Не надо себя сдерживать! ;)