Man muss ja nicht! Aber man kann Module ab Joomla 4.3 mit einer "moderneren" Grundstruktur aufsetzen. So fallen seit vielen Jahren übliche Dateien raus und neue kommen dazu; inklusive neuen Ordnern. Als Autodidaktdummdudler bin ich gar nicht in der Lage, alles wissenschaftlich korrekt zu beschreiben. Aber vielleicht ja gar nicht schlecht, wenn ich mich in meinem kurzen Überblick auf Wesentliches beschränke?

Was hier auf der Seite nicht passiert

Eine Kleinklein-Erklärung wie man Joomla-Module bzw. -Erweiterungen grundlegend aufbaut. Hier gehe ich nur auf die prägnantesten Änderungen ein und das, was mir beim ersten Umbau eines eigenen Modules nach traditioneller Art auffiel. Die üblichen Manuals sind grundlegend in größten Teilen richtig; egal, ob nun für uralte, alte oder neue Struktur.

Vorgeplänkel. Das Demomodul

heißt mod_demoghsvs. Das "ghsvs" hänge ich meinen Erweiterungen immer hintan. Also nur eine private Usus-Sache.

Es findet sich auch auf GitHub: https://github.com/GHSVS-de/mod_demoghsvs. Dort kann man zumindest auf Desktop-PCs in der Code-Ansicht von Dateien in der linken Sidebar leichter zwischen Files hin und her klickern.

Stellt man den Branch-Schalter, der im Bild auf "main" steht auf den Tag "0.0.1" um, sieht man exklusiv die Dateien, die in diesem Artikel besprochen werden.

GitHub-Sidebar zum Navigieren zwischen Dateien in der Code Ansicht

Wer es installieren möchte, aber wozu?, findet auf GitHub die jeweils aktuellste ZIP-Datei wie im Bild zu sehen. Dieser Download kann aber veränderlich sein und passt dann nicht mehr unbedingt zu 100% zu diesem Artikel.

Modul ZIP auf GitHub herunterladen

Einen Download der exklusiv zu diesem Beitrag passt, findet man dagegen hier: https://github.com/GHSVS-de/mod_demoghsvs/releases/tag/0.0.1

Verzeichnis-Grundaufbau

mod_demoghsvs/

  • LICENSE.txt
  • mod_demoghsvs.xml
  • language/
    • en-GB/
      • mod_demoghsvs.ini
      • mod_demoghsvs.sys.ini
  • services/
    • provider.php
  • src/
    • Dispatcher/
      • Dispatcher.php
    • Helper/
      • DemoGhsvsHelper.php
  • tmpl/
    • default.php

Schnellüberflug

Was zuerst auffällt, ist, dass die seit Jahren in Joomla-Modulen übliche Datei mod_demoghsvs.php im Stammverzeichnis /modules/mod_demoghsvs/ des Moduls nicht mehr existiert.

Wer es noch nicht weiß: Die Dateien im Ordner language/en-GB/ brauchen unter Joomla 4+ kein weiteres Prefix en-GB. mehr. Natürlich gilt das auch für Sprachdateien anderer Sprachen, wenn man welche anlegt. Hauptsache im richtigen Unterordner, also hier en-GB/. Zusätzlich der Hinweis, dass ich bei meinen Erweiterungen die Sprach-Files nicht per der Manifestdatei mod_demoghsvs.xml in den Joomla-Ordner /language/ bei der Installation verfrachten lasse. Spart Arbeit und ist schon lange erlaubt.

Oops, ein neuer Ordner inklusive neuer Datei services/provider.php. Die gehört zur neuen Struktur; ist elementar. Beide in Kleinschrift und außerhalb des src/-Verzeichnisses. In dem fängt nämlich (fast immer) alles mit Großbuchstaben an und verzichtet auf Bindestriche, Unterstriche und so Kram.

Ein Ordner src/. Das ist nicht ganz neu, aber jetzt elementarer, weil viel mit PHP-Namensräumen/Namespaces gemacht wird und wir die diesbezüglichen Automatismen, die Joomla bietet, nicht unbedingt durch zu viel Kreativität und Spieltrieb komplizieren wollen; oder?

Eine für die neue Struktur elementare, neue Datei src/Dispatcher/Dispatcher.php.

Die früher oft zu sehende helper.php ist verschwunden und die Helper-Funktionalitäten finden sich in Datei src/Helper/DemoGhsvsHelper.php. Man beachte die Schreibweise. Die ist nicht komplett verpflichtend so. Trotzdem sollte man sich dabei an die Joomla-Konventionen halten, weil es unter dem berühmten Strich Vieles erleichtert. Schon am Dateinamen kann man erkennen, was sich dahinter versteckt.

Es ginge gleichermaßen DemoghsvsHelper.php oder Demoghsvshelper.php oder Meindollerhelfer.php. Allerdings finde (nicht nur) ich "erklärend separierende" CamelCase-Schreibweise schick; ebenso bei "sprechenden" Methoden-, Funktions- und Variablennamen. Vorbei die Zeiten, in denen man Codes "hieroglyphisch kürzen" wollte; hlp() versus getHeadlinePrefix() oder so. Kurz: Wenn schon doof, wenigstens MeinDollerHelfer.php.

Natürlich gehen auf diese Art und Weise mehrere Helper im selben Ordner.

Das Modulmanifest mod_demoghsvs.xml

Das meiste in der Manifestdatei ist altbekannter Standard, auf den ich nicht eingehe. Es soll übrigens schlaue Menschen geben, die fragen Menschen und nicht Maschinen, wenn sie was nicht verstehen.

<?xml version="1.0" encoding="utf-8"?>
<extension type="module" client="site" method="upgrade">
	<name>MOD_DEMOGHSVS</name>
	<author>G@HService Berlin Neukölln, Volkmar Volli Schlothauer</author>
	<creationDate>2023-05-08</creationDate>
	<copyright>(C) 2023, G@HService Berlin Neukölln, Volkmar Volli Schlothauer (ghsvs.de)</copyright>
	<license>GNU General Public License version 3 or later; see LICENSE.txt.</license>
	<authorUrl>https://ghsvs.de</authorUrl>
	<version>0.0.1</version>
	<description>MOD_DEMOGHSVS_XML_DESCRIPTION</description>

	<files>
		<folder module="mod_demoghsvs">services</folder>
		<filename>LICENSE.txt</filename>
		<folder>language</folder>
		<folder>src</folder>
		<folder>tmpl</folder>
	</files>

	<namespace path="src">GHSVS\Module\DemoGhsvs</namespace>

	<config>
		<fields name="params">

			<fieldset name="basic">

				<field name="meinImage" type="media"
					label="MOD_DEMOGHSVS_IMAGE"/>

				<field name="meinText" type="textarea"
					label="MOD_DEMOGHSVS_TEXT"/>

			</fieldset>

			<fieldset name="advanced">
				<field
					name="layout"
					type="modulelayout"
					label="JFIELD_ALT_LAYOUT_LABEL"
					class="form-select"
					validate="moduleLayout"
				/>

				<field
					name="moduleclass_sfx"
					type="textarea"
					label="COM_MODULES_FIELD_MODULECLASS_SFX_LABEL"
					rows="3"
					validate="CssIdentifier"
				/>

				<field
					name="cache"
					type="list"
					label="COM_MODULES_FIELD_CACHING_LABEL"
					default="1"
					filter="integer"
					validate="options"
					>
					<option value="1">JGLOBAL_USE_GLOBAL</option>
					<option value="0">COM_MODULES_FIELD_VALUE_NOCACHING</option>
				</field>

				<field
					name="cache_time"
					type="number"
					label="COM_MODULES_FIELD_CACHE_TIME_LABEL"
					default="900"
					filter="integer"
				/>

				<field
					name="cachemode"
					type="hidden"
					default="static"
					>
					<option value="static"></option>
				</field>

			</fieldset>
		</fields>
	</config>
</extension>

Wohin mit dem module-Attribut?

Die erste Auffälligkeit im Vergleich zu altherkömmlichen Modul-Manifesten sehe ich im <files>...</files>-Block (Zeilen 12 bis 18). In Zeile 13 hätte man früher so was gehabt:

<filename module="mod_demoghsvs">mod_demoghsvs.php</filename>

Jetzt sieht man, weil es die Datei mod_demoghsvs.php ja nicht mehr gibt:

<folder module="mod_demoghsvs">services</folder>

Dass das jetzt so ausschaut, betrachte ich als Joomla-Konvention, werde es also ab jetzt immer so machen. Nur erwähnt: Es gehen ebenso Variationen. Im Joomla-Core habe ich z.B. ein Modul entdeckt, wo das module-Attribut beim src und nicht services untergebracht ist: <folder module="mod_demoghsvs">src</folder>. Es scheint also egal, wo man es unterbringt.

Der Master-Namensraum der Erweiterung

In Zeile 20 wird der Wunsch-Namespace, also der PHP-Namensraum des Moduls definiert. Wichtig, wichtig, damit Joomla mit Hilfe des Plugins Erweiterungen – Namespace Updater (plg_extension_namespacemap) uns lästige Vorarbeiten bei der Installation verrichten kann! So ist später in der Datei /administrator/cache/autoload_psr4.php auch unser Namensraum in einen "echten" Dateipfad übersetzt. Aus

<namespace path="src">GHSVS\Module\DemoGhsvs</namespace>

wird z.B.

'GHSVS\\Module\\DemoGhsvs\\Site\\' => [JPATH_SITE . '/modules/mod_demoghsvs/src']

Ach, Wunder! Der Joomla-NamespaceMapper (/libraries/namespacemap.php) hat links noch ein Site\\ hinzugezaubert.

Schon am einleitenden GHSVS\ sieht man, dass das keine "echte" Pfadangabe ist, sondern erst durch Joomla-Magie im Hintergrund in einen echten Pfad gewandelt wird. Wäre unser Modul eines im Joomla-Core würde der Namespace lauten (mit Joomla\ beginnend):

<namespace path="src">Joomla\Module\DemoGhsvs</namespace>

Alle meine Erweiterungen, obwohl sie ja verschiedener Art sind (Plugins, Module, Komponenten etc.) und in unterschiedlichsten Verzeichnissen liegen und sogar auf unterschiedlichsten Internetseiten betrieben werden, haben einen Namensraum, der mit meinem individuellen, selbst gewählten GHSVS beginnt. Meine private Konvention.

Ich könnte auch in dieser Erweiterung als ersten Teil FirLeFanz\ verwenden und in einer anderen Furlapure\. Oder Joomla\, was ich anfänglich versehentlich gemacht hatte bis ich verstand. Hauptsache sie sind konsistent innerhalb der jeweilgen Erweiterung.

Den Rest des namespace-Tags, das Module\ gefolgt vom Namen des Moduls, sollte man aber tunlichst so in der Art formulieren wie zu sehen. Übrigens, auch, wenn das Modul ein Administrator-Modul ist und nicht ein Site-Modul wie unser Demomodul mod_demoghsvs.

Das path="src" im Tag sagt dem Joomla, "alle zum Namespace gehörigen Dateien findest du im src/-Ordner dieses Moduls". Auch Konvention. Warum anders machen? (wenn keine Not...)

Zwei Eingabefelder

Merken kann man sich für Folgendes noch, dass das Demomodul zwei eigene Eingabefelder im Manifest-XML ganz altherkömmlich definiert hat; in Zeilen 27 und 30. Eins für ein Bild (name="meinImage"), eins für einen Text (name="meinText").

Die Helper-Datei src/Helper/DemoGhsvsHelper.php

<?php

namespace GHSVS\Module\DemoGhsvs\Site\Helper;

\defined('_JEXEC') or die;

class DemoGhsvsHelper
{
	public function getMeinText($params)
	{
		$text = trim($params->get('meinText', ''));
		return $text;
	}

	public function getMeinImage($params)
	{
		$image = trim($params->get('meinImage', ''));
		return $image;
	}
}

In Zeile 3 wird der Namespace-"Pfad" der Datei angegeben. Die ersten drei Elemente, ich nenne sie mal "Master-Namensraum der Erweiterung", kennen wir schon aus der XML-Manifestdatei. Danach folgt ein Teil Site\, da es sich um ein Modul für das Frontend, also "Joomla-Client = Site" handelt. Wäre es ein Backendmodul würde da Administrator\ stehen. Es ist halt mal so, dass Site-Module im Ordner /modules/ liegen, während Administrator-Module im Verzeichnis /administrator/modules/ rumdümpeln. Joomla braucht also in der Namensraum-Angabe einen entsprechenden Hinweis.

Am Ende der Teil Helper. So heißt der Ordner, in dem die Datei liegt. Wären mehrere Helper im Ordner angelegt, bekäme jeder von denen exakt die selbe namespace-Zeile.

In Zeile 7 wird die Klasse benannt. Exakt wie der Dateiname; ohne *.php.

Wieso ein Rückwärtsstrich vor Zeile 5? Ein Hinweis, dass die allgemeine PHP-Funktion defined() außerhalb des Namespaces der Datei zu finden ist. Sonst sucht PHP unter bestimmten Umständen ggf. in "falschen" Namensräumen nach einer defined()-Methode. Auch, wenn es nicht immer einen Fehler gibt, kann so sinnloses Suchen verhindert werden. So ungefähr; ein komplexeres Thema.

Auffällig noch, dass die zwei Helper-Methoden (Zeilen 9 und 15) nicht als static deklariert sind, wie oft üblich. Das ist für Gewohnheitstiere etwas Denklast, weil man sie von "sonstwo" nicht mehr via Doppelpunkt-Konstrukten wie DemoGhsvsHelper::getMeinText($params) verwenden kann.

Die Datei services/provider.php

Die muss bei einfachen Modulen wie unserem nur an zwei Stellen angepasst werden.

<?php
defined('_JEXEC') or die;

use Joomla\CMS\Extension\Service\Provider\HelperFactory;
use Joomla\CMS\Extension\Service\Provider\Module;
use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;

return new class () implements ServiceProviderInterface
{
	public function register(Container $container)
	{
		$container->registerServiceProvider(
			new ModuleDispatcherFactory('\\GHSVS\\Module\\DemoGhsvs'));
		$container->registerServiceProvider(
			new HelperFactory('\\GHSVS\\Module\\DemoGhsvs\\Site\\Helper'));
		$container->registerServiceProvider(new Module());
	}
};

In den Zeilen 15 und 17 sehen wir oben definierte Namespaces; allerdings mit jeweils zwei Rückwärtsstrichen vor den Elementen der "Pfade". Der erste ist identisch mit dem Master-Namensraum des Moduls, also wie in die Manifest-Datei eingetragen. Der zweite repräsentiert unseren Helper; oder besser: den Namensraum-"Ordner", in dem später zu ladende Helfer-Klassen rumliegen. Oder ist das schlechter?

Die einleitenden use-Zeilen 4 bis 8 sind Joomla-Klassen, die im weiteren Code der selben Datei benötigt/verwendet werden. Muss uns nicht weiter tangieren; außer, dass sie da sein müssen, sonst erhält man class not found-Fehlermeldungen. Und an den "komischen Rückwärtsstrich-Pfaden" kann man ahnen, dass auch für die irgendwie/irgendwo Namensräume definiert wurden. Nicht von uns. Zauber im Hintergrund.

Die provider.php wird von Joomla magisch geladen, da wir uns brav an die Konventionen der Grundstruktur halten.

Die services/provider.php findet man mehr und mehr in anderen Joomla-Erweiterungen-Typen (Komponenten, Plugins usw.); natürlich erheblich abweichend.

Die Datei src/Dispatcher/Dispatcher.php

Der "Disponent" sammelt mit Hilfe des Helpers oder ohne oder ganz anders (wie's dem Entwickler halt beliebt) die Variablen zusammen, die wir später im Modul-Layout tmpl/default.php verwenden können, wie schon früher. Einige, wie $module, $params waren (und sind) immer ohne Zutun da, andere hat man gerne in der jetzt ja nicht mehr vorhandenen mod_demoghsvs.php "zusammengebastelt". Und jetzt halt in der Dispatcher.php.

Altes Beispiel

Hätte ich den Wunsch, dass die Layout-Datei eine Variable $meinText direkt verwenden kann (siehe oben: eines der beiden Eingabefelder des Moduls), würde man in die mod_demoghsvs.php reingetäteren haben tun:

$meinText = $params->get('meinText', '');

--- DAMIT KANN ICH DANN IN DER default.php DIREKT VERWENDEN: ---
echo $meinText;

Und jetzt neu, der Dispatcher

<?php

namespace GHSVS\Module\DemoGhsvs\Site\Dispatcher;

\defined('_JEXEC') or die;

use Joomla\CMS\Dispatcher\AbstractModuleDispatcher;
use Joomla\CMS\Helper\HelperFactoryAwareInterface;
use Joomla\CMS\Helper\HelperFactoryAwareTrait;

class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareInterface
{
	use HelperFactoryAwareTrait;

	protected function getLayoutData()
	{
		$data = parent::getLayoutData();

		$helper = $this->getHelperFactory()->getHelper('DemoGhsvsHelper');

		$data['meinImage'] = $helper->getMeinImage($data['params']);
		$data['meinText'] = $helper->getMeinText($data['params']);
		$data['modId'] = 'modId-' . $data['module']->id;
		return $data;
	}
}

Puuh! Zum Glück kann man aber das Grundgerüst nahezu immer 1:1 kopieren.

Kennen wir schon vom Helper (siehe oben): In Zeile 3 wird der Namespace-"Pfad" der Datei unseres Moduls angegeben und muss je nach Modul entsprechend angepasst werden; also das DemoGhsvs\. Die ersten drei Elemente, ich nenne sie mal "Master-Namensraum der Erweiterung", kennen wird schon aus der XML-Manifestdatei. Danach folgt ein Teil Site\. Am Ende der Teil Dispatcher. So heißt der Ordner, in dem die Datei liegt.

Etwas klarer wird damit auch der Sinn dieser namespace-Zeilen. Es gibt viele, viele Klassen im Joomla, die einfach nur class Dispatcher heißen und die in unserem Modul nicht vielleicht ModDemoDispatcher genannt werden muss, damit sie von anderen unterschieden werden kann. In Zeiten, wo man noch mit require und Kram arbeitete, um Klassen einzubinden, musste man aufpassen, dass Klassennamen eindeutig waren, um keine harten PHP-Fehler zu sehen.

Kennen wir schon vom "Provider" (siehe oben): Die use-Zeilen 7 bis 9 sind Joomla-Klassen, die im weiteren Code der selben Datei benötigt/verwendet werden. Zeile 7 ist absolut elementar, auch, wenn unser Modul keinen eigenen Helper verwenden wollte. Sie macht die Parent-Klasse, die Elternklasse verfügbar die uns selbst zusätzliche Zeilen Code erspart. Zeilen 8 und 9 sind Helper-spezifisch, wenn wir nach den Regeln der "neuen Struktur" weitermachen wollen. Auch, wenn sie komplett anonym sind, also auf unseren Helper noch gar nicht explizit verweisen.

Zeilen 11 bis 13 sehen superfies aus, haben aber den Vorteil, dass man sie plump kopieren kann.

Zeile 15 eröffnet die ebenfalls elementare Methode getLayoutData(). Dass sie eine protected ist, zeigt uns, dass Joomla-Magie sie nutzen wird und wir gar nicht die Möglichkeit hätten, sie aus unserem sonstigen Code heraus direkt nutzen zu können. Dafür müsste sie public sein.

Zeile 17 bemüht die Methode  getLayoutData() der Elternklasse AbstractModuleDispatcher. Sie gibt uns eine Sammlung von Variablen zurück, die wie gewohnt in der Layout-Datei direkt nutzbar sind. Derzeit (Joomla 4.3.1) sind das

  • $module (das Module-Objekt, in dem sich z.B. $module->title versteckt)
  • $params (die berühmten Parameter des Moduls. U.a. die gemachten Einstellungen/Eingaben im Modul)
  • $app (das Application-Objekt, das man ansonsten vielleicht per Factory::getApplication() selbst dingsen müsste)
  • $input (das Input-Objekt, wenn man bspw. den aktuellen View abfragen will ($view = $input->get('view', ''))
  • $template (der Templatename, z.B. "cassiopeia")

Diese sind reserviert, sozusagen. Aber man darf sie natürlich auch erweitern/manipulieren.

Aaaaber im Moment, in unserer Methode getLayoutData() liegen die noch in einem Array $data. Wenn wir davon folgend was nutzen wollen, verwenden wir noch $data['template'] und nicht $template und so weiter. Siehe Zeile 23, wo ich die eigene Variable $modId vorbereite.

Andererseits wissen wir jetzt aber auch, wo unsere selbst zu definierenden Variablen reingepackt werden. Eine Zeile

$data['killekille'] = 'KolloKalle';

ermöglicht in der Layout-Datei später ein direktes

echo $killekille;

um "KolloKalle" anzuzeigen.

In Zeile 19 wird unser Helper konnektiert. Das sieht wüst aus und baut auf der Arbeit auf, die im Provider (siehe oben) geleistet wurde.

Zeilen 21 und 22 holen sich nun mit Hilfe des DemoGhsvsHelper die $data-Arrayinhalte für die späteren "direkten" Variablen $meinImage und $meinText. Beachte, dass wir an die im Helper befindlichen beiden Funktionen die Moduleparameter als $data['params'] übergeben müssen.

Natürlich, natürlich und ja, ja weiß ich ja, ist bei unserem einfachen Beispiel der Helper eigentlich übertrieben, wenn man sich ansieht, was die beiden Funktionen darin eigentlich machen; recht plump die beiden Parameter auslesen. Geht es hier ja auch gar nicht drum ;-)