Suchmaschinenfreundliche URLs funktionieren bisher nicht. Die URL zu den Elementen der Komponente erscheint in der Form JOOMLA/category?view=category&id=8
. Anhand eines Services reparieren wir diesen Missstand. Gleichzeitig ist dies ein gutes Beispiel, um herauszuarbeiten, was notwendig ist, um einen Service in einer Joomla Erweiterung zu integrieren.
Suchmaschinenfreundlich (SEF), menschlich lesbare[^de.wikipedia.org/wiki/Clean_URL] sind URLs, die sowohl für Menschen als auch für Suchmaschinen Sinn machen, weil sie den Pfad zu der bestimmten Seite erklären. Joomla ist in der Lage, URLs in jedem Format zu erstellen. Dies hängt nicht vom URL-Rewriting ab, das vom Webserver ausgeführt wird, so dass es auch dann funktioniert, wenn Joomla einen anderen Server als Apache mit dem Modul mod_rewrite verwendet. Die SEF-URLs folgen einem bestimmten festen Muster, aber der Benutzer kann einen kurzen beschreibenden Text Alias[^docs.joomla.org/Alias/de] für jedes Segment der URL definieren.
Intern wird der lokale Teil einer SEF-URL (der Teil nach dem Domainnamen) als Route bezeichnet. Das Erstellen und Verarbeiten von SEF-URLs wird daher als Routing bezeichnet, und der entsprechende Code wird Router genannt.
Ein Beispiel für das Routing ist die URL zum Artikel "Willkommen bei Joomla" in den Beispieldaten. Ohne eingeschaltete SEF-URLs ist die URL
/index.php?option=com_content&view=article&id=1:welcome-to-joomla&catid=1:latest-news&Itemid=50
. Mit eingeschalteten SEF-URLs und ausgeschaltetem mod_rewrite ist es/index.php/the-news/1-latest-news/1-welcome-to-joomla
. Mit eingeschalteten SEF-URLs und mod_rewrite ist es/the-news/1-latest-news/1-welcome-to-joomla
.
Suchmaschinenfreundliche URLs können aktiviert werden, indem die Option Search Engine Friendly URLs in der Globalen Konfiguration eingeschaltet wird. Diese Option ist in Joomla 4 standardmäßig aktiviert. Weitere Informationen findest du unter Aktivieren von suchmaschinenfreundlichen (SEF) URLs in der Dokumentation[^docs.joomla.org/Enabling_Search_Engine_Friendly_(SEF)_URLs].
Für Ungeduldige: Sieh dir den geänderten Programmcode in der Diff-Ansicht[^codeberg.org/astrid/j4examplecode/compare/t26...t27] an und übernimm diese Änderungen in deine Entwicklungsversion.
Schritt für Schritt
Neue Dateien
components/com_foos/src/Service/Router.php
Der Service components/com_foos/src/Service/Router.php
erledigt die eigentliche Arbeit und wandelt die URLs in suchmaschinenfreundliche Versionen.
components/com_foos/src/Service/Router.php
// https://codeberg.org/astrid/j4examplecode/raw/branch/t27/src/components/com_foos/src/Service/Router.php
<?php
/**
* @package Joomla.Site
* @subpackage com_foos
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace FooNamespace\Component\Foos\Site\Service;
\defined('_JEXEC') or die;
use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Categories\CategoryFactoryInterface;
use Joomla\CMS\Categories\CategoryInterface;
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 Joomla\CMS\Component\Router\Rules\StandardRules;
use Joomla\CMS\Menu\AbstractMenu;
use Joomla\Database\DatabaseInterface;
use Joomla\Database\ParameterType;
/**
* Routing class from com_foos
*
* @since __BUMP_VERSION__
*/
class Router extends RouterView
{
/**
* Flag to remove IDs
*
* @var boolean
*/
protected $noIDs = false;
/**
* The category factory
*
* @var CategoryFactoryInterface
*
* @since __BUMP_VERSION__
*/
private $categoryFactory;
/**
* The category cache
*
* @var array
*
* @since __BUMP_VERSION__
*/
private $categoryCache = [];
/**
* The db
*
* @var DatabaseInterface
*
* @since __BUMP_VERSION__
*/
private $db;
/**
* Content 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_foos');
$this->noIDs = (bool) $params->get('sef_ids');
$categories = new RouterViewConfiguration('categories');
$categories->setKey('id');
$this->registerView($categories);
$category = new RouterViewConfiguration('category');
$category->setKey('id')->setParent($categories, 'catid')->setNestable();
$this->registerView($category);
$foo = new RouterViewConfiguration('foo');
$foo->setKey('id')->setParent($category, 'catid');
$this->registerView($foo);
$this->registerView(new RouterViewConfiguration('featured'));
$form = new RouterViewConfiguration('form');
$form->setKey('id');
$this->registerView($form);
parent::__construct($app, $menu);
$this->attachRule(new MenuRules($this));
$this->attachRule(new StandardRules($this));
$this->attachRule(new NomenuRules($this));
}
/**
* Method to get the segment(s) for a category
*
* @param string $id ID of the category to retrieve the segments for
* @param array $query The request that is built right now
*
* @return array|string The segments of this item
*/
public function getCategorySegment($id, $query)
{
$category = $this->getCategories()->get($id);
if ($category) {
$path = array_reverse($category->getPath(), true);
$path[0] = '1:root';
if ($this->noIDs) {
foreach ($path as &$segment) {
list($id, $segment) = explode(':', $segment, 2);
}
}
return $path;
}
return [];
}
/**
* Method to get the segment(s) for a category
*
* @param string $id ID of the category to retrieve the segments for
* @param array $query The request that is built right now
*
* @return array|string The segments of this item
*/
public function getCategoriesSegment($id, $query)
{
return $this->getCategorySegment($id, $query);
}
/**
* Method to get the segment(s) for a foo
*
* @param string $id ID of the foo to retrieve the segments for
* @param array $query The request that is built right now
*
* @return array|string The segments of this item
*/
public function getFooSegment($id, $query)
{
if (!strpos($id, ':')) {
$id = (int) $id;
$dbquery = $this->db->getQuery(true);
$dbquery->select($this->db->quoteName('alias'))
->from($this->db->quoteName('#__foos_details'))
->where($this->db->quoteName('id') . ' = :id')
->bind(':id', $id, ParameterType::INTEGER);
$this->db->setQuery($dbquery);
$id .= ':' . $this->db->loadResult();
}
if ($this->noIDs) {
list($void, $segment) = explode(':', $id, 2);
return [$void => $segment];
}
return [(int) $id => $id];
}
/**
* Method to get the segment(s) for a form
*
* @param string $id ID of the foo form to retrieve the segments for
* @param array $query The request that is built right now
*
* @return array|string The segments of this item
*
* @since __BUMP_VERSION__
*/
public function getFormSegment($id, $query)
{
return $this->getFooSegment($id, $query);
}
/**
* Method to get the id for a category
*
* @param string $segment Segment to retrieve the ID for
* @param array $query The request that is parsed right now
*
* @return mixed The id of this item or false
*/
public function getCategoryId($segment, $query)
{
if (isset($query['id'])) {
$category = $this->getCategories(['access' => false])->get($query['id']);
if ($category) {
foreach ($category->getChildren() as $child) {
if ($this->noIDs) {
if ($child->alias == $segment) {
return $child->id;
}
} else {
if ($child->id == (int) $segment) {
return $child->id;
}
}
}
}
}
return false;
}
/**
* Method to get the segment(s) for a category
*
* @param string $segment Segment to retrieve the ID for
* @param array $query The request that is parsed right now
*
* @return mixed The id of this item or false
*/
public function getCategoriesId($segment, $query)
{
return $this->getCategoryId($segment, $query);
}
/**
* Method to get the segment(s) for a foo
*
* @param string $segment Segment of the foo to retrieve the ID for
* @param array $query The request that is parsed right now
*
* @return mixed The id of this item or false
*/
public function getFooId($segment, $query)
{
if ($this->noIDs) {
$dbquery = $this->db->getQuery(true);
$dbquery->select($this->db->quoteName('id'))
->from($this->db->quoteName('#__foos_details'))
->where(
[
$this->db->quoteName('alias') . ' = :alias',
$this->db->quoteName('catid') . ' = :catid',
]
)
->bind(':alias', $segment)
->bind(':catid', $query['id'], ParameterType::INTEGER);
$this->db->setQuery($dbquery);
return (int) $this->db->loadResult();
}
return (int) $segment;
}
/**
* Method to get categories from cache
*
* @param array $options The options for retrieving categories
*
* @return CategoryInterface The object containing categories
*
* @since __BUMP_VERSION__
*/
private function getCategories(array $options = []): CategoryInterface
{
$key = serialize($options);
if (!isset($this->categoryCache[$key])) {
$this->categoryCache[$key] = $this->categoryFactory->createCategory($options);
}
return $this->categoryCache[$key];
}
}
Geänderte Dateien
administrator/components/com_foos/services/provider.php
Im Service Provider registrieren wir den Service.
administrator/components/com_foos/services/provider.php
use FooNamespace\Component\Foos\Administrator\Extension\FoosComponent;
use FooNamespace\Component\Foos\Administrator\Helper\AssociationsHelper;
use Joomla\CMS\Association\AssociationExtensionInterface;
use Joomla\CMS\Component\Router\RouterFactoryInterface;
use Joomla\CMS\Extension\Service\Provider\RouterFactory;
/**
* The foos service provider.
public function register(Container $container)
$container->registerServiceProvider(new CategoryFactory('\\FooNamespace\\Component\\Foos'));
$container->registerServiceProvider(new MVCFactory('\\FooNamespace\\Component\\Foos'));
$container->registerServiceProvider(new ComponentDispatcherFactory('\\FooNamespace\\Component\\Foos'));
$container->registerServiceProvider(new RouterFactory('\\FooNamespace\\Component\\Foos'));
$container->set(
ComponentInterface::class,
function (Container $container) {
$component->setMVCFactory($container->get(MVCFactoryInterface::class));
$component->setCategoryFactory($container->get(CategoryFactoryInterface::class));
$component->setAssociationExtension($container->get(AssociationExtensionInterface::class));
$component->setRouterFactory($container->get(RouterFactoryInterface::class));
return $component;
}
Die Zeilen $container->registerServiceProvider (new RouterFactory('\\Joomla\\Component\\Foos'))
und $component->setRouterFactory ($container->get(RouterFactoryInterface::class))
kommen hinzu.
administrator/components/com_foos/src/Extension/FoosComponent.php
Wir implementieren RouterServiceInterface
und nutzen RouterServiceTrait
, so dass diese Dateien zur Verfügung stehen.
administrator/components/com_foos/src/Extension/FoosComponent.php
use FooNamespace\Component\Foos\Administrator\Service\HTML\Icon;
use Psr\Container\ContainerInterface;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\Component\Router\RouterServiceInterface;
use Joomla\CMS\Component\Router\RouterServiceTrait;
/**
* Component class for com_foos
*
* @since __BUMP_VERSION__
*/
class FoosComponent extends MVCComponent implements BootableExtensionInterface, CategoryServiceInterface, AssociationServiceInterface
class FoosComponent extends MVCComponent implements BootableExtensionInterface, CategoryServiceInterface, AssociationServiceInterface, RouterServiceInterface
{
use CategoryServiceTrait;
use AssociationServiceTrait;
use HTMLRegistryAwareTrait;
use RouterServiceTrait;
/**
* Booting the extension. This is the function to set up the environment of the extension like
Teste deine Joomla-Komponente
- Installiere deine Komponente in Joomla Version 4, um sie zu testen:
Kopiere die Dateien im administrator
Ordner in den administrator
Ordner deiner Joomla 4 Installation.
Kopiere die Dateien im components
Ordner in den components
Ordner deiner Joomla 4 Installation.
Installiere deine Komponente wie in Teil eins beschrieben, nachdem du alle Dateien kopiert hast. Joomla aktualisiert bei der Installation die Namespaces für dich. Da eine neue Datei hinzugekommen ist, ist dies erforderlich.
- Aktiviere die Einstellung suchmaschinenfreundliche URLs in der globalen Konfiguration.
- Erstelle einen Menüpunkt für ein Foo-Element
- Überprüfe die URLs mit der der Menüpunkt im Frontend aufgerufen wird. Anstelle von
http://localhost/ single-foo-astrid?view=foo&id=2
solltehttp://localhost/ single-foo-astrid
erscheinen - jenachdem wie du deine Menüpunkte genannt hast. Im Falle von Kategorien ist die Verbesserung noch augenscheinlicher.
Beachte: Die URL
http://localhost/ single-foo-astrid?view=foo&id=2
ist technisch gesehen weiterhin vorhanden und aufrufbar.
Links
Routing in com_contact[^github.com/joomla/joomla-cms/pull/27693]
Webmentions