Блог лаборанта

Разработка компонента Joomla 4 ч. 1 - фронтенд

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

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

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

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

Таблица mywalks

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

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

CREATE TABLE `#__mywalks` (
    `id` int(11) NOT NULL,
    `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=utf8;

Таблица mywalks_dates

    CREATE TABLE `#__mywalk_dates` (
    `id` int(11) NOT NULL,
    `walk_id` int(11) NOT NULL,
    `date` date NOT NULL,
    `weather` varchar(256) DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

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

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.

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


<?xml version="1.0" encoding="UTF-8"?>
<extension type="component" version="4.0" method="upgrade" client="site">
    <name>Mywalks</name>
    <!-- The following elements are optional and free of formatting conttraints -->
    <creationDate>August 2019</creationDate>
    <author>Clifford E Ford</author>
    <authorEmail>Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в вашем браузере должен быть включен Javascript.</authorEmail>
    <authorUrl>http://www.fford.me.uk/</authorUrl>
    <copyright>Copyright (C) 2019 Clifford E Ford, All rights reserved.</copyright>
    <license>GNU/GPL Version 2 or later - http://www.gnu.org/licenses/gpl-2.0.html</license>
    <!--  The version string is recorded in the components table -->
    <version>0.1.2</version>
    <!-- The description is optional and defaults to the name -->
    <description>Mywalks Component</description>
    <namespace>J4xdemos\Component\Mywalks</namespace>

    <install> <!-- Runs on install -->
        <sql>
            <file driver="mysql" charset="utf8">sql/install.mysql.utf8.sql</file>
        </sql>
    </install>
    <uninstall> <!-- Runs on uninstall -->
        <sql>
            <file driver="mysql" charset="utf8">sql/uninstall.mysql.utf8.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>Controller</folder>
        <folder>Helper</folder>
        <folder>Model</folder>
        <folder>Service</folder>
        <folder>tmpl</folder>
        <folder>View</folder>
    </files>
    
    <languages folder="site">
        <language tag="en-GB">language/en-GB/en-GB.com_mywalks.ini</language>
    </languages>
    
    <administration>
        <files folder="admin">
            <file>access.xml</file>
            <file>config.xml</file>
            <folder>Controller</folder>
            <folder>Extension</folder>
            <folder>forms</folder>
            <folder>Model</folder>
            <folder>Service</folder>
            <folder>services</folder>
            <folder>sql</folder>
            <folder>Table</folder>
            <folder>tmpl</folder>
            <folder>View</folder>
        </files>
        <languages folder="admin">
            <language tag="en-GB">language/en-GB/en-GB.com_mywalks.ini</language>
            <language tag="en-GB">language/en-GB/en-GB.com_mywalks.sys.ini</language>
        </languages>
        <menu img="class:default" link="option=com_mywalks">Mywalks</menu>
    </administration>
</extension>

Namespace

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

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

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

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

site                                     - the folder containing the site files       
     language                            - the folder containing the site language translation file
          en-GB                          - the folder containing English translations
               en-GB.com_mywalks.ini     - the file of translated keys

And the en-GB.com_mywalks.ini contains this:

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_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-items.php
              default.php
              default.xml

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

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

defined('_JEXEC') or die;

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

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

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

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

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

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

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

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

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

defined('_JEXEC') or die;

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

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

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

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

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

Пункты списка - tmpl/mywalks/default_items.php

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

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

defined('_JEXEC') or die;

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

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

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

Это часть функции 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\HTML\HTMLHelper;
//use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;

/**
 * HTML Mywalk View class 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;
    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)
    {
        $state      = $this->get('State');
        $item       = $this->get('Item');
        $reports    = $this->get('Reports');

        $this->state       = &$state;
        $this->item        = &$item;
        $this->reports     = &$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\HTML\HTMLHelper;
//use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
//use Joomla\CMS\Router\Route;

/**
 * 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)
    {
        $app    = Factory::getApplication();
        $params = $app->getParams();

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

        // Flag indicates to not add limitstart=0 to URL
        $pagination->hideEmptyLimitstart = true;

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

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

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

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

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

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

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

namespace J4xdemos\Component\Mywalks\Site\Model;

defined('_JEXEC') or die;

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

/**
 * Mywalk Component Mywalk Model
 *
 * @since  1.5
 */
class MywalkModel extends ItemModel
{
    /**
     * 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);

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

        // Load the parameters.
        $params = $app->getParams();
        $this->setState('params', $params);
    }

    /**
     * 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->getDbo();
                $query = $db->getQuery(true)
                    ->select(
                        $this->getState(
                            'item.select', 'a.*'
                        )
                    );
                $query->from('#__mywalks AS a')
                    ->where('a.id = ' . (int) $pk);

                $db->setQuery($query);

                $data = $db->loadObject();

                if (empty($data))
                {
                    throw new \Exception(Text::_('COM_MYWALKS_ERROR_WALK_NOT_FOUND'), 404);
                }
            }
            catch (\Exception $e)
            {
                if ($e->getCode() == 404)
                {
                    // 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->getDbo();
            $query = $db->getQuery(true)
            ->select('b.*');
            $query->from('#__mywalk_dates AS b')
            ->where('b.walk_id = ' . (int) $pk);
            $query->order('`date` DESC');

            $db->setQuery($query);

            $data = $db->loadObjectList();

            // 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\Factory;
use Joomla\CMS\MVC\Model\ListModel;

/**
 * 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',
            );
        }

        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 = 'ordering', $direction = 'ASC')
    {
        $app = Factory::getApplication();

        // List state information
        $value = $app->input->get('limit', $app->get('list_limit', 0), 'uint');
        $this->setState('list.limit', $value);

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

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

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

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

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

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

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

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

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

    /**
     * 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.

        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()
    {
        // Get the current user for authorisation checks
        $user = Factory::getUser();

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

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

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

        // Add the list ordering clause.
        $query->order($this->getState('list.ordering', 'a.id') . ' ' . $this->getState('list.direction', 'ASC'));

        return $query;
    }

    /**
     * Method to get a list of walks.
     *
     * Overridden to inject convert the attribs field into a \JParameter object.
     *
     * @return  mixed  An array of objects on success, false on failure.
     *
     * @since   1.6
     */
    public function getItems()
    {
        $items  = parent::getItems();
        return $items;
    }

    /**
     * Method to get the starting number of items for the data set.
     *
     * @return  integer  The starting number of items available in the data set.
     *
     * @since   3.0.1
     */
    public function getStart()
    {
        return $this->getState('list.start');
    }
}

Управление

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

Стоит помнить, что не-SEF URL для страницы со списком прогулок index.php?option=com_mywalks&task=display&view=mywalks. Часть task часто опускается, и в этом случае задача по умолчанию устанавливается на отображение. Если часть 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\Categories\CategoryFactoryInterface;
use Joomla\CMS\Component\Router\RouterFactoryInterface;
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
use Joomla\CMS\Extension\ComponentInterface;
//use Joomla\CMS\Extension\MVCComponent;
use Joomla\CMS\Extension\Service\Provider\CategoryFactory;
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
use Joomla\CMS\Extension\Service\Provider\MVCFactory;
use Joomla\CMS\Extension\Service\Provider\RouterFactory;
use Joomla\CMS\HTML\Registry;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use J4xdemos\Component\Mywalks\Administrator\Extension\MywalksComponent;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;

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

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

                    return $component;
        }
        );
    }
};

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

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

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

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

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

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

Файл маршрутизатора: component/com_mywalks/Service/Router.php

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

namespace J4xdemos\Component\Mywalks\Site\Service;

defined('_JEXEC') or die;

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

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

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

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

    /**
     * Mywalks Component router constructor
     *
     * @param   SiteApplication           $app              The application object
     * @param   AbstractMenu              $menu             The menu object to work with
     * @param   CategoryFactoryInterface  $categoryFactory  The category object
     * @param   DatabaseInterface         $db               The database object
     */
    public function __construct(SiteApplication $app, AbstractMenu $menu,
            CategoryFactoryInterface $categoryFactory, DatabaseInterface $db)
    {
        $this->categoryFactory = $categoryFactory;
        $this->db              = $db;

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

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

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

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

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

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

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

Правила включают функцию build для создания ссылок на отдельные прогулки и функцию 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'];
        $segments[] = $query['slug'];
        unset($query['view']);
        unset($query['id']);
        unset($query['slug']);
    }
}

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

И наконец

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

The Mywalks site walks list

Todo:

  • Добавьте код для сортировки списка прогулок по выбранному столбцу.
  • Добавьте код для пагинации списка прогулок.
  • Добавьте какую-нибудь ссылку back на страницу одиночного перехода или измените модуль хлебных крошек.

Навигация