Joomla 4: Mywalks, Часть 2 - Код админки



От автора:

Была предпринята попытка для того, чтобы была возможность прочитать Часть 2 руководства Mywalks, не читая сначала Часть 1. Проходите их в любом порядке. Эта часть касается кода администратора. Будет некоторое повторение общих для обоих моментов. Загрузите полный код с Github и прочтите его там, где указано ниже в руководстве.

Краткий обзор Части 1

Сценарий состоит в том, что в учебных целях компонент Mywalks управляет списком прогулок и случаев, когда эти прогулки были предприняты. Итак, нужны две таблицы. Здесь нет никаких излишеств: нет категорий, оценок, счетчиков посещений или ввода данных на стороне сайта и т.д.. Просто простой код, чтобы новички и обновившиеся до Joomla 4 начали писать собственные компоненты.

Схема данных приведена в Части 1. Или вы можете посмотреть файл admin/sql/install.mysql.sql в разархивированном исходном коде. Вы также можете просмотреть файл манифеста mywalks.xml в корне загруженного источника, чтобы увидеть структуру компонентов. Обратите внимание, что папки и файлы с первыми буквами в верхнем регистре в именах папок и файлов разделяются именами, а другие - нет. И есть другая структура папок и файлов по сравнению с предыдущими версиями Joomla.

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

Небольшие компоненты становятся больше, а более крупные - еще больше. Это может привести к тому, что языковые файлы будут беспорядочными и сложными в обслуживании. Файл lib_joomla.ini содержит более 800 строк. Так что стоит подумать о структуре заранее. Вот мое предложение:

[COM_COMPONENT]_[VIEW]_[USAGE]_[DESCRIPTIVE_TEXT]="The translation"

Например:

COM_MYWALKS_MYWALKS_PAGE_TITLE="Mywalks - list of Walks"
COM_MYWALKS_MYWALKS_TABLE_CAPTION="List of Walks"
COM_MYWALKS_MYWALKS_LABEL_DESCRIPTION="Description"
COM_MYWALKS_MYWALKS_LABEL_DISTANCE="Distance"

И отсортируйте в алфавитном порядке. Вы можете посмотреть файлы основного языка Joomla, чтобы найти альтернативные способы упорядочения информации в файлах с языковыми константами. Выбор за вами. Просто подумайте об этом!

Для справки: файл [component].sys.ini используется административной системой для управления компонентом. В нем очень мало терминов. В файле [component].ini находятся языковые константы компонента.

Предупреждение: все языковые константы английского языка в компоненте должен отображаться как ключи. У вас может возникнуть соблазн ввести в код только текстовое значение, чтобы исправить его позже. Если вы это сделаете, вы пожалеете об этом! ;)

Файлы админки

Смотрим структуру папок администратора из файла с манифестом mywalks.xml:

    <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">Mywalks</menu>
    </administration>

Обратите внимание, что папка src содержит весь код с пространством имен в отдельных подпапках.

Не так очевидно то, что есть четыре представления (вида), которыми нужно управлять:

  • список прогулок,
  • форма редактирования прогулки,
  • список дат прогулок и
  • форма даты прогулки.

Итак, четыре файла просмотра, четыре файла tmpl, четыре файла модели, четыре файла форм, четыре файла контроллера, но только два файла таблиц. Подробнее об этом позже. Вероятно, достаточно рассмотреть два примера: просмотр списка прогулок и вид редактирования прогулки. Обратите внимание на единственное и множественное число: walk и walks. Первое подразумевает представление редактирования, а второе - представление списка. Тонкая разница для усталых старых глаз! Компонент вводится через пункт меню, ведущий к просмотру списка прогулок. Ни в одном из других представлений нет пунктов меню. Представление списка прогулок и представление редактирования прогулки имеют несколько сложных проблем связывания, о которых мы поговорим позже.

Список прогулок

Это снимок экрана рабочего списка "Прогулки" в админке Joomla 4:

снимок экрана рабочего списка "Прогулки" в админке Joomla 4

Код для создания этого списка объясняется здесь в обратном алфавитном порядке: View, tmpl, Model и Controller. Таблица не требуется, поскольку список не изменяет данные и фактически извлекает данные из двух таблиц.

View file - View/Mywalks/HtmlView.php

Рассмотрим фрагменты файла по частям:

Пространство имен и операторы use

    namespace J4xdemos\Component\Mywalks\Administrator\View\Mywalks;
    
    defined('_JEXEC') or die;
    
    use Joomla\CMS\Helper\ContentHelper;
    use Joomla\CMS\Language\Text;
    use Joomla\CMS\MVC\View\GenericDataException;
    use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
    use Joomla\CMS\Toolbar\Toolbar;
    use Joomla\CMS\Toolbar\ToolbarHelper;

Оператор namespace (пространства имен) должен стоять перед любыми исполняемыми операторами, поэтому перед проверкой того, что этот скрипт был загружен Joomla и не был вызван напрямую через веб-адрес. Ссылки на использование Joomla\CMS на самом деле относятся к классам, находящимся в site_root/libraries/src. Если вы копируете и вставляете из другого компонента, у вас могут быть файлы библиотеки, которые вам не нужны, и могут отсутствовать те, которые у нужны вам.

Операторы класса

    class HtmlView extends BaseHtmlView
    {
        protected $items;
        protected $pagination;
        protected $state;
        public $filterForm;
        public $activeFilters;
        protected $sidebar;

Когда файл tmpl использует $this, он ссылается на этот класс. Итак, $this->items необходимо заполнить данными прогулок в этом файле.

Функция отображения display()

    public function display($tpl = null)
    {
        $this->items         = $this->get('Items');
        $this->pagination    = $this->get('Pagination');
        $this->state         = $this->get('State');
        $this->filterForm    = $this->get('FilterForm');
        $this->activeFilters = $this->get('ActiveFilters');

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

        $this->addToolbar();

        return parent::display($tpl);
    }

Это не очевидно, но вызовы формы $this->get('Items') относятся к публичной функции getItems() в модели или ее родительской модели. Так что ищите функции в Model/MywalksModel.php. После настройки вида в модели, элемент управления данными передается в parent::display() и возвращает все, что он возвращает.

function addToolbar()

   protected function addToolbar()
    {
        // Получить экземпляр объекта Toolbar (панели инструментов)
        $toolbar = Toolbar::getInstance('toolbar');

        ToolbarHelper::title(Text::_('COM_MYWALKS_MYWALKS_PAGE_TITLE'), 'mywalks');

        $canDo = ContentHelper::getActions('com_mywalks');

        if ($canDo->get('core.create'))
        {
            $toolbar->addNew('mywalk.add');
        }

Вызов ToolbarHelper::Title помещает заголовок (Title) вверху страницы. В данном случае переводится как Mywalks - список прогулок.

Функция ContentHelper::getActions вызывается, чтобы узнать, что этот пользователь может делать с этим компонентом. Если пользователю разрешено создавать новые прогулки, на панель инструментов добавляется кнопка «Новый». Посмотрите на код, чтобы увидеть, как создаются другие кнопки. Есть много кнопок на выбор, и вы можете создавать собственные кнопки.

Файл шаблона компонента (tmpl file) - tmpl/mywalks/default.php

Поскольку это представление компонента по умолчанию, файл tmpl называется файлом default.php. Взгляните на него в исходный код. Обратите внимание, что в верхней части файла нет оператора namespace. Этот файл не объявляет класс, поэтому пространство имен не требуется. Ниже объясняются короткие выдержки.

Установка некоторых переменных

    $listOrder = $this->escape($this->state->get('list.ordering'));
    $listDirn  = $this->escape($this->state->get('list.direction'));
    $states = array (
            '0' => Text::_('JUNPUBLISHED'),
            '1' => Text::_('JPUBLISHED'),
            '2' => Text::_('JARCHIVED'),
            '-2' => Text::_('JTRASHED')
    );
    $editIcon = '<span class="fa fa-pen-square mr-2" aria-hidden="true"></span>';

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

Вывод формы

    <form action="<?php echo Route::_('index.php?option=com_mywalks'); ?>" 
        method="post" name="adminForm" id="adminForm">
        <div class="row">
            <div class="col-md-12">
                <div id="j-main-container" class="j-main-container">
                    <?php echo LayoutHelper::render('joomla.searchtools.default', array('view' => $this)); ?>

При отправке форма вызывает сама себя. Действия, которые могут быть предприняты, включают изменение порядка сортировки или изменение состояния одной или нескольких прогулок. Практически все формы Joomla имеют adminForm в HTML атрибутах name и id. Все остальное требует специальной обработки. Вызов LayoutHelper::render создает панель инструментов поиска (joomla.searchtools.default) над таблицей результатов.

Разметка Bootstrap 4 не рассматривается в этом руководстве! (прим.переводчика - вроде как Joomla 4 перешла на Bootstrap 5)

No Data Alert

    <?php if (empty($this->items)) : ?>
        <div class="alert alert-info">
            <span class="fa fa-info-circle" aria-hidden="true"></span>
            <span class="sr-only"><?php echo Text::_('INFO'); ?></span>
            <?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS'); ?>
        </div>
    <?php else : ?>

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

Таблица с данными (Data Table)

    <table class="table" id="mywalksList">
        <caption id="captionTable">
            <?php echo Text::_('COM_MYWALKS_MYWALKS_TABLE_CAPTION'); ?>, 
            <?php echo Text::_('JGLOBAL_SORTED_BY'); ?>
        </caption>
        <thead>
            <tr>
                <td style="width:1%" class="text-center">
                    <?php echo HTMLHelper::_('grid.checkall'); ?>
                </td>
                <th scope="col" style="width:1%; min-width:85px" class="text-center">
                    <?php echo HTMLHelper::_('searchtools.sort', 'JSTATUS', 'a.state', $listDirn, $listOrder); ?>
                </th>

HTMLHelper здесь создает флажок «Отметить все» и функцию сортировки столбцов в заголовке таблицы.

Строки таблицы с данными (Table Rows)

    </thead>
    <tbody>
    <?php
    $n = count($this->items);
    foreach ($this->items as $i => $item) :
    ?>
        <tr class="row<?php echo $i % 2; ?>">
            <td class="text-center">
                <?php echo HTMLHelper::_('grid.id', $i, $item->id); ?>
            </td>
            <td class="class="article-status"">
                <?php echo $states[$item->state]; ?>
            </td>
            <th scope="row" class="has-context">
                <a class="hasTooltip" href="/<?php echo Route::_('index.php?option=com_mywalks&task=mywalk.edit&id=' . $item->id); ?>">
                <?php echo $editIcon; ?><?php echo $this->escape($item->title); ?>
                </a>
            </th>
            <td class="">
                <?php echo $item->description; ?>
            </td>

Чтобы создать таблицу результатов, код циклически перебирает каждый из $items, создавая по одной строке для каждого $item. Доступ к результатам элемента осуществляется как переменные объекта: $item->id, $item->state, $item->description и так далее.

Разбивка на страницы (Pagination)

        <?php endforeach; ?>
    </tbody>
</table>

<?php // load the pagination. ?>
<?php echo $this->pagination->getListFooter(); ?>

Если результатов больше, чем указано в фильтрах (по умолчанию обычно 20 или 25 и устанавливается глобально), функция разбиения на страницы возвращает html-код для визуализации селектора страниц. В противном случае он ничего не возвращает.

Скрытые поля формы (Hidden Variables)

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

Эти переменные важны! Переменная task изначально не имеет значения. Ему присваивается значение при нажатии кнопки действия. Например, кнопки «Создать» или «Состояние» или кнопка «Параметры фильтра». Затем отправляется форма с запрошенным действием. Значение boxchecked устанавливается и снимается Javascript и используется для блокировки отправки формы, если флажки не установлены. Помощник токена формы HTMLHelper создает скрытое поле с длинным буквенно-цифровым именем и значением 1. Токен формы проверяется при отправке формы. Если он недействителен, действие блокируется и возвращается пустой экран.

Filter XML File - forms/mywalks_filter.xml

Обратите внимание на соглашение об именах файлов. Придерживайтесь этого, иначе фильтр не появится. Как правило, содержимое xml состоит из двух частей: фильтра (filter) и списка (list).

    <?xml version="1.0" encoding="utf-8"?>
    <form>
        <fields name="filter">
            <field
                name="search"
                type="text"
                label="COM_MYWALKS_FILTER_SEARCH_LABEL"
                description="COM_MYWALKS_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>
    
        <fields name="list">
            <field
                name="fullordering"
                type="list"
                label="JGLOBAL_SORT_BY"
                default="a.name ASC"
                onchange="this.form.submit();"
                >
                <option value="">JGLOBAL_SORT_BY</option>
                <option value="a.published ASC">JSTATUS_ASC</option>
                <option value="a.published DESC">JSTATUS_DESC</option>
                <option value="a.title ASC">JGLOBAL_TITLE_ASC</option>
                <option value="a.title DESC">JGLOBAL_TITLE_DESC</option>
                <option value="a.id ASC">JGRID_HEADING_ID_ASC</option>
                <option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
            </field>
    
            <field
                name="limit"
                type="limitbox"
                label="JGLOBAL_LIST_LIMIT"
                default="25"
                onchange="this.form.submit();"
            />
        </fields>
    </form>

В этом случае панель инструментов фильтра будет содержать поле формы текстового поиска и поле формы статуса. Вы можете добавить больше полей - например, поле формы одежды. Если вы это сделаете, вам также нужно будет сделать запись в модели, о которой идет речь. Обратите внимание, что изменение поля фильтра ограничения списка (limit) инициирует немедленную отправку формы.

Model File - Model/MywalksModel

См. исходный код для получения полного файла с блоками документации Joomla.

Конструктор класса (The class constructor)

    class MywalksModel extends ListModel
    {
        public function __construct($config = array())
        {
            if (empty($config['filter_fields']))
            {
                $config['filter_fields'] = array(
                    'id', 'a.id',
                    'title', 'a.title',
                    'state', 'a.state',
                );
            }
    
            parent::__construct($config);
        }

Здесь должны быть объявлены поля фильтра, добавленные в mywalks_filter.xml, чтобы они отображались в выводе. Родительский ListModel содержит много кода, не упомянутого здесь. См. Документацию по API, когда она появится. Или просто найдите файл, содержащий класс ListModel и его родительский элемент.

function populateState()

    protected function populateState($ordering = 'a.id', $direction = 'asc')
    {
        $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search');
        $this->setState('filter.search', $search);

        $published = $this->getUserStateFromRequest($this->context . '.filter.published', 'filter_published', '');
        $this->setState('filter.published', $published);

        // Список информации состояния.
        parent::populateState($ordering, $direction);
    }

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

function getStoreId()

    protected function getStoreId($id = '')
    {
        // Compile the store id.
        $id .= ':' . $this->getState('filter.search');
        $id .= ':' . $this->getState('filter.published');

        return parent::getStoreId($id);
    }

Сложно объяснить! Эта функция объединяет строку, из которой вычисляется хэш, который используется для кэширования части SQL-запроса, чтобы избежать конфликтов с другим расширением, таким как модуль, который может выполняться при той же загрузке страницы.

function getListQuery()

    protected function getListQuery()
    {
        // Создание нового объекта запроса к базе данных.
        $db    = $this->getDbo();
        $query = $db->getQuery(true);

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

        // Фильтр по состоянию полученному из запроса
        $published = (string) $this->getState('filter.published');

        if (is_numeric($published))
        {
            $query->where($db->quoteName('a.state') . ' = :published');
            $query->bind(':published', $published, ParameterType::INTEGER);
        }
        elseif ($published === '')
        {
            $query->where('(' . $db->quoteName('a.state') . ' = 0 OR ' . $db->quoteName('a.state') . ' = 1)');
        }

        // Фильтр поиска по названию.
        $search = $this->getState('filter.search');

        if (!empty($search))
        {
            $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%'));
            $query->where('(a.title LIKE ' . $search . ')');
        }

        // Добавление информации о порядке списка.
        $orderCol  = $this->state->get('list.ordering', 'a.id');
        $orderDirn = $this->state->get('list.direction', 'ASC');

        $query->order($db->escape($orderCol) . ' ' . $db->escape($orderDirn));

        return $query;
    }

Здесь вам нужно знать SQL! Вам нужно уметь писать запросы и отлаживать их. Для начала нам понадобится объект базы данных в переменной $db и объект запроса базы данных в $query. Передача true в $db->getQuery(true) возвращает новый объект. Если истина опущена, возвращается существующий запрос, который уже заполнен, что обычно приводит к катастрофе. В Joomla части запроса могут быть объединены в цепочку, что усложняет понимание. Так что здесь детали собираются отдельно.

Оператор 'select': часть, в которой говорится, что $this->getState, либо получит запрос с использованием хэша storeId, либо создаст новый запрос и сохранит его в хеше для дальнейшей загрузки этой страницы. Сам запрос выбирает все поля из таблицы a и выбирает список встреч для каждой возвращенной прогулки. ToDo: добавьте дополнительно where для выборки только опубликованных посещений на прогулках.

Оператор "from": он просто говорит, какую таблицу использовать. Часть #__ заменяется префиксом таблицы, который меняется от сайта к сайту.

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

Оператор 'order' добавляет сортировку порядка выбранных значений.

Затем запрос возвращается вызывающей функции для дальнейшей работы с выбранными из БД данными.

Если в синтаксисе sql есть ошибка, вы можете просто увидеть сообщение 500 Internal Error. В таком случае перед оператором return вы можете вставить echo $query->__tostring(); die(); чтобы увидеть, что содержится в вашем запросе на этом этапе отладки.

function getItems()

    public function getItems()
    {
        $items = parent::getItems();

        return $items;
    }

Это нужно только в том случае, если вы хотите добавить правила доступа или как шаг при отладке. Если функция не указана, будет вызвана родительская функция getIems.

Controller File - Controller/DisplayController.php

Примечание: это файл контроллера с удаленными блоками документов. Вставьте их или оставьте в любом создаваемом вами компоненте.

    namespace J4xdemos\Component\Mywalks\Administrator\Controller;
    
    defined('_JEXEC') or die;
    
    use Joomla\CMS\MVC\Controller\BaseController;
    
    class DisplayController extends BaseController
    {
        protected $default_view = 'mywalks';
    
        public function display($cachable = false, $urlparams = array())
        {
            return parent::display();
        }
    }

Единственная задача, которую должен выполнить контроллер, - это определить представление компонентов по умолчанию.

Вот и все! Теперь это представление должно отображаться при выборе пункта меню.

Форма редактирования прогулки

В списке прогулок Название прогулки сделано ссылкой на форму редактирования. Вид ссылки index.php?option=com_mywalks&task=mywalk.edit&id=1. Используется тот же набор файлов, что и для списка прогулок, за исключением того, что все имена файлов являются единственными, и на этот раз требуется файл таблицы.

Это снимок экрана рабочей формы редактирования данных прогулки в админке Joomla 4:

снимок экрана рабочей формы редактирования данных прогулки в админке Joomla 4

View file - View/Mywalk/HtmlView

Этот файл очень похож на файл View/Mywalks/Html, поэтому показывать его полностью не стану. Он загружает другой заголовок и другой набор кнопок. Смотрите исходный код.

tmpl file - tmpl/Mywalk/edit.php

Код верхней части вкладки формы

    <form action="<?php echo Route::_('index.php?option=com_mywalks&view=mywalk&layout=edit&id=' . (int) $this->item->id); ?>"
        method="post" name="adminForm" id="mywalks-form" class="form-validate">
    
        <?php echo LayoutHelper::render('joomla.edit.title_alias', $this); ?>
    
        <div>
            <?php echo HTMLHelper::_('uitab.startTabSet', 'myTab', array('active' => 'details')); ?>
    
            <?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'details', Text::_('COM_MYWALKS_MYWALK_TAB_DETAILS')); ?>

Обратите внимание, что действие формы относится к самому себе. В этом случае идентификатор отличается, если нам нужно реализовать настраиваемый валидатор формы. Функция LayoutHelper::render помещает заголовок прогулки над остальной частью формы, которая разбивается на панели с вкладками. Это обычный макет Joomla.

Набор вкладок и отдельные вкладки создаются HTMLHelper. Можно получить структуру формы из XML-файла формы и циклически перемещаться по вкладкам и полям в коде. Этот метод здесь не реализован.

Вкладка сведений и поля ее формы

        <?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'details', Text::_('COM_MYWALKS_MYWALK_TAB_DETAILS')); ?>
        <div class="row">
            <div class="col-md-9">
                <div class="row">
                    <div class="col-md-6">
                        <?php echo $this->form->renderField('description'); ?>
                        <?php echo $this->form->renderField('distance'); ?>
                        <?php echo $this->form->renderField('id'); ?>
                    </div>
                </div>
            </div>
            <div class="col-md-3">
                <div class="card card-light">
                    <div class="card-body">
                        <?php echo LayoutHelper::render('joomla.edit.global', $this); ?>
                    </div>
                </div>
            </div>
        </div>
        <?php echo HTMLHelper::_('uitab.endTab'); ?>

В этом макете появляются первые три поля формы, занимающие 9/12 ширины родительского div. Поле состояния отображается справа, занимая 3/12 ширины родительского блока div.

Вкладка параметров и поля ее формы

        <?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'options', Text::_('COM_MYWALKS_MYWALK_TAB_OPTIONS')); ?>
        <div class="row">
            <div class="col-md-12">
                <?php echo $this->form->renderField('toilets'); ?>
                <?php echo $this->form->renderField('cafe'); ?>
                <?php echo $this->form->renderField('bogs'); ?>
            </div>
        </div>
        <?php echo HTMLHelper::_('uitab.endTab'); ?>

Вкладка с изображением и поля ее формы

        <?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'picture', Text::_('COM_MYWALKS_MYWALK_TAB_PICTURE')); ?>
        <div class="row">
            <div class="col-md-12">
                <?php echo $this->form->renderField('picture'); ?>
                <?php echo $this->form->renderField('width'); ?>
                <?php echo $this->form->renderField('height'); ?>
                <?php echo $this->form->renderField('alt'); ?>
            </div>
        </div>
        <?php echo HTMLHelper::_('uitab.endTab'); ?>

        <?php echo HTMLHelper::_('uitab.endTabSet'); ?>
    </div>
    <input type="hidden" name="task" value="">
    <?php echo HTMLHelper::_('form.token'); ?>
</form>

Внешний вид каждого из полей формы определяется в XML-файле формы.

xml form file - forms/mywalk.xml

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

Поля сведений (details)

    <?xml version="1.0" encoding="utf-8"?>
    <form>
    
        <fieldset name="details" label="COM_MYWALKS_MYWALK_TAB_DETAILS">
    
            <field
                name="id"
                type="number"
                label="JGLOBAL_FIELD_ID_LABEL"
                default="0"
                class="readonly"
                readonly="true"
            />
    
            <field
                name="title"
                type="text"
                label="COM_MYWALKS_MYWALK_LABEL_TITLE"
                size="40"
                required="true"
             />
    
            <field
                name="description"
                type="textarea"
                label="COM_MYWALKS_MYWALK_LABEL_DESCRIPTION"
                rows="5"
                cols="40"
                required="true"
             />
    
            <field
                name="distance"
                type="decimel"
                label="COM_MYWALKS_MYWALK_LABEL_DISTANCE"
                required="true"
             />
    
            <field
                name="published"
                type="list"
                label="JSTATUS"
                default="1"
                id="published"
                class="custom-select-color-state"
                size="1"
                >
                <option value="1">JPUBLISHED</option>
                <option value="0">JUNPUBLISHED</option>
                <option value="2">JARCHIVED</option>
                <option value="-2">JTRASHED</option>
            </field>
    
        </fieldset>

Поле id установлено только для чтения, потому что идентификатор автоматически формируется при создании записи и не может быть изменен. Обратите внимание на различные типы полей: число (number), текст (text), текстовое поле (textarea), десятичное число (decimel) и список (list). Здесь устанавливаются различные атрибуты, такие как класс (class), только чтение (readonly), размер (size) и т.д.

Поля опций (options)

    <fieldset name="options" label="COM_MYWALKS_MYWALK_TAB_OPTIONS">
        <field name="toilets"
            type="list"
            label="COM_MYWALKS_MYWALK_LABEL_TOILETS"
        >
            <option value="0">JNO</option>
            <option value="1">JYES</option>
        </field>

        <field name="cafe"
            type="list"
            label="COM_MYWALKS_MYWALK_LABEL_CAFE"
            default="0"
            size="1"
            >
            <option value="0">JNO</option>
            <option value="1">JYES</option>
        </field>

        <field name="bogs"
            type="list"
            label="COM_MYWALKS_MYWALK_LABEL_BOGS"
            default="0"
            size="1"
            >
            <option value="0">JNO</option>
            <option value="1">JYES</option>
        </field>
    </fieldset>

Поля здесь возвращают логические значения Да/Нет. В некоторых случаях может быть предпочтительным число, например, по шкале от 1 до 5.

Поля изображений (picture)

    <fieldset name="picture" label="COM_MYWALKS_MYWALK_TAB_PICTURE">

        <field
            name="picture"
            type="media"
            label="COM_MYWALKS_MYWALK_LABEL_PICTURE_URL"
        />

        <field
            name="width"
            type="number"
            label="COM_MYWALKS_MYWALK_LABEL_PICTURE_WIDTH"
            default="0"
         />

        <field
            name="height"
            type="number"
            label="COM_MYWALKS_MYWALK_LABEL_PICTURE_HEIGHT"
            default="0"
         />

        <field
            name="alt"
            type="text"
            label="COM_MYWALKS_MYWALK_LABEL_PICTURE_ALT"
            size="40"
         />
    </fieldset>

</form>

Поле типа media позволяет выбрать фотографию из папки изображений сайта. Появившаяся форма выбора также позволяет загружать файлы. Задача: реализовать использование изображений.

Model file - Models/MywalksModel.php

Файл модели занимает около 300 строк и в основном представляет собой шаблонный код, который не требует особых комментариев.

function getTable()

    public function getTable($name = '', $prefix = '', $options = array())
    {
        $name = 'mywalks';
        $prefix = 'Table';

        if ($table = $this->_createTable($name, $prefix, $options))
        {
            return $table;
        }

        throw new \Exception(Text::sprintf('JLIB_APPLICATION_ERROR_TABLE_NAME_NOT_SUPPORTED', $name), 0);
    }

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

Table file - Table/MywalksTable.php

    namespace J4xdemos\Component\Mywalks\Administrator\Table;
    
    defined('JPATH_PLATFORM') or die;
    
    use Joomla\CMS\Table\Table;
    use Joomla\Database\DatabaseDriver;
    
    class MywalksTable extends Table
    {
        public function __construct(DatabaseDriver $db)
        {
            parent::__construct('#__mywalks', 'id', $db);
        }
    }

Controller file - Controller/MywalkController

    namespace J4xdemos\Component\Mywalks\Administrator\Controller;
    
    defined('_JEXEC') or die;
    
    use Joomla\CMS\MVC\Controller\FormController;
    
    class MywalkController extends FormController
    {
    }

Этот файл ничего не делает! Все необходимые действия выполняются родительским классом.

Вот и все! Теперь у нас должна быть возможность редактировать существующие прогулки и создавать новые.

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

Большая часть кода для обработки дат прогулки аналогична коду для обработки прогулок, и нет реальной необходимости повторять это здесь в учебных целях. Однако есть одно большое различие, которое требует объяснения. Это касается передачи идентификатора прогулки из списка прогулок в список дат прогулки.

Доступ к списку посещений осуществляется по ссылке в списке прогулок: index.php?option=com_mywalks&view=mywalk_dates&walk_id=1. Таким образом, идентификатор прогулки передается в список, чтобы отобразить список дат прогулки для этой прогулки, включая ни одного, если прогулка еще была без посещений. Идентификатор прогулки необходимо сохранить в скрытом поле в форме списка, поскольку он исчезает из URL-адреса при изменении сортировки таблицы или фильтра. Таким образом, мы фиксируем идентификатор прогулки в функции populateState или из переменной состояния сеанса.

Отрывок из Mywalk_datesModel.php

    protected function populateState($ordering = 'a.id', $direction = 'asc')
    {
        $app = Factory::getApplication();

        $walk_id = $app->input->get('walk_id', 0, 'int');
        if (empty($walk_id)) {
            $walk_id = $app->getUserState('com_mywalks.walk_id');
        }
        $this->setState('walk_id', $walk_id);
        // сохранить walk_id для добавления новых посещений
        $app->setUserState('com_mywalks.walk_id', $walk_id);

Мы получаем название прогулки и идентификатор прогулки в файле tmpl/Mywalk_dates/default.php и отображаем заголовок на тот случай, если мы забыли, с какой прогулкой мы имеем дело. И мы помещаем идентификатор прогулки в скрытую переменную формы.

Отрывок из tmpl/mywalk_dates/default.php

    $editIcon = '<span class="fa fa-pen-square mr-2" aria-hidden="true"></span>';
    $title = MywalksHelper::getWalkTitle($this->state->get('walk_id'))->title;
    $walk_id = $this->state->get('walk_id')
    ?>
    <h3><?php echo Text::_('COM_MYWALKS_MYWALK_DATES_PAGE_TOP') . ' ' . $walk_id . ': ' . $title; ?></h3>
    <form action="<?php echo Route::_('index.php?option=com_mywalks&view=mywalk_dates'); ?>" method="post" name="adminForm" id="adminForm>
    ...
                    <input type="hidden" name="task" value="">
                    <input type="hidden" name="boxchecked" value="0">
                    <input type="hidden" name="walk_id" value="<?php echo $walk_id; ?>">
                    <?php echo HTMLHelper::_('form.token'); ?>
    ...
    </form>

В списке дат прогулки есть ссылка на существующие посещения, поэтому нам не нужен идентификатор прогулки для редактирования существующих посещений. Кнопка «New» не помогает, потому что идентификатор по умолчанию равен 0. Это идентификатор нового посещения до тех пор, пока он не будет зафиксирован в базе данных, когда он получит следующее доступное значение. Мы берем идентификатор прогулки в tmp/mywalk_date/edit.php и устанавливаем значение идентификатора прогулки в поле формы:

    $app = Factory::getApplication();
    $walk_id = $app->getUserState('com_mywalks.walk_id');
    
    if (empty($walk_id)) {
        throw new GenericDataException("\nThe walk id was not set!\n", 500);
    }
    ...
                            <?php echo $this->form->renderField('date'); ?>
                            <?php echo $this->form->renderField('weather'); ?>
                            <?php echo $this->form->renderField('id'); ?>
                            <?php $this->form->setValue('walk_id', null, $walk_id); ?>
                            <?php echo $this->form->renderField('walk_id'); ?>

Поле walk_id доступно только для чтения, чтобы пользователь не мог его изменить. И вот один из тех случаев, когда строка вывода текста была набрана на английском языке, а не объявлена константой из языкового файла. Осталось проиллюстрировать суть дела!

Задача: реализовать кнопку «Сохранить и закрыть».

И наконец!

Вот и все! Примитивный, но рабочий компонент.

Clifford E Ford. August 2019. [перевод: А. Захаров + Google Translate. Апрель 2021 года]

Оригинал на аглицком:
https://docs.joomla.org/Part_2:_The_Administrator_code

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

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