Joomla 4 Development
Joomla 4 Development
Content
1. Intro 1
2. Preface 5
I. Component 25
5. A Menu Item 49
49.Favicon 513
50.Lighthouse 517
Index 543
1. Intro
In this book you will learn the basics of Joomla 4 and create extensions without complicated tools.
The book contains references to further reading material and exercises at the end of each chapter.
After reading the book, you will know the basics to create your own Joomla 4 extension. And, what is
important nowadays: I’ll keep the text up to date.
1.1. Thanks
Thank you very much to everyone who reported an error in the text at github.com/astridx/meinblog/pulls
or in the sample code at codeberg.org/astrid/j4examplecode/pulls. These suggestions are a great help
and very important to improve the content.
1.2. FAQ
How do you ideally read this book? Please do not read the text on the sofa, but in front of a computer
on which Joomla 4 is installed. The folder structure of the files here in the book is the same as that of
Joomla. In each chapter there is a section that describes the new files to be added and one that lists
the files to be changed. Copy all this data into your Joomla installation. There is no need to type long
sections of code yourself. You can copy them from the example files. Most changes are immediately
visible and testable in Joomla. Sometimes it is necessary to reinstall the extension. The last section of
each chapter describes the easiest way to install and test the new functions.
Not all the texts in the administration section are readable on the pictures in the printed book. My
main idea was to give you some orientation. I assume that you opened the backend at the same time
as you were reading. In my opinion, the arrows in the pictures are sufficient to follow the examples.
You are not satisfied with the quality of the pictures in the book?
The sample code is on Github. Please note that the zip you download from Github via the Code button
is not an installable file. How to create this installable file is explained in the book and there is an
example under Releases in the Github repository. The final version of the sample code can be found
at codeberg.org/astrid/j4examplecode. Each chapter has its own branch. At the beginning of each
chapter it says:
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t0...t1
You can find the names of the branches here. t0 is the branch of the previous chapter and t1 is the branch
that is worked on in this chapter. The link https://codeberg.org/astrid/j4examplecode/compare/t0...t1
shows you the current changes in the tab Changed Files.
How do I get updates? You can always find the latest content on my website1 .
Is the learning material up to date? Books on programming are often out of date shortly after their
publication. Since I have published this book as a self-publisher, it is possible for me to update it at
short notice if necessary. Whenever something significant changes, I will revise the book and publish a
new version.
How can I get help reading the book? The sample files are in a Github Repository2 . Feel free to open
an issue there. This way you get help. Or you offer help yourself. Supporting each other helps you and
others. Please don’t expect a direct answer from me, because I don’t always have the time. Would you
like some feedback, please ask in the Joomla Community, for example in the Joomla Forum3 or at
Joomla Stack Exchange4 .
How and where do I report an error? If you find an error in the code, please report it via Github Issue5
in the repository of the sample files. You found a bug in the text of the book? Please open an issue in
the repository with the contents of my blog6 or write me an email. Contact information can be found
on the website astrid-guenther.de. I am very grateful for your help!
How do you read the code examples in the book? If a file is new, it is printed in full. If a file has
changed, I just printed the change. I have marked lines that have been added with a plus sign. Deleted
lines are preceded by a minus sign.
1
blog.astrid-guenther.de/en/der-weg-zu-joomlae4-erweiterungen
2
codeberg.org/astrid/j4examplecode
3
forum.joomla.org/
4
joomla.stackexchange.com
5
codeberg.org/astrid/j4examplecode/issues
6
github.com/astridx/meinblog/issues
You don’t know Joomla yet? If you are learning web development and have a basic understanding of
CSS and HTML, this book will give you the essentials you need to program Joomla 4 extensions. If you
feel unsure and think your Joomla knowledge is incomplete, fill in that gap before continuing with the
book. In the book you will find hints and links to basic knowledge.
If you used Joomla extensively before, the new Joomla 4 era seems overwhelming. The basic knowl-
edge hasn’t changed, it’s still Joomla and HTML — so this book will help you make the switch.
If you use any other content management system, you are familiar with the various aspects of web
development. After learning the basics with this book, you will quickly find your way around Joomla
4.
If you work in design, user interaction, usability or user experience, don’t hesitate to pick up this
book. If you are familiar with HTML and CSS, this will be beneficial. After going through some Joomla
basics, you will understand the contents of this book. Nowadays, UI and UX are getting closer to the
implementation details. It benefits you to know how things work in the code.
Interested in who I am? I am happy about that! You can find information about me or information
about a working relationship on my website astrid-guenther.de.
2. Preface
If you are new to Joomla, please read Absolute basics of how a component works1 .
This tutorial is intended for Joomla 4. For information on creating a component for Joomla 3, see
Developing a Model View Controller Component / 3.x2 .
You need Joomla 4.x for this tutorial. You can find Joomla 4 on GitHub3 on the Developer Website4 or
create a free website at launch.joomla.org.
This tutorial does not create a practical example. I have intentionally kept everything general. My main
goal is to show you how Joomla works - and to help you understand it better yourself. In the end, you
replace the name “foo” in all files with the name of your component and extend it with your special
requirements. If you like, you can use the script duplicate.sh5 for this.
Therefore, this tutorial is primarily intended for programmers who want to create a new com-
ponent and do not know Joomla yet. The tutorial is also a help for programmers of a Joomla
3 component, if they extend their component for Joomla 4. For example, if you are working on
validating your Joomla 3 component, you will find what you need in the chapters on server-side
and client-side validation - no more and no less.
Each chapter builds on the previous builds. However, if you are interested in a specific topic, feel free
to look at a separate chapter. Be aware, however, that elements integrated in a previous chapter may
be necessary.
Why this structure? There are many examples of components in the standard Joomla. For example
1
docs.joomla.org/Absolute_Basics_of_How_a_Component_Functions
2
docs.joomla.org/J3.x:Developing_an_MVC_Component
3
github.com/joomla/joomla-cms
4
developer.joomla.org/nightly-builds.html
5
github.com/astridx/boilerplate/blob/t43/duplicate.sh
• com_content
• com_banner
• com_tags or
• com_contact
In each component you see implementation details in context. Each is complex and finding and
separating certain elements, such as page numbering or custom fields, is a hassle. This tutorial focuses
on one detail per chapter.
You create a component for Joomla 4, reusing the many existing implementations in Joomla. You
are not reinventing the wheel. Joomla offers a whole range of standard functions.
If you want to get started immediately, scroll to The first view in the backend. Below you will find some
things about Joomla 4 that you do not necessarily need for editing. However, some of them are good
to know.
2.3. Basics
• Components: A component fills the main content of the site. It usually uses data that is stored in
the database.
• Modules: A module is an add-on to the site that extends the functionality. It has a secondary part
on the web page and is displayed at different positions. It is selectable on which active menu
elements it is displayed. Modules are light and flexible extensions. They are used for small parts
of the page that are less complex and are displayed across different components.
• Plugins: A plugin processes the output generated by the system. It is not normally called as a
separate part of the site. It takes data from other sources and manipulates it before output. A
plugin works in the background.
• Languages: The most basic extensions are languages. Essentially, language package files consist
of key/value pairs that allow the translation of static text strings in the Joomla source code.
• Templates: A template defines the design of your Joomla website.
• CLI (used to access Joomla from the command line and for cron jobs);
• API (Web Services - used to create APIs for machine-accessible content);
This text is about the Joomla code. It is not about the latest tools for developers. However, a few things
are essential.
You want to program an extension for Joomla and therefore need an environment in which Joomla is
installed. In my opinion, a XAMPP server package6 on a local workstation is an ideal prerequisite for
developing new extensions. The direct access to the files of Joomla in the local file system facilitates
the handling.
A good editor is also essential. This should be one you feel comfortable with. Wikipedia maintains a list
of editors.7 .
More convenience is offered by an integrated development environment IDE. By convenience I mean
functions like
You can also get an overview of IDEs from Wikipedia using a List of IDEs9 .
In the Joomla community, the IDE PHPStorm10 , which is subject to a fee, is popular. Users of Visual
Studio Code11 are becoming increasingly common. Also worth mentioning are NetBeans and Eclipse.
6
www.apachefriends.org/index.html
7
en.wikipedia.org/wiki/List_of_text_editors
8
wikipedia.org/wiki/Git
9
en.wikipedia.org/wiki/Comparison_of_integrated_development_environments
10
www.jetbrains.com/phpstorm/
11
code.visualstudio.com/
Are you looking for instructions on how to set up the development environment? Joomla with Vi-
sual Studio Code can be found in the Joomla Documentation12 . For PHPStorm, Jetbrains provides a
description.
If you like, you can read my first steps with Visual Studio Code at blog.astrid-guenther.de/ubuntu-
vscode-docker-lamp.
2.4.2.1.1. Frontend and Backend accessible and with Bootstrap 5 Joomla 4 integrates Bootstrap
5 into the Joomla core. Thereby, the included templates are accessible and correspond to level AA of
WCAG 2.1. WCAG 2.1 complements WCAG 2.0 and is the web standard for digital accessibility, which is
mandatory for public bodies in the European Union. As an extension developer, it is not mandatory
that you use Bootstrap 5. But technically it should comply with the latest standards. It would be a
shame not to use the good template that Joomla offers.
2.4.2.1.2. Optimised Web Assets With a single call, Web Assets allow developers to load multiple
Javascript and CSS files in a specified order. For example, if an extension developer uses styles that
depend on Font Awesome being loaded first and they know that Joomla 4 uses the icon font set, Web
Assets come into play. Web Assets are described in several places in this tutorial.
Use the Web Asset Manager when developing for Joomla 4. All calls to HTMLHelper::_('
stylesheet or script ...) work, but these assets are appended after the Web Asset Man-
ager assets. This results in overriding styles that are set in the template. Thus, a user does
not have the possibility to manipulate by means of a user.css. See in this context (Issue
35706)https://github.com/joomla/joomla-cms/issues/35706.
2.4.2.1.3. Web Services enable automated data exchange Joomla 4 web services make content
accessible to other websites or mobile applications. What is a web service? Different definitions cause
confusion. The SOAP standards are referred to as web services. Others know them under the term
REST API. The W3C generally defines a web service as an interface for automated communication via
computer networks. The API integration in Joomla 4 implements such an interface in the core of the
CMS with the help of REST. In the example we are building in this text, we also support the Joomla
API.
12
docs.joomla.org/Visual_Studio_Code
2.4.2.1.4. Workflow With the new Workflow component, it is possible to link website content to a
workflow. Thirt Party extensions can also offer a workflow with the core extension. This function is not
yet included here in the book.
2.4.2.1.5. Many other changes and improvements Joomla 4 includes new security features such
as support for prepared statements for database systems. This prevents SQL injection because the
database checks the validity of parameters. In addition, the code base has been restructured. The code
was thoroughly cleaned up, obsolete functions removed and PHP namespaces introduced.
This text is primarily written for developers who are starting a new extension. Nevertheless, issues re-
lated to compatibility with Joomla 3 are of interest. A page in the Joomla documentation13 summarises
the important points.
The purpose of Joomla extensions is to have a system that can be extended. It is possible that your
code and Joomla Core code can be provided with new functions independently of each other.
If make changes to Joomla itself, these will be overwritten with the next update.
You have the feeling that your function can only be implemented with a core hack? Your feeling is
wrong! There is always a solution that leaves the system files untouched.
That you should not change the system files does not mean that you do not even look at them. Quite
the opposite! By reading you will come across lots of code that is not documented anywhere. If you
are not sure how to best implement a function, just rummage around in the Joomla code. The solution
is usually be found in the heart of Joomla.
The following text was added to the README on Github using the PR 28436a : “Joomla cre-
ates a cache of the namespaces of its extensions in JOOMLA_ROOT/administrator/ cache/
autoload_psr4.php. If extensions are created, deleted or removed in git then this file needs to
be recreated. You can simply delete the file and it will be regenerated on the next call to Joomla.”
a
github.com/joomla/joomla-cms/pull/28436
2.4.6. Namespace
Remember to include the path="src" parameter if you put the namespace files in the src sub-
directory. This is common in Joomla and the sample extensions created in this tutorial also use this
directory[github.com/astridx/boilerplate/blob/62a970704ee2899addd3922e88c918b7f6af72a2/
src/administrator/components/com_foos/foos.xml#L12].
Why use namespaces? All PHP classes are thus organised in a defined structure and automatically
loaded via the Classloader. Thereby ContentModelArticles becomes Joomla\Component\
Content\ Administrator\Model\ArticlesModel.
JLoader can process the namespaces automatically and distinguishes between front-end and back-
end classes.
Files with namespaces can be found in the directory /srca
a
github.com/joomla/joomla-cms/pull/27687
You will notice that some of the Joomla 4 folder and file names start with upper case letters and others
with lower case letters. At first glance, this seems unstructured. At second glance it makes sense.
The folders in upper case contain PHP classes with namespace. Those in lower case contain XML files,
template files. There are a few lower case folders that contain PHP files. These are necessary to ensure
compatibility with Joomla 3. Often these are helper files.
The component MVC classes have more meaningful names in Joomla 4. For example, the
controllers now have controller as a suffix for their class name. Thus, FooNamespace\Component
\Foos\ Administrator\Controller\Foos becomes FooNamespace\Component\Foos\
Administrator\Controller\ FoosController.
Additionally, the default controller, which in Joomla 3 is just called Controller, gets the name
DisplayController to better reflect what the class does. See: https://github.com/joomla/joomla-
cms/pull/17624
2.4.9. index.html?
Do you need an empty file index.html in each folder of your component? The index.html is no
longer needed, as that is directory listings not allowed in the default configuration14 . If you are further
interested read the discussion on the topic in the Google Group15 .
Do you know how those responsible at Joomla decide which functions are supported and what is
not pursued further? That’s what the statistics plugin16 is for. Thanks to the users who activate this
extension, important information flows into the development.
2.4.11. Why is a blank line inserted at the end of a source code file in Joomla files?
There are several reasons why a blank line at the end of a file is included as a requirement in the Joomla
Coding Standards:
• Apart from the fact that it is a nicer cursor position when you go to the end of a file in a text editor,
a line break at the end of the file allows you to easily check that the file has not been truncated.
• When you paste something at the end of a file, the difference display in Git shows that you’ve
changed the last line, when the only thing you’ve actually pasted is a line break. This is confusing.
• Today it doesn’t matter, but: many older tools in the programming field misbehave if the last
line of data in a file is not terminated with a newline or a carriage return/newline combination.
14
github.com/joomla/joomla-cms/pull/4171
15
groups.google.com/forum/#!topic/joomla-dev-cms/en1G7QoUW2s
16
developer.joomla.org/about/stats.html
2.4.12. PHP
2.4.12.1. Why should you omit the closing tag of PHP sections at the end of a file?
The closing tag of a PHP block at the end of a file is optional, and in some cases it is helpful to omit it.
By omitting the closing tag, you can avoid accidentally inserting spaces or line breaks at the end of the
file. For further explanation, see php.net[php.net/basic-syntax.instruction-separation].
2.4.12.2. PHP operators for equality (== two equal signs) and identity (=== three equal signs)
1 === 1: true 1 == 1: true 1 === "1": false // 1 is an integer, “1” is a string 1 == "1": true // “1” is
converted to an integer, which is 1 "foo"=== "foo": true // both operands are strings and have the
same value
Note: Two instances of the same class with equivalent elements will be evaluated by the operator with
three equal signs === with false. Example:
1 $a = new stdClass();
2 $a->foo = "bar";
3 $b = clone $a;
4 var_dump($a === $b); // bool(false)
In Joomla, we use single quotes. Using single quotes is more performant, usually more readable and
more straightforward when used with associative arrays. PHP does not need any additional processing
to interpret what is inside the single quote. If you use double quotes, PHP must check to see if there
are any variables in the string.
More information about this and the explanation of two other ways to use strings in PHP can be
found on the website php.neta .
a
php.net/manual/en/language.types.string.php
2.4.12.3.1. Single quotes The simplest way to specify a string is to enclose it in single quotes. Single
quotes are generally faster, and anything enclosed in quotes is treated as a single string. Example:
2.4.12.3.2. Double quotes Use double quotes in PHP to avoid using a period when separating. Use
curly braces {} in strings to enclose variables if you don’t like to use the concatenation operator (.).
Example:
PHP offers an additional notation for control structures. This is especially handy when outputting
larger blocks of HTML directly - without using echo. Use them in template files. This way they remain
clear.
Use
instead of
In this way, a single line is self-contained and the HTML code is still clearly structured.
As an extension developer, you ideally develop your extension so that the database prefix is variable.
For this purpose, one uses the string #__. The string #__ is replaced with the correct prefix at runtime
by Joomla.
Where do you best store JavaScript, CSS and image files? Store these data in the directory media in the
Joomla root directory. This way it is possible to overwrite them. This is particularly advantageous for
CSS files to make the design of the whole Joomla Website consistent. The Best Practice Guidelines17
also recommend this.
Examples: For this tutorial extension I will later use media/com_foos/js/ for the JavaScript files
of the component. The CSS files of the module mod_articles_news can be found in the directory
media/mod_articles_news/css/. And the images for the plugin plg_content_vote are in
the folder media/plg_content_vote/images/.
You want to use icons but don’t want to add your own library. Use the free icons from fontawe-
some.com/icons in the frontend and backend. At least if you use the standard templates Cassiopeia
and Atum, this will work. If your template does not support FontAwesome, you can load the icons
yourself via the WebassetManager. In Joomla Fontawesome is delivered with the template. Marking
them as dependency18 is sufficient.
Attention: In Joomla Core files, you cannot simply copy them, because Joomla add the text icon-
in front of the icon name. This is then converted via the file build/media_source/ system/
scss/_icomoon.scss for Fontawesome. In this way, only the icons included in the previously
mentioned file will work. Why does Joomla complicate the selection of Font Awesome icons? The
reason for this is as follows: Extensions that were programmed for Joomla 3 can still be used.
A new JLayout19 as of Joomla 4.0.5 allows developers to output HTML image tags easily:
1
2 <?php
3 echo '<img src="' . $imageURL .'" alt="' . htmlspecialchars($imageAlt,
ENT_COMPAT, 'UTF-8') . '">';
4 ?>
1 <?php
2 echo LayoutHelper::render('joomla.html.image', ['src' => imageURL, 'alt
' => $imageAlt]);
3 ?>
Advantages:
2.4.17. Dates
An error occurred in one of my Joomla extensions. Dates and times were not displayed correctly. The
time zone was obviously the problem. The solution seemed simple at first glance. I had worked with
dates and the class DateTime in PHP 20 in the past and had experience with time zones. In Joomla,
however, there are special features.
Let’s look at my concrete problem. A user who lives in the time zone “Australia/Adelaide” (UTC/GMT
+10:30 hours) fills out a form in the summer which contains a field in which a date is stored. The
timezone “Australia/Adelaide” has a difference to the timezone “UTC” of +10:30 hours in summer, in
wintertime the difference is +9:30 hours.
20
php.net/manual/en/class.datetime.php
The server is located in Johannesburg, thus in South Africa. The time zone of the server is set to
“Africa/Johannesburg” in the global configuration. The time zone “Africa/Johannesburg” has a dif-
ference to the time zone “UTC” of +2:00 hours in summer time, in winter time the difference is +1:00
hours.
Figure 2.2.: Joomla 4 | Set time zone of the server in the global configuration
My extension is a contest. The date is 4.10.2022. The time is 00:00:01. That is exactly when the competi-
tion ends. It is important that the game is made inactive at the same time all over the world. This is
different from, for example, an Advent Calendar. With the Advent calendar, it may be intentional that
something happens at a certain time in each time zone. The first door opens in Australia, in Africa and
in Europe when it is locally the 1.12. and not at the same time.
1 <field
2 name="advent_publish_up"
3 type="calendar"
4 label="COM_AGADVENTS_FIELD_PUBLISH_UP_LABEL"
5 translateformat="true"
6 showtime="true"
7 size="22"
8 filter="user_utc"
9 />
To my surprise, I notice during my first tests that instead of 2022-10-04 00:00:01 the string
2022-10-03 13:30:01 is stored in the database. With a little research, I realise that the calendar
field converts the date to the UTC time zone and stores it in this form. The information about the time
zone itself is not stored in the database. The latter is not necessary if it is ensured that one and the
same time zone is always used. In the case of Joomla, this is UTC.
1 ...
2 public function filter($value, $group = null, Registry $input = null)
3 {
4 // Make sure there is a valid SimpleXMLElement.
5 if (!($this->element instanceof \SimpleXMLElement)) {
6 throw new \UnexpectedValueException(sprintf( %s::filter `
element ` is not an instance of S i m p l e X M L E l e m e n t , \
get_class($this)));
7 }
8
9 if ((int) $value <= 0) {
10 return ;
11 }
12
13 if ($this->filterFormat) {
14 $value = DateTime::createFromFormat($this->filterFormat, $value
)->format( Y -m-d H:i: s );
15 }
16
17 $app = Factory::getApplication();
18
19 // Get the field filter type.
21
docs.joomla.org/Calendar_form_field_type
The variable $value contains the value entered by the user, in this case 2022-10-04 00:00:01. This
is converted to the time zone UTC and then stored in the variable $return. The converted value is
passed to the database for saving. This way you can always be sure that the date stored in the database
for the time zone UTC is correct.
In Joomla, SERVER_UTC and USER_UTC offer the possibility of outputting the date either in the time
zone with which the web server is configured or in the time zone set by the user. In short, the constants
indicate whether the value of the variable $value is in the time zone stored at the user or the time
zone of the webserver, which is configurable in the global configuration.
Why is the date converted to the time zone UTC for saving in the database? Actually, it doesn’t
matter which time zone you choose. It makes sense to define one. That way you always have a fixed
starting point. Otherwise, you would have to determine the conversion factor or offset for each
time zone combination. Joomla’s standard behaviour ensures that the date stored in the database
is correct for the UTC time zone and that only the difference/offset to this time zone is needed for
the conversion. The default time zone can be configured in the file configuration.php. The
variable is called $offset. The default is public $offset = 'UTC';. When displaying the
time in the frontend of the website, one now only has to calculate the difference between UTC
1 ...
2 public function __construct($date = now , $tz = null)
3 {
4 // Create the base GMT and server time zone objects.
5 if (empty(self::$gmt) || empty(self::$stz)) {
6 // @TODO: This code block stays here only for B/C, can be
removed in 5.0
7 self::$gmt = new \DateTimeZone( GMT );
8 self::$stz = new \DateTimeZone(@date_default_timezone_get());
9 }
10
11 // If the time zone object is not set, attempt to build it.
12 if (!($tz instanceof \DateTimeZone)) {
13 if (\is_string($tz)) {
14 $tz = new \DateTimeZone($tz);
15 } else {
16 $tz = new \DateTimeZone( UTC );
17 }
18 }
19
20 // Backup active time zone
21 $activeTZ = date_default_timezone_get();
22
23 // Force UTC timezone for correct time handling
24 date_default_timezone_set( UTC );
25
26 // If the date is numeric assume a unix timestamp and convert it.
27 $date = is_numeric($date) ? date( c , $date) : $date;
28
29 // Call the DateTime constructor.
30 parent::__construct($date, $tz);
31
32 // Restore previously active timezone
33 date_default_timezone_set($activeTZ);
34
35 // Set the timezone object for access later.
36 $this->tz = $tz;
37 }
38 ...
The class Joomla\CMS\Date is used, among other things, in the function getDate of the file /
libraries/src/Factory.php, which helps Joomla extension programmers to always output the
date with the appropriate offset, i.e. in the correct time zone. The code of the function getDate() is
printed below for the sake of completeness:
1 ...
2
3 public static function getDate($time = now , $tzOffset = null)
4 {
5 static $classname;
6 static $mainLocale;
7
8 $language = self::getLanguage();
9 $locale = $language->getTag();
10
11 if (!isset($classname) || $locale != $mainLocale) {
12 // Store the locale for future reference
13 $mainLocale = $locale;
14
15 if ($mainLocale !== false) {
16 $classname = str_replace( - , _ , $mainLocale) .
Date ;
17
18 if (!class_exists($classname)) {
19 // The class does not exist, default to Date
20 $classname = Joomla \\CMS\\Date\\ D a t e ;
21 }
22 } else {
23 // No tag, so default to Date
24 $classname = Joomla \\CMS\\Date\\ D a t e ;
25 }
26 }
27
28 $key = $time . - . ($tzOffset instanceof \DateTimeZone ?
$tzOffset->getName() : (string) $tzOffset);
29
30 if (!isset(self::$dates[$classname][$key])) {
31 self::$dates[$classname][$key] = new $classname($time,
$tzOffset);
32 }
33
34 $date = clone self::$dates[$classname][$key];
35
36 return $date;
37 }
38 ...
How do you display the date in the correct time zone in your Joomla extension in the frontend? You
are on the safe side if you use the functions provided by Joomla. Let’s take a look at the interaction of
Let’s start very simply. The following code displays the string that is stored for the date in the
database.
1 echo $this->item->advent_publish_up;
1 2022-10-03 13:30:01
I have already explained why the time is displayed in the UTC time zone.
If you want to display the date in the time zone that is stored with the user, the following code shows a
possibility.
1 ...
2 $date = Factory::getDate($this->item->advent_publish_up, 'UTC');
3 $user = Factory::getApplication()->getIdentity();
4 $date->setTimezone($user->getTimezone());
5 echo $this->value = $date->format('Y-m-d H:i:s', true, false);
6 echo "<br><pre>";
7 print_r($date);
8 echo "</pre>";
If a user is logged in with the time zone set to Australia/Adelaide, the following text appears in
the frontend:
1 2022-10-04 00:00:01
2
3 Joomla\CMS\Date\Date Object
4 (
5 [tz:protected] => DateTimeZone Object
6 (
7 [timezone_type] => 3
8 [timezone] => Australia/Adelaide
9 )
10
11 [date] => 2022-10-04 00:00:01.000000
12 [timezone_type] => 3
13 [timezone] => Australia/Adelaide
14 )
If no user is logged in, the time zone of the server is fallback. In our example, the following text
appears:
1 2022-10-03 15:30:01
2
3 Joomla\CMS\Date\Date Object
4 (
5 [tz:protected] => DateTimeZone Object
6 (
7 [timezone_type] => 3
8 [timezone] => Africa/Johannesburg
9 )
10
11 [date] => 2022-10-03 15:30:01.000000
12 [timezone_type] => 3
13 [timezone] => Africa/Johannesburg
14 )
The following code displays the date in the time zone stored for the web server in the global configura-
tion.
1 2022-10-03 15:30:01
2
3 Joomla\CMS\Date\Date Object
4 (
5 [tz:protected] => DateTimeZone Object
6 (
7 [timezone_type] => 3
8 [timezone] => Africa/Johannesburg
9 )
10
11 [date] => 2022-10-03 15:30:01.000000
12 [timezone_type] => 3
13 [timezone] => Africa/Johannesburg
14 )
The following code outputs the date immediately in the standard time zone UTC.
1 2022-10-03 13:30:01
2
3 Joomla\CMS\Date\Date Object
4 (
5 [tz:protected] => DateTimeZone Object
6 (
7 [timezone_type] => 3
8 [timezone] => UTC
9 )
10
11 [date] => 2022-10-03 13:30:01.000000
12 [timezone_type] => 3
13 [timezone] => UTC
14 )
Conclusion: Depending on the use case, i.e. whether it is a competition where everything should
happen at the same time or an Advent calendar where the actual time is relevant, the date can be
programmed in the Joomla extension.
Part I.
Component
We’ll start with the basics. For this we create the View in the administration area rudimentary. At the
end of this text you know how to insert a menu item in the menu of the administration area. Via the
menu item you open the view to your component. Don’t be disappointed: This view contains nothing
more than a short text. You have a good basis for the next steps.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t0...t1
3.1.1.1. administrator/components/com_foos/foos.xml
foos.xml tells Joomla how to install our component. Just like modules and plugins, components
have an XML installation file that informs Joomla about the extension to be installed. This file is called
a manifest and contains details such as
Create a new file and name it foos.xml. This is the name of the extension without the prefix com_.
We will then go through each line and see what it does.
The first line is not specific to Joomla. It tells us that this is an XML file
Then we tell Joomla that this is a component. And we want the upgrade installation method to be
used. So it is possible to use this package not only for installation but also for an upgrade
Sometimes you will find a parameter with a version number in the <extension> tag of
the manifest. This is not used anywhere, so it is unnecessary. For more information, see
github.com/joomla/joomla-cms/pull/25820.
1 <name>COM_FOOS</name>
1 <creationDate>[DATE]</creationDate>
2 <author>[AUTHOR]</author>
3 <authorEmail>[AUTHOR_EMAIL]</authorEmail>
4 <authorUrl>[AUTHOR_URL]</authorUrl>
5 <copyright>[COPYRIGHT]</copyright>
6 <license>GNU General Public License version 2 or later;</license>
This is the first version of the component. We will give it the version number 1.0.0: <version
>1.0.0</version>. If we fix a small bug, the next number would be 1.0.1. If we introduce a new
feature, we choose 1.1.0. If we made major changes that alter implementations in earlier versions,
the next version would be 2.0.0. It is important that you use the three-part version numbering, as
this makes it easier to create updates later using semantic versioning.
1 <description>COM_FOOS_ XML_DESCRIPTION</description>
At the moment, this has no effect. Later, this text will change based on the language files we introduce
in one of the next chapters. The description of the component will be shown during installation and
when you click the menu System and open the submenu Manage | Extensions.
Next we set the HTML tag for the namespace. In the preface I explained why we use namespaces. Now
we create it practically. How do you name your namespace?
• The first element of the namespace is your CompanyName. For this tutorial I have used
FooNamespace. It is used to distinguish the code from the code in other extensions. This makes
it possible to use identical class names without conflicts. The namespace is also used to register
a service provider. A service provider is a PHP class that provides services.
• The second element is the type of extension: component, module, plugin, language or template.
• The third element is the name of the extension without the preceding com_, mod_, plg_ or tpl_,
in our case Foos.
1 <namespace>FooNamespace\Component\Foos</namespace>
With the script file you call code when your component is installed, uninstalled or updated.
1 <scriptfile>script.php </scriptfile>
Like Joomla itself, components have a frontend and an administration area. The folder
administrator/components/ com_foos contains all files used by the backend. You add
individual files with the tag “filename”. For a complete directory it is better to use the tag folder.
The files for the administration area of your component are all inside the tag administration. Here
is also a menu tag. This is the menu item that is displayed in the sidebar in the backend. We use the
language string COM_FOOS, which we will replace later with text from a language file.
1 <administration>
2 <!-- Menu entries -->
3 <menu view="foos">COM_FOOS</menu>
4 <submenu>
5 <menu link="option=com_foos">COM_FOOS</menu>
6 </submenu>
7 <files folder="administrator/components/com_foos">
8 <filename>foos.xml</filename>
9 <folder>services</folder>
10 <folder>src</folder>
11 <folder>tmpl</folder>
12 </files>
13 </administration>
We will look at the changelogurl and updateservers tags in more detail in the next chapter.
1<changelogurl>https://codeberg.org/astrid/j4examplecode/raw/branch/
tutorial/changelog.xml</changelogurl>
2 <updateservers>
3 <server type="extension" name="Foo Updates">https://codeberg.org/
astrid/j4examplecode/raw/branch/tutorial/foo_update.xml</server>
4 </updateservers>
You need this if you use the Download Key Manager. In general, this is only the case for commercial
extensions. You can find more information on Github at github.com/joomla/joomla-cms/pull/25553.
administrator/components/com_foos/foos.xml
1
2 <?xml version="1.0" encoding="utf-8" ?>
3 <extension type="component" method="upgrade">
4 <name>COM_FOOS</name>
5 <creationDate>[DATE]</creationDate>
6 <author>[AUTHOR]</author>
7 <authorEmail>[AUTHOR_EMAIL]</authorEmail>
8 <authorUrl>[AUTHOR_URL]</authorUrl>
9 <copyright>[COPYRIGHT]</copyright>
10 <license>GNU General Public License version 2 or later;</license>
11 <version>__BUMP_VERSION__</version>
12 <description>COM_FOOS_XML_DESCRIPTION</description>
13 <namespace path="src">FooNamespace\Component\Foos</namespace>
14 <scriptfile>script.php</scriptfile>
15 <!-- Back-end files -->
16 <administration>
17 <!-- Menu entries -->
18 <menu view="foos">COM_FOOS</menu>
19 <submenu>
20 <menu link="option=com_foos">COM_FOOS</menu>
21 </submenu>
22 <files folder="administrator/components/com_foos">
23 <filename>foos.xml</filename>
24 <folder>services</folder>
25 <folder>src</folder>
26 <folder>tmpl</folder>
27 </files>
28 </administration>
29 <changelogurl>https://codeberg.org/astrid/j4examplecode/raw/branch/
tutorial/changelog.xml</changelogurl>
30 <updateservers>
31 <server type="extension" name="Foo Updates">https://codeberg.
org/astrid/j4examplecode/raw/branch/tutorial/foo_update.xml
</server>
32 </updateservers>
33 <dlid prefix="dlid=" suffix="" />
34 </extension>
3.1.1.2. administrator/components/com_foos/script.php
administrator/components/com_foos/script.php
1
2 <?php
3 \defined('_JEXEC') or die;
4 use Joomla\CMS\Installer\InstallerAdapter;
5 use Joomla\CMS\Language\Text;
6 use Joomla\CMS\Log\Log;
7
8 class Com_FoosInstallerScript
9 {
10 private $minimumJoomlaVersion = '4.0';
11
12 private $minimumPHPVersion = JOOMLA_MINIMUM_PHP;
13
14 public function install($parent): bool
15 {
16 echo Text::_('COM_FOOS_INSTALLERSCRIPT_INSTALL');
17
18 return true;
19 }
20
21 public function uninstall($parent): bool
22 {
23 echo Text::_('COM_FOOS_INSTALLERSCRIPT_UNINSTALL');
24
25 return true;
26 }
27
28 public function update($parent): bool
29 {
30 echo Text::_('COM_FOOS_INSTALLERSCRIPT_UPDATE');
31
32 return true;
33 }
34
35 public function preflight($type, $parent): bool
36 {
37 if ($type !== 'uninstall') {
38 // Check for the minimum PHP version before continuing
39 if (!empty($this->minimumPHPVersion) && version_compare(
PHP_VERSION, $this->minimumPHPVersion, '<')) {
40 Log::add(
41 Text::sprintf('JLIB_INSTALLER_MINIMUM_PHP', $this->
minimumPHPVersion),
42 Log::WARNING,
43 'jerror'
44 );
45
46 return false;
47 }
48
49 // Check for the minimum Joomla version before continuing
50 if (!empty($this->minimumJoomlaVersion) && version_compare(
JVERSION, $this->minimumJoomlaVersion, '<')) {
51 Log::add(
52 Text::sprintf('JLIB_INSTALLER_MINIMUM_JOOMLA',
$this->minimumJoomlaVersion),
53 Log::WARNING,
54 'jerror'
55 );
56
57 return false;
58 }
59 }
60
61 echo Text::_('COM_FOOS_INSTALLERSCRIPT_PREFLIGHT');
62
63 return true;
64 }
65
66 public function postflight($type, $parent)
67 {
68 echo Text::_('COM_FOOS_INSTALLERSCRIPT_POSTFLIGHT');
69
70 return true;
71 }
72 }
The install function, as the name suggests, is called when the component is installed. At the moment,
text is output. It is possible to install sample data.
uninstall is called when someone uninstalls the component. At the moment, only text is displayed.
The update function update is called whenever you update the component. Have there been changes
to save locations in the extension? In that case, you might want to delete files? Then the update
method could look like this:
';
5
6 $this->removeFiles();
7
8 return true;
9 }
The preflight function is called before the component is installed, discover_installed, updated or
uninstalled. You can add code here to check the prerequisites like the PHP version or to check if another
extension is installed or not.
The postflight function is called after the component has been installed, discover_installed, updated
or uninstalled. This function is used to set default values for component parameters.
Note: In Joomla 3, only plugins called the Preflight method during the uninstall process and
Postflight was never used during uninstall. As of version 4.0, these two hooks are available during
the uninstall for all extension types.
Do you want to know exactly when which method is called? Then have a look at the
file /libraries/src/Installer/InstallerAdapter.php. The commands $this->
triggerManifestScript(''); will start the execution of the related method. For example,
the postflight function is triggered via $this->triggerManifestScript('postflight'
);. See Potential backward compatibility issues in Joomla 4a .
a
docs.joomla.org/Potential_backward_compatibility_issues_in_Joomla_4#CMS_Libraries
provider.php is used to implement the component services. Via an interface, the component class
defines which services it provides. A dependency injection container or DI container is used for this.
To register, ComponentDispatcherFactory and MVCFactory are mandatory for each component.
Registering CategoryFactory is at this place optional, we need CategoryFactory when we inte-
grate categories later. Using provider.php it is possible to introduce new services without breaking
backwards compatibility (BC). If you are not familiar with the concept of DI Container but would like to
learn more, you can find explanations and some examples in the following links:
• joomla-framework/di1 .
• docs/why-dependency-injection.md2 .
You often see the word “factory” in Joomla. This is because Joomla uses the factory design pattern4 .
The factory method is a pattern where the interface to create an object is an abstract method of an
inheriting class. However, the concrete implementation of the creation of new objects does not take
place in the superclass, but in subclasses derived from it. The latter implement the said abstract
method. To program extensions for Joomla it is not mandatory that you know the design patterns.
However, it can be worthwhile to think outside the box. In software engineering, a design pattern5 is a
general, reusable solution to a common problem. Someone else had the same problem and found a
solution. We don’t have to solve the same problem, but can build on it.
administrator/components/com_foos/services/provider.php
1
2 <?php
3
4 \defined('_JEXEC') or die;
5
6 use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
7 use Joomla\CMS\Extension\ComponentInterface;
8 use Joomla\CMS\Extension\Service\Provider\CategoryFactory;
9 use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
10 use Joomla\CMS\Extension\Service\Provider\MVCFactory;
11 use Joomla\CMS\HTML\Registry;
12 use Joomla\DI\Container;
13 use Joomla\DI\ServiceProviderInterface;
14 use FooNamespace\Component\Foos\Administrator\Extension\FoosComponent;
15
16 return new class implements ServiceProviderInterface
17 {
18 public function register(Container $container)
19 {
20 $container->registerServiceProvider(new CategoryFactory('\\
FooNamespace\\Component\\Foos'));
21 $container->registerServiceProvider(new MVCFactory('\\
FooNamespace\\Component\\Foos'));
22 $container->registerServiceProvider(new
ComponentDispatcherFactory('\\FooNamespace\\Component\\Foos'
));
23
24 $container->set(
25 ComponentInterface::class,
26 function (Container $container) {
27 $component = new FoosComponent($container->get(
ComponentDispatcherFactoryInterface::class));
28
29 $component->setRegistry($container->get(Registry::class
));
4
en.wikipedia.org/wiki/factory_method_pattern
5
en.wikipedia.org/wiki/software_design_pattern
30
31 return $component;
32 }
33 );
34 }
35 };
The file DisplayController.php is the entry point for the Model View Controller part in the admin-
istration area of the Foo component. Name the class DisplayController. Joomla expects it like this.
Extend BaseController to use many things out-of-the-box.
The main task of this controller is to prepare the display. Therefore the default controller is called
DisplayController. It calls the method display() of the parent class BaseController in the names-
pace Joomla\CMS\MVC\Controller - exactly this is the file libraries/src/MVC/Controller/
BaseController.php. In the Model-View-Controller model, controllers are often used to set up the
start environment.
Let’s create the DisplayController. As always, we first create the DocBlock. Here is an example of a
typical document block.
What does __BUMP_VERSION__ or __DEPLOY_VERSION__ stand for? Sometimes you see strange
text like this in a DocBlock. For example in PR 27712a . In Joomla, we put __DEPLOY_VERSION__
in place of the version number of a new method we create. Since we don’t know in which version
this new code will be accepted in Joomla, we can’t use a real version number. When the new code
is added to the core, this strange string is automatically replaced with the current version number.
In other systems __BUMP_VERSION__ is common. I use __BUMP_VERSION__ here as well.
a
github.com/joomla/joomla-cms/pull/27712/files
How to create DocBlocks for Joomla is explained in the Joomla coding standards at devel-
oper.joomla.org/ coding-standards/docblocks.html and the pull request github.com/joomla/joomla-
cms/ pull/31504.
A DocBlock is displayed before each class and before each function. All code contains DocBlock
comments, which make it easier for automated tools to generate documentation. In addition
it helps some IDEs to provide code completion. And sometimes the comment is helpful for
programmers. I don’t print the documentary blocks further here. In the code examples on Github,
the DocBlocks are still present.
1 namespace FooNamespace\Component\Foos\Administrator\Controller;
You declare this with the corresponding keyword. Namespaces were introduced in Joomla 4. If this
concept is new to you, read the overview of namespaces at php.net6 . It is important that it is in the file
before any other code.
1 \defined('_JEXEC') or die; `
Next, we import the namespace of the parent class BaseController with the keyword use to be able
to use this class.
1 use Joomla\CMS\MVC\Controller\BaseController;
Then we create the class for the controller. I already wrote that you should call this DisplayController
and extend the class BaseController. Then define the variable $default_view in which you set
the default view with foos. You choose foos as the view because the name of the component is foos
and for this reason you will also created the directory /administrator/components/ com_foos
/src/View/Foos. If nothing is defined, the Foos view with the default layout is used by default.
Setting this variable is not necessary. But I think it is always better to insert this line.
If you take a closer look at the URL while using a component in the administration area, you may notice
the view and layout variables. For example, the URL index.php ?option=com_foos &view=foos
&layout=default loads the foos view with the default layout default. Thus, the file components/
+ com_foos/tmpl/foos/ + default.php is called when you are in the frontend. If you are working in
the backend, administrator/components/ + com_foos/tmpl/foos/ + default.php is used.
The visibility is defined in PHP with public, private or protected. When to use which is
6
php.net/manual/en/language.namespaces.php
Create everything as it is intended in Joomla. This will bring you advantages. For many frequently
used functions, you do not reinvent the wheel. You can see this in practice with the display method.
You do not implement any action in your code. All the work is done by parent::display().
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Administrator\Controller;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\MVC\Controller\BaseController;
9
10 class DisplayController extends BaseController
11 {
12 protected $default_view = 'foos';
13
14 public function display($cachable = false, $urlparams = [])
15 {
16 return parent::display();
17 }
18 }
FoosComponent.php is the code for booting the extension. It is the first file that is called when
Joomla loads the component. Boot’ is the function to set up the environment of the extension, such
as registering new classes. For more information, see the pull request github.com/joomla/joomla-
cms/pull/20217. In the following we will expand the file FoosComponent.php.
administrator/components/com_foos/Extension/FoosComponent.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Administrator\Extension;
5
6 defined('JPATH_PLATFORM') or die;
7
8 use Joomla\CMS\Categories\CategoryServiceInterface;
9 use Joomla\CMS\Categories\CategoryServiceTrait;
10 use Joomla\CMS\Extension\BootableExtensionInterface;
11 use Joomla\CMS\Extension\MVCComponent;
12 use Joomla\CMS\HTML\HTMLRegistryAwareTrait;
13 use FooNamespace\Component\Foos\Administrator\Service\HTML\
AdministratorService;
14 use Psr\Container\ContainerInterface;
15
16 class FoosComponent extends MVCComponent implements
BootableExtensionInterface, CategoryServiceInterface
17 {
18 use CategoryServiceTrait;
19 use HTMLRegistryAwareTrait;
20
21 public function boot(ContainerInterface $container)
22 {
23 $this->getRegistry()->register('foosadministrator', new
AdministratorService);
24 }
25 }
Although we are developing the code for a minimal component, some administrator files are needed.
The file AdministratorService.php will be used later to add functions like multilingualism or
main entries/featured. At the moment we do not need these functions. But we are already preparing
everything here.
administrator/components/com_foos/service/HTML/AdministratorService.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Administrator\Service\HTML;
5
6 defined('JPATH_BASE') or die;
7
8 class AdministratorService
9 {
10 }
In the file HtmlView.php all buttons and titles of the toolbar are defined. The model is called to
prepare the data for the view. At the moment we only call the function of the parent class to display the
default template: parent::display($tpl);. Why do it yourself when there are functions in Joomla
to do it?
When naming a view, it is best to use only a capital letter as the initial letter. I had a problem with
the name of an additional View. I used FOOPlaces. The view was not found under this name.
After I renamed the view folder and namespace to Fooplaces, everything works fine. I found an
explanation of naming conventions on Githuba . According to this page, the folder name for the
template should be written in lower case. It does not say that in addition the view is allowed to
use an uppercase letter only for initial letters. According to a discussionb this is nevertheless the
case.
a
docs.joomla.org/j4.x:file_structure_and_naming_conventions
b
github.com/joomla/joomla-cms/discussions/36679
administrator/components/com_foos/src/View/Foos/HtmlView.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Administrator\View\Foos;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
9
10 class HtmlView extends BaseHtmlView
11 {
12 public function display($tpl = null): void
13 {
14 parent::display($tpl);
15 }
16 }
The file default.php is the template for rendering the view. You can further identify them by the
directory name tmpl. In it is the text that we display. At the moment we are putting all the effort into
the output of the text “Hello Foos”.
administrator/components/com_foos/tmpl/foos/default.php
1
2 <?php
3 \defined('_JEXEC') or die;
4 ?>
5 Hello Foos
I wrote in the preface that the file index.html is not needed. That is correct! Here I only added
it because I am putting together an installation package, but Joomla reports an error during the
installation if there is no folder for the frontend or if an empty directory is passed in the installation
package. At the moment we have no content for the frontend. The temporary insertion of the file is
therefore only a help at this point to avoid error messages during the installation. I create the folder
api for the sake of completeness.
components/com_foos/index.html
1
2 <!DOCTYPE html><title></title>
1. install your component in Joomla version 4 to test it: In the beginning, the easiest thing to
do is to copy the files manually in place. Copy the files in the administrator folder into the
administrator folder of your Joomla 4 installation. Copy the files in the components folder
into the components folder of your Joomla 4 installation.
2. open the menu System | Install | Discover. Here you will see an entry for the compo-
nent you just copied. Select it and click on the button ’Install.
Figure 3.2.: View that allows you to find extensions that were not installed via the normal Joomla
installation
3. if everything works, you will see these displays in front of you after the installation.
4. next test if you get the view for your component without errors.
In previous Joomla versions, the text was output in the backend at the end of the installation,
which is inserted into the installation script with the command echo Text::_('...'). Since
Joomla 4, this no longer happens without further ado. More information is available on Githuba .
a
github.com/joomla/joomla-cms/issues/36343
Up to this point, it wasn’t rocket science. We have a solid basis for the next steps.
After you have a working backend for your component, you implement the frontend. Currently, with
the extension it is possible to display a static text. We don’t have dynamic data yet. This will change
soon. First, however, we build the rough structure.
For impatient people: Look at the changed program code in the diff viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t1...t2
The administration area of our component is located in the folder com_foos under /administrator
/component. Now we work on the frontend. The data of the frontend view are stored in the folder
com_foos directly under /components.
4.1.1.1. components/com_foos/src/Controller/DisplayController.php
components/com_foos/src/Controller/DisplayController.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Site\Controller;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\MVC\Controller\BaseController;
9 use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
10
11 class DisplayController extends BaseController
12 {
13 public function __construct($config = [], MVCFactoryInterface
$factory = null, $app = null, $input = null)
14 {
15 parent::__construct($config, $factory, $app, $input);
16 }
17
18 public function display($cachable = false, $urlparams = [])
19 {
20 parent::display($cachable);
21
22 return $this;
23 }
24 }
4.1.1.2. components/com_foos/src/View/Foo/HtmlView.php
At the moment, the view of our component is simple. Only a static text is displayed. This will change!
There are several files that work together to generate the view in the frontend. For example, the
controller that calls it. We created this earlier in the current chapter. Later on, we will add a special
cell model that prepares the data. At the moment we use the model of the parent classes, because we
build on Joomla standard. The file HtmlView.php calls the inherited model to prepare the data for
the view.
components/com_foos/src/View/Foo/HtmlView.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Site\View\Foo;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
9
10 class HtmlView extends BaseHtmlView
11 {
12 public function display($tpl = null)
13 {
14 return parent::display($tpl);
15 }
16 }
4.1.1.2.1. Logging and debugging Joomla logging offers the possibility to log messages in a file
and on the screen. In the case of the screen, you will find this within the Joomla debug console at the
bottom of the web page when debugging is active. This function may be helpful when developing,
so I mention it here. The entry Log::add('Log me.', Log::DEBUG); causes a line to be added
to the log file. It is important that the necessary functions are loaded in the head of the file via use
Joomla\CMS\Log\Log;. The following images show where the logging and the debugging are set in
the Joomla backend.
We do not use the file here, just because it fits a note: The file libraries/src/Log/
DelegatingPsrLogger.php becomes final in Joomla 5 and cannot be overwritten further. See
PR 39134 a .
a
github.com/joomla/joomla-cms/pull/39134
1
2 <?php
3 \defined('_JEXEC') or die;
4 ?>
5 Hello Foos
4.1.2.1. administrator/components/com_foos/foos.xml
Administrator/components/com_foos/foos.xml’ is the file that tells Joomla how to install our compo-
nent. Therefore, we add the two newly added files here. This way, when installing or updating, Joomla
knows that the directories src and tmpl exist and where to copy them to. The copy destination is the
directory components/com_foos because of folder="components/com_foos".
administrator/components/com_foos/foos.xml
1 <description>COM_FOOS_XML_DESCRIPTION</description>
2 <namespace path="src">FooNamespace\Component\Foos</namespace>
3 <scriptfile>script.php</scriptfile>
4 + <!-- Frond-end files -->
5 + <files folder="components/com_foos">
6 + <folder>src</folder>
7 + <folder>tmpl</folder>
8 + </files>
9 <!-- Back-end files -->
10 <administration>
11 <!-- Menu entries -->
1. at the end, install your component in Joomla version 4 to test it: Perform a new installation. This
is necessary, otherwise the new files will not be recognised in the frontend. To do this, uninstall
your previous installation and copy all files again. Copy the files in the administrator folder
into the administrator folder of your Joomla 4 installation. Copy the files in the components
folder into the components folder of your Joomla 4 installation. Install your component as
described in part one, after you have copied all the files. Joomla will set up namespaces for you
during installation.
Do you care about Search Engine Friendly (SEF) URLsa . Please do not enable this feature yet. This
sample extension does not support SEF yet. We will add the Joomla conform routing later.
a
docs.joomla.org/enabling_search_engine_friendly_(sef)_urls
5. A Menu Item
In this article you will learn how to create a menu item for the frontend view of your component. So
it is not necessary that you know the exact URL. Later a modification to search engine friendly (SEF)
URLs1 is possible. As a note, please do not enable this feature yet. This sample extension does not
support SEF yet. We will add the Joomla conform routing later.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t2...t3
The menu item in the frontend works differently than the one in the administration area. We create a
separate XML file. Later we will use parameters. But for now we keep it straightforward. We add some
language strings for text. Later on, we will see how to translate them.
Create the file default.xml under components/com_foos/tmpl/foo and add the following
code:
components/com_foos/tmpl/foo/default.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <metadata>
4 <layout title="COM_FOOS_FOO_VIEW_DEFAULT_TITLE">
5 <message>
6 <![CDATA[COM_FOOS_FOO_VIEW_DEFAULT_DESC]]>
7 </message>
1
docs.joomla.org/enabling_search_engine_friendly_(sef)_urls/
8 </layout>
9 </metadata>
I already mentioned it in the chapter on the update server: The term CDATAa is used in the XML
markup language for various purposes. It indicates that a given part of the document is general
characters rather than program code with a more specific, limited structure. The CDATA section
may contain markup characters (<, > and &). These are not interpreted further by the parser. The
use of entities such as < and & is not necessary.
a
en.wikipedia.org/wiki/cdata
The title attribute in the layout tag here is used when we create a new menu item for this component
in the administration area. The text in the message tag is displayed as a description. The language
string does not stay as it is. It will be translated into different languages. We will work on this later.
Here we prepare everything.
1. install your component in Joomla version 4 to test it: Copy the files in the components folder
to the components folder of your Joomla 4 installation. A new installation is not necessary.
Continue using the ones from the previous part.
2. open the menu manager to create a menu item. To do this, click on Menu in the left sidebar and
then on All Menu Items.
Then click on the New button and fill in all the necessary fields.
Figure 5.2.: Joomla - Select the type of menu item in the backend
3. find the appropriate Menu Item Type with the Select button.
5. switch to the frontend and make sure that the menu item is created correctly and works.
Figure 5.4.: Joomla - The view of the menu item in the frontend
No new functionality is added in this part. We improve the previous structure. A web application
usually consists of
• Logic,
• data and
• the presentation.
It is problematic to combine these three elements in one class. Especially for larger projects. Joomla
uses the Model-View-Controller-Concept (MVC)1 . In this tutorial part, we add a Model to the frontend.
The Model object is responsible for the data and its processing.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t3...t4
6.1.1.1. components/com_foos/src/Model/FooModel.php
With the model it is also so that you do not reinvent the wheel. You extend the Joomla class
BaseDatabaseModel. Then implement only what you specifically use. In our case it is the output
$this->message = 'Hello Foo!'; for which we create the method getMsg().
The model classes that are available as parent class of Joomla can be found in the directory
libraries/src/MVC/Model/. BaseDatabaseModel is implemented in the file libraries/
src/MVC/Model/BaseDatabaseModel.php.
components/com_foos/src/Model/FooModel.php
1
en.wikipedia.org/wiki/model_view_controller
1
2 <?php
3
4
5 namespace FooNamespace\Component\Foos\Site\Model;
6
7 \defined('_JEXEC') or die;
8
9 use Joomla\CMS\MVC\Model\BaseDatabaseModel;
10
11 class FooModel extends BaseDatabaseModel
12 {
13 protected $message;
14
15 public function getMsg()
16 {
17 if (!isset($this->message)) {
18 $this->message = 'Hello Foo!';
19 }
20
21 return $this->message;
22 }
23 }
6.1.2.1. components/com_foos/src/View/Foo/HtmlView.php
We get the data of the model in the view with $this->msg = $this->get('Msg');. This seems
complicated in this simple example. In complex applications, this procedure has proven itself. The
data calculation is done in the model. The view handles the design of the data.
components/com_foos/src/View/Foo/HtmlView.php
You may be confused by the call $this->get('Msg'); as I was when I first started using Joomla.
The method in the model is called getMsg(), but we call it here via get('Msg'). Somehow that
doesn’t fit. If you have dealt with object oriented programming before, you are tempted to call it
via getMsg(). If you are using Joomla, you will have an easier time using things the way they are
prepared. You call Gettera in the model via the method get() with the appropriate parameter.
a
en.wikipedia.org/wiki/mutator_method
We output the data via the template. Here, everything will be packed into HTML tags later.
components/com_foos/tmpl/foo/default.php
1 \defined('_JEXEC') or die;
2 ?>
3 -Hello Foos
4 +
5 +Hello Foos: <?php echo $this->msg;
1. install your component in Joomla version 4 to test it: Copy the files in the administrator folder
into the administrator folder of your Joomla 4 installation. Copy the files in the components
folder into the components folder of your Joomla 4 installation. A new installation is not
necessary. Continue using the files from the previous part.
2. look at the frontend view of your component. Make sure that the data for the output is generated
by the model. The text output now is Hello Foos: Hello Foo! instead of Hello Foos if
you followed my example.
Sometimes you need to customize the frontend output for a menu item. For this you need a variable.
In this part of the tutorial we will add a text variable to the menu item and use it for the display in the
frontend.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t4...t5
7.1.2.1. components/com_foos/src/Model/FooModel.php
In the model, change the method in which text is calculated for output. Delete the following entry:
components/com_foos/src/Model/FooModel.php
1 ...
2 if (!isset($this->message))
3 {
4 $this->message = 'Hello Foo!';
5 }
6 ...
1 ...
2 $app = Factory::getApplication();
3 $this->message = $app->input->get('show_text', "Hi");
4 ...
You can do without the check if (!isset($this->message)) in the new variant because
the statement get('show_text', "Hi"); catches the error that occurs when the parameter
show_text is not set. Whenever the value show_text is not set, the second parameter "Hi" is
used as default.
components/com_foos/src/Model/FooModel.php
1 \defined('_JEXEC') or die;
2
3 +use Joomla\CMS\Factory;
4 use Joomla\CMS\MVC\Model\BaseDatabaseModel;
5
6
7 public function getMsg()
8 {
9 - if (!isset($this->message))
10 - {
11 - $this->message = 'Hello Foo!';
12 - }
13 + $app = Factory::getApplication();
14 + $this->message = $app->input->get('show_text', "Hi");
15
16 return $this->message;
17 }
7.1.2.1.1. Side note: How to handle request variables in Joomla The function $app->input
->get('show_text', "Hi") is a help. It is impemented via the Input class in the libraries
/vendor/joomla/input/src/Input.php file and works together with InputFilter in the
libraries/vendor/joomla/filter/src/InputFilter.php. file.
Extension development is about processing user input. The parameter added here is entered by a user
through a form and then stored in the database table. To ensure that the value of the parameter is
correct, i.e. does not contain malicious code or syntactical errors, it is necessary to filter the value. This
is where the Input class comes into play. Those already familiar with PHP may work directly with raw
request variables such as $_POST and $_GET. These work fine in Joomla. However, it is easier and
possibly safer to let the Input class do the work.
If you browse the code of Joomla, you will find many examples that show the basic uses of the Input
class. For example, $app->input->get('show_text', "Hi") is checked for a string, because it is
a string. To return the parameter without filtering, $app->input->get('show_text', "Hi", '
RAW') would be the appropriate command.
Possible data types for filtering are: - INT: An integer - UINT: An unsigned integer - FLOAT: A floating point
number - BOOLEAN: A boolean value - WORD: A string containing A-Z or underscores only (not case
sensitive) - ALNUM: A string containing A-Z or 0-9 only (not case sensitive) - CMD: A string containing A-Z,
0-9, underscores, periods or hyphens (not case sensitive) - BASE64: A string containing A-Z, 0-9, forward
slashes, plus or equals (not case sensitive) - STRING: A fully decoded and sanitised string (default) -
HTML: A sanitised string - ARRAY: An array - PATH: A sanitised file path - TRIM: A string trimmed from
normal, non-breaking and multibyte spaces - USERNAME: Do not use (use an application specific filter)
- RAW: The raw string is returned with no filtering - unknown: An unknown filter will act like STRING. If
the input is an array it will return an array of fully decoded and sanitised strings.
So far, so good. We are still missing the possibility to configure the value for show_text at the menu
item in the backend. We implement this next in the file default.xml.
In your extension you offer the possibility to save a value at the menu item by extending the XML file
with an input element. The following code shows you how to add a text input field.
components/com_foos/tmpl/foo/default.xml
1 <![CDATA[COM_FOOS_FOO_VIEW_DEFAULT_DESC]]>
2 </message>
3 </layout>
4 + <!-- Add fields to the request variables for the layout. -->
5 + <fields name="request">
6 + <fieldset name="request">
7 + <field
8 + name="show_text"
9 + type="text"
10 + label="COM_FOOS_FIELD_TEXT_SHOW_LABEL"
11 + default="Hi"
12 + />
13 + </fieldset>
14 + </fields>
15 </metadata>
1 <field
2 name="show_text"
3 type="text"
4 label="COM_FOOS_FIELD_TEXT_SHOW_LABEL"
5 default="Hi"
6 />
turns Joomla into the following HTML code for the output in the backend form:
1 <div class="control-label">
2 <label id="jform_request_show_text-lbl" for="jform_request_show_text"
>
3 COM_FOOS_FIELD_TEXT_SHOW_LABEL</label
4 >
5 </div>
6 <div class="controls has-success">
7 <input
8 type="text"
9 name="jform[request][show_text]"
10 id="jform_request_show_text"
11 value="Hi"
12 class="form-control valid form-control-success"
13 aria-invalid="false"
14 />
15 </div>
With type="text" we use one of the simplest form fields, that is the one of type text. Various
types of form fields are built into Joomla. The Joomla documentation lists these standard types.
Take a look at the table on the website docs.joomla.org/Form_field/en. Often you can implement
a special requirement with a special field.
1. install your component in Joomla version 4 to test it: Copy the files in the components folder
to the components folder of your Joomla 4 installation. A new installation is not necessary.
Continue using the ones from the previous part.
2. switch to the Menu Manager and open a created menu item or create a new one. Here you will
now see a text field where you can insert any text.
3. now switch to the frontend view. Make sure that the text you entered for the menu item is
displayed in the correct variant in the frontend.
I’m sure you can think of funnier or more useful examples. But the sense and function of the variables
will be clear.
4. Create multiple menu items, each containing different text. Don’t just output the text on the
frontend, style the output using conditional statements1 . A popular use case is to change the
design of the output using variables. For example, you use the variable to query whether the
content is to be output in a list or in a table.
1
developer.mozilla.org/en/docs/web/javascript/reference/statements/if...else
Your view in the administration area usually does not contain only static text. You display data here
that is dynamic. At least that’s how most extensions work. That’s why in this part we create a database
for your component. In the database, we store three records during setup and display them in the
administration area. A static list is displayed. The single entries are not changeable via the backend.
We will work on that in the next part.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t5...t6
We create a file that contains SQL statements for creating the database table. So that these statements
are called, we add the name of the file later in the manifest.
Besides the id, which we use to make the element uniquely findable and the name name, which is
optional and names the item in our extension, there is the alias alias. The latter prepares the data
for routing, among other things. Imagine a system URL like http://www.example.com/index.php
?option=com_foos&view=foo&id=1. This is not very readable for humans. Machines like search
engines also process such a URL poorly. A textual description is mandatory. In Joomla this is done
with the help of the alias. This can be defined by the user. So that the alias text for the URL contains
only valid characters, there are automatic processes in the background processing of Joomla.
With CREATE TABLE IF NOT EXISTS ... we create the database table if it does not already exist.
With INSERT INTO ... we store sample contents in the database table. In a real extension, I would
not add sample data via the SQL file during installation. In Joomla 4, a plugin of the type sampledata
is a good choice. For inspiration you can find plugins in Joomla in the directory plugins/sampledata
.
administrator/components/com_foos/sql/install.mysql.utf8.sql
1
2 CREATE TABLE IF NOT EXISTS `#__foos_details ` (
3 `id ` int(11) NOT NULL AUTO_INCREMENT,
4 `alias ` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT
NULL DEFAULT '',
5 `name ` varchar(255) NOT NULL DEFAULT '',
6 PRIMARY KEY ( `id`)
7 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=
utf8mb4_unicode_ci;
8
9 INSERT INTO `#__foos_details ` ( `name`) VALUES
10 ('Nina'),
11 ('Astrid'),
12 ('Elmar');
Read in the preface of this tutorial what exactly the prefix #__ means, if you are unfamiliar with it.
So that Joomla does not contain unnecessary data in case of uninstallation, we simultaneously create
a file that contains the SQL command to delete the database table. This automatically executed when
uninstalling.
administrator/components/com_foos/sql/uninstall.mysql.utf8.sql
1
2 DROP TABLE IF EXISTS `#__foos_details`;
You might think ahead and ask yourself already how to handle potential future database changes.
What is needed to store the first name in addition to the name in a future version. SQL updates are
name-based in Joomla. This means exactly: For each version of the component you have to create
a file whose name consists of the version number and the file extension .sql in case database
contents change. Practically you will experience this in the further course of this tutorial.
Next, we create a Model for the administration area. Since we are extending the ListModel class, we
do not need to take care of the connection to the database ourselves. We create the getListQuery()
method and specify our specific requirements here. Specific are for example the name of the database
table and the column.
If not done so far, you will see here why the separation of model and view makes sense. Have a
look at the method getListQuery() in Joomla components, for example in com_content. The
SQL statement is usually extensive. Therefore it is clearer to encapsulate this from other parts.
The following code shows you the model, which in our case is still quite clear.
administrator/components/com_foos/src/Model/FoosModel.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Administrator\Model;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\MVC\Model\ListModel;
9
10 class FoosModel extends ListModel
11 {
12 public function __construct($config = [])
13 {
14 parent::__construct($config);
15 }
16 protected function getListQuery()
17 {
18 // Create a new query object.
19 $db = $this->getDbo();
20 $query = $db->getQuery(true);
21
22 // Select the required fields from the table.
23 $query->select(
24 $db->quoteName(['id', 'name', 'alias'])
25 );
26 $query->from($db->quoteName('#__foos_details'));
27
28 return $query;
29 }
30 }
8.1.2.1. administrator/components/com_foos/foos.xml
The entry in the installation manifest marked with a plus sign causes the SQL statements in the named
files to be called at the right moment, either during an installation or during an uninstallation..
administrator/components/com_foos/foos.xml
1 <description>COM_FOOS_XML_DESCRIPTION</description>
2 <namespace path="src">FooNamespace\Component\Foos</namespace>
3 <scriptfile>script.php</scriptfile>
4 + <install> <!-- Runs on install -->
5 + <sql>
6 + <file driver="mysql" charset="utf8">sql/install.mysql.utf8.
sql</file>
7 + </sql>
8 + </install>
9 + <uninstall> <!-- Runs on uninstall -->
10 + <sql>
11 + <file driver="mysql" charset="utf8">sql/uninstall.mysql.
utf8.sql</file>
12 + </sql>
13 + </uninstall>
14 <!-- Frond-end files -->
15 <files folder="components/com_foos">
16 <folder>src</folder>
17
18 <files folder="administrator/components/com_foos">
19 <filename>foos.xml</filename>
20 <folder>services</folder>
21 + <folder>sql</folder>
22 <folder>src</folder>
23 <folder>tmpl</folder>
24 </files>
In this example, I only support a MySQL database. Joomla supportsa as well as MySQL (from 5.6)
and PostgreSQL (from 11). If you also support both databases, you can find an implementation to
check out in the Weblinks componentb . How you name the drivers is flexible. postgresql and
mysql are correct. mysqli, pdomysql and pgsql are changed by Joomla in the file /libraries
/src/ Installer/Installer.php if you use this.
a
downloads.joomla.org/technical-requirements
b
github.com/joomla-extensions/weblinks
8.1.2.1.1. Updates For the sake of completeness, I anticipate here changes of a following chapter
concerning updating. If something changes, it is sufficient to include only the changes in the database.
You should take care that existing data are not affected. You save the changes in a separate file for
each version. The directory, where the files for the future updates are to be stored, you write in the
<update> tag. This is logical, right?
1 ...
2 <update> <!-- Runs on update -->
3 <schemas>
4 <schemapath type="mysql">sql/updates/mysql</schemapath>
5 </schemas>
6 </update>
7 ...
Previously it was not necessary to set the MVC factory in provider.php, now it is required. Other-
wise you will see the following error message or you will be forced to program the connection to the
database yourself: MVC factory not set in Joomla\CMS\Extension\MVCComponent.
administrator/components/com_foos/services/provider.php
1 use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
2 use Joomla\CMS\Extension\Service\Provider\MVCFactory;
3 use Joomla\CMS\HTML\Registry;
4 +use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
5 use Joomla\DI\Container;
6 use Joomla\DI\ServiceProviderInterface;
7 use FooNamespace\Component\Foos\Administrator\Extension\FoosComponent;
8
9 $component = new FoosComponent($container->get(
ComponentDispatcherFactoryInterface::class));
10
11 $component->setRegistry($container->get(Registry::class
));
12 + $component->setMVCFactory($container->get(
MVCFactoryInterface::class));
13
14 return $component;
15 }
In the view we get all the items at the end. For this we call the method $this->get('Items') in the
model:
administrator/components/com_foos/src/View/Foos/HtmlView.php
2 {
3 + protected $items;
4 +
5
6 public function display($tpl = null): void
7 {
8 + $this->items = $this->get('Items');
9 parent::display($tpl);
10 }
11 }
Last but not least, we display everything using the template file. Instead of the static text Hello Foos
there is now a loop that goes through all elements.
administrator/components/com_foos/tmpl/foos/default.php
1 \defined('_JEXEC') or die;
2 ?>
3 -Hello Foos
4 +<?php foreach ($this->items as $i => $item) : ?>
5 +<?php echo $item->name; ?>
6 +</br>
7 +<?php endforeach; ?>
Are you wondering about the syntax? In the preface I had explained why I choose the alternative
syntax for PHP in a template file and enclose the individual lines in PHP tags.
1. install your component in Joomla version 4 to test it: Copy the files in the administrator
folder to the administrator folder of your Joomla 4 installation. Install your component as
described in part one, after copying all files. Joomla creates the database during the installation.
2. Next, test if the view of your component in the administration area is correct. Do you see three
entries? We had entered these as sample data in the SQL file when setting up the database.
3. make sure that the elements are stored in the database. I use locally phpmyadmin.net for
database administration.
In the previous part we set up a database for the Joomla components. In this part you will learn how to
change or add data using a form in the administration area. At the end, the view of your component in
the administration area contains a button to add new items. You change an existing item by clicking on
the title in the list view.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t6...t6b
Joomla creates the form for you if you give it the requirements in an XML file. Below you can see this
for our example.
administrator/components/com_foos/forms/foo.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <form>
4 <fieldset>
5 <field
6 name="id"
7 type="number"
8 label="JGLOBAL_FIELD_ID_LABEL"
9 default="0"
10 class="readonly"
11 readonly="true"
12 />
13
14 <field
15 name="name"
16 type="text"
17 label="COM_FOOS_FIELD_NAME_LABEL"
18 size="40"
19 required="true"
20 />
21
22 <field
23 name="alias"
24 type="text"
25 label="JFIELD_ALIAS_LABEL"
26 size="45"
27 hint="JFIELD_ALIAS_PLACEHOLDER"
28 />
29 </fieldset>
30 </form>
You want an overview of all possible form elements? In the Joomla documentationa all standard
form fields are listed.
a
docs.joomla.org/form_field
Further tip: We have a simple form so far. Later, more specific requirements will surely be added.
For example: What is the best way to place JavaScript in a Joomla form? A quick and simple but
messy solution is this: You create a field type=note in the XML definition and then write the
JavaScript code into the language constant of the description. I found a more elegant solution
in Allrounder template by Bakual a . First he creates a new field of type loadjscssb . He then
includes this in the file templateDetails.xmlc . Don’t worry if you don’t see through the last
variant right away. We will create more fields as we go along.
a
github.com/Bakual/Allrounder
b
github.com/Bakual/Allrounder/blob/master/fields/loadjscss.php
c
github.com/Bakual/Allrounder/blob/master/templateDetails.xml#L51
We create more or less an empty class with FooController. Although it contains no logic of its
own, we need it because it inherits from FormController. Joomla expects FooController as the
controller of the extension in this place under this name.
administrator/components/com_foos/src/Controller/FooController.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Administrator\Controller;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\MVC\Controller\FormController;
9
10 class FooController extends FormController
11 {
12 }
Now we create the model to fetch the data for an element from the database. This we call FooModel.
It inherits the main implementations from AdminModel. We add our own special requirements. With
$typeAlias we set the typalias for the content type. This way Joomla knows for all inherited functions
to which element it has to apply them exactly. For example, the alias in loadFormData() is used
to convert the matching XML file into a form. Remember, you created the file in the current chapter.
And for the correct mapping of the table, the alias is essential when you reuse Joomla functions. The
typalias plays a big role in the background without you noticing it.
administrator/components/com_foos/src/Model/FooModel.php
1
2 <?php
1
libraries/src/MVC/Controller/FormController.php
3
4 namespace FooNamespace\Component\Foos\Administrator\Model;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\Factory;
9 use Joomla\CMS\MVC\Model\AdminModel;
10
11 class FooModel extends AdminModel
12 {
13 public $typeAlias = 'com_foos.foo';
14
15 public function getForm($data = [], $loadData = true)
16 {
17 // Get the form.
18 $form = $this->loadForm($this->typeAlias, 'foo', ['control' =>
'jform', 'load_data' => $loadData]);
19
20 if (empty($form)) {
21 return false;
22 }
23
24 return $form;
25 }
26
27 protected function loadFormData()
28 {
29 $app = Factory::getApplication();
30
31 $data = $this->getItem();
32
33 $this->preprocessData($this->typeAlias, $data);
34
35 return $data;
36 }
37
38 protected function prepareTable($table)
39 {
40 $table->generateAlias();
41 }
42 }
We implement the access to the database table. It is important to set $this->typeAlias and to
specify the name of the table #__foos_details.
administrator/components/com_foos/src/Table/FooTable.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Administrator\Table;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\Application\ApplicationHelper;
9 use Joomla\CMS\Table\Table;
10 use Joomla\Database\DatabaseDriver;
11
12 class FooTable extends Table
13 {
14 public function __construct(DatabaseDriver $db)
15 {
16 $this->typeAlias = 'com_foos.foo';
17
18 parent::__construct('#__foos_details', 'id', $db);
19 }
20
21 public function generateAlias()
22 {
23 if (empty($this->alias)) {
24 $this->alias = $this->name;
25 }
26
27 $this->alias = ApplicationHelper::stringURLSafe($this->alias,
$this->language);
28
29 if (trim(str_replace('-', '', $this->alias)) == '') {
30 $this->alias = Factory::getDate()->format('Y-m-d-H-i-s');
31 }
32
33 return $this->alias;
34 }
35 }
administrator/components/com_foos/src/View/Foo/HtmlView.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Administrator\View\Foo;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\Factory;
9 use Joomla\CMS\Language\Text;
10 use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
11 use Joomla\CMS\Toolbar\ToolbarHelper;
12
13 class HtmlView extends BaseHtmlView
14 {
15 protected $form;
16
17 protected $item;
18
19 public function display($tpl = null)
20 {
21 $this->form = $this->get('Form');
22 $this->item = $this->get('Item');
23
24 $this->addToolbar();
25
26 return parent::display($tpl);
27 }
28
29 protected function addToolbar()
30 {
31 Factory::getApplication()->input->set('hidemainmenu', true);
32
33 $isNew = ($this->item->id == 0);
34
35 ToolbarHelper::title($isNew ? Text::_('COM_FOOS_MANAGER_FOO_NEW
') : Text::_('COM_FOOS_MANAGER_FOO_EDIT'), 'address foo');
36
37 ToolbarHelper::apply('foo.apply');
38 ToolbarHelper::cancel('foo.cancel', 'JTOOLBAR_CLOSE');
39 }
40 }
In the file edit.php is the view implemented, which is called for editing. It is important for me to
address the Webassetmanager2 $wa = $this->document->getWebAssetManager(); here. This
2
docs.joomla.org/j4.x:web_assets
is new in Joomla 4. You load two JavaScript files via Webassetmanager. useScript('keepalive')
loads media/system/js/keepalive.js and keeps your session alive while you edit or create an
article. useScript('form.validate') loads a lot of helpful functions with media/system/js/
core.js. For example, validation, which we’ll look at in more detail later.
If you do not include webassets correctly, you will get the following error in the console of
your browser when you save the form: joomla document.formvalidator is undefined.
Joomla validates the forms by default and expects the file media/system/js/core.js to be
loaded.
administrator/components/com_foos/tmpl/foo/edit.php
1
2 <?php
3
4 \defined('_JEXEC') or die;
5
6 use Joomla\CMS\Factory;
7 use Joomla\CMS\HTML\HTMLHelper;
8 use Joomla\CMS\Router\Route;
9
10 $app = Factory::getApplication();
11 $input = $app->input;
12
13 $wa = $this->document->getWebAssetManager();
14 $wa->useScript('keepalive')
15 ->useScript('form.validate');
16
17 $layout = 'edit';
18 $tmpl = $input->get('tmpl', '', 'cmd') === 'component' ? '&tmpl=
component' : '';
19 ?>
20
21 <form action="<?php echo Route::_('index.php?option=com_foos&layout=' .
$layout . $tmpl . '&id=' . (int) $this->item->id); ?>" method="post
" name="adminForm" id="foo-form" class="form-validate">
22 <?php echo $this->getForm()->renderField('name'); ?>
23 <?php echo $this->getForm()->renderField('alias'); ?>
24 <input type="hidden" name="task" value="">
25 <?php echo HTMLHelper::_('form.token'); ?>
26 </form>
Are you interested in the content of the files Core.jsa or Keepalive.jsb ? In this case, look at
them directly in Joomla. In the development version, they are located in the directory build/
media_source/system/js/ and are prepared for installation with the help of scripts, Node.jsc
and Composerd in the directory media/system/js. For further information, please refer to the
Joomla Documentatione .
a
build/media_source/system/js/core.es6.js
b
build/media_source/system/js/keepalive.es6.js
c
nodejs.org
d
getcomposer.org/
e
docs.joomla.org/j4.x:setting_up_your_local_environment
9.1.1.7. administrator/components/com_foos/tmpl/foos/emptystate.php
administrator/components/com_foos/tmpl/foos/emptystate.php
1
2 <?php
3 \defined('_JEXEC') or die;
4
5 use Joomla\CMS\Factory;
6 use Joomla\CMS\Layout\LayoutHelper;
7
8 $displayData = [
9 'textPrefix' => 'COM_FOOS',
10 'formURL' => 'index.php?option=com_foos',
11 'helpURL' => 'https://codeberg.org/astrid/j4examplecode/src/branch/
main/README.md',
12 'icon' => 'icon-copy',
13 ];
14
15 $user = Factory::getApplication()->getIdentity();
16
17 if ($user->authorise('core.create', 'com_foos') || count($user->
getAuthorisedCategories('com_foos', 'core.create')) > 0) {
18 $displayData['createURL'] = 'index.php?option=com_foos&task=foo.add
';
19 }
20
21 echo LayoutHelper::render('joomla.content.emptystate', $displayData);
'icon'=> 'icon-copy' only works with icons that are included by name in the file build/
media_source/system/scss/_icomoon.scssa . I explained in the preface why this is like it
is. Adjust the layout for the icon if you want to display a different symbol.
a
build/media_source/system/scss/_icomoon.scss
The Empty State layout has been integrated into Joomla in PR 332643 . The implementation
3
github.com/joomla/joomla-cms/pull/33264
of the Empty-State-Layout here in the tutorial takes the hint from Issue 35712 into account
and inserts the code if (count($errors = $this->get('Errors'))){ throw new
GenericDataException(implode("\n", $errors), 500);} before the code if (!count(
$this->items)&& $this->get('IsEmptyState')){ $this->setLayout('emptystate')
;} in the file administrator/components/com_foos/src/View/Foos/HtmlView.php. This is
done in a later chapter.
Good design is already a challenge when there is data to display. It is even more difficult to
implement an empty page in a user-friendly way. Have a look at emptystat.es if you want to get
inspired about your Empty State implementation.
9.1.2.1. administrator/components/com_foos/foos.xml
To ensure that the “forms” directory is passed to Joomla during a new installation, enter it in the
installation manifest.
administrator/components/com_foos/foos.xml
1 </submenu>
2 <files folder="administrator/components/com_foos">
3 <filename>foos.xml</filename>
4 + <folder>forms</folder>
5 <folder>services</folder>
6 <folder>sql</folder>
7 <folder>src</folder>
In the view that displays the overview list, we add the toolbar. Here we insert a button that creates a new
element. We also query with if (!count($this->items)&& $this->get('IsEmptyState'))
whether there are items to display. If the view is empty, we display the user-friendly Empty State layout
$this->setLayout('emptystate');.
administrator/components/com_foos/src/View/Foos/HtmlView.php
1
2 \defined('_JEXEC') or die;
3
4 +use Joomla\CMS\Language\Text;
5 use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
6 +use Joomla\CMS\Toolbar\Toolbar;
7 +use Joomla\CMS\Toolbar\ToolbarHelper;
8
9 class HtmlView extends BaseHtmlView
10 public function display($tpl = null): void
11 {
12 $this->items = $this->get('Items');
13 +
14 + if (!count($this->items) && $this->get('IsEmptyState')) {
15 + $this->setLayout('emptystate');
16 + }
17 +
18 + $this->addToolbar();
19 +
20 parent::display($tpl);
21 }
22 +
23 + protected function addToolbar()
24 + {
25 + // Get the toolbar object instance
26 + $toolbar = Toolbar::getInstance('toolbar');
27 +
28 + ToolbarHelper::title(Text::_('COM_FOOS_MANAGER_FOOS'), 'address
foo');
29 +
30 + $toolbar->addNew('foo.add');
31 + }
32 }
In the template of the overview list, we replace the simple text with a form. The form contains a form
field for each column in the database table and makes it possible to create or change data.
administrator/components/com_foos/tmpl/foos/default.php
1 \defined('_JEXEC') or die;
2 +
3 +use Joomla\CMS\HTML\HTMLHelper;
4 +use Joomla\CMS\Language\Text;
5 +use Joomla\CMS\Router\Route;
6 ?>
7 -<?php foreach ($this->items as $i => $item) : ?>
8 - <?php echo $item->name; ?>
9 -</br>
10 -<?php endforeach; ?>
11 +<form action="<?php echo Route::_('index.php?option=com_foos'); ?>"
method="post" name="adminForm" id="adminForm">
12 + <div class="row">
13 + <div class="col-md-12">
1. install your component in Joomla version 4 to test it: Copy the files in the administrator folder
into the administrator folder of your Joomla 4 installation. Copy the files in the components
folder into the components folder of your Joomla 4 installation. A new installation is not
necessary. Continue using the files from the previous part.
2. next, open the list view of your component in the administration area. Are the three items
provided with links? Do you see a button to create a new item?
3. then click on the button New or on the title of an item. You will see the form for creating or editing
items. Add a new item.
5. delete all Foo-Items via the database and make sure that the Empty-State layout is displayed.
Have you not yet edited the database yourself? In the previous section I suggested phpmyad-
min.net as a tool. In the following you will see the standard view followed by our user-friendly
Empty State version for comparison. In the next but one section we will take care of the language
files, then the layout will be more friendly. Later, the button for deleting items is also added.
Figure 9.4.: Edit Joomla Component in Backend - Empty View without Empty State Layout
Figure 9.5.: Edit Joomla Component in Backend - Empty View with Empty State Layout
We have a database where the data about the component is stored. The next step is to display the
dynamic content in the frontend. In this part, I’ll show you how to output the content for an element
via menu item. For this we will create our own form field.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t6b...t7
First, we create the form field through which it is possible to select or deselect a Foo element. In this
case, we cannot access a ready-made field. Basically, we implement the methods getInput and
getLabel and we set the type to Modal_Foo. It is not mandatory that the name of the class starts
with the word “Field” and that the class is stored in the directory “Field”. However, it can be helpful
because it is standard in Joomla’s own extension.
It is possible to extend the field so that a Foo element is created via a button. I have left this out so
far for the sake of simplicity. Sample code is provided by the component com_contact in the file
administrator/components/com_contact/ src/Field/Modal/ContactField.php.
administrator/components/com_foos/src/Field/Modal/FooField.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Administrator\Field\Modal;
5
6 \defined('JPATH_BASE') or die;
7
8 use Joomla\CMS\Factory;
9 use Joomla\CMS\Form\FormField;
10 use Joomla\CMS\HTML\HTMLHelper;
11 use Joomla\CMS\Language\Text;
12 use Joomla\CMS\Session\Session;
13
14 class FooField extends FormField
15 {
16 protected $type = 'Modal_Foo';
17
18 protected function getInput()
19 {
20 $allowClear = ((string) $this->element['clear'] != 'false');
21 $allowSelect = ((string) $this->element['select'] != 'false');
22
23 // The active foo id field.
24 $value = (int) $this->value > 0 ? (int) $this->value : '';
25
26 // Create the modal id.
27 $modalId = 'Foo_' . $this->id;
28
29 // Add the modal field script to the document head.
30 $wa = Factory::getApplication()->getDocument()->
getWebAssetManager();
31
32 // Add the modal field script to the document head.
33 $wa->useScript('field.modal-fields');
34
35 // Script to proxy the select modal function to the modal-
fields.js file.
36 if ($allowSelect) {
37 static $scriptSelect = null;
38
39 if (is_null($scriptSelect)) {
40 $scriptSelect = [];
41 }
42
43 if (!isset($scriptSelect[$this->id])) {
44 $wa->addInlineScript("
45 window.jSelectFoo_" . $this->id . " = function (id,
title, object) {
46 window.processModalSelect('Foo', '" . $this->id . "
', id, title, '', object);
47 }",
48 [],
49 ['type' => 'module']
50 );
51
52 $scriptSelect[$this->id] = true;
53 }
54 }
55
56 // Setup variables for display.
57 $linkFoos = 'index.php?option=com_foos&view=foos&layout
=modal&tmpl=component&'
58 . Session::getFormToken() . '=1';
59 $linkFoo = 'index.php?option=com_foos&view=foo&layout=
modal&tmpl=component&'
60 . Session::getFormToken() . '=1';
61 $modalTitle = Text::_('COM_FOOS_CHANGE_FOO');
62
63 $urlSelect = $linkFoos . '&function=jSelectFoo_' . $this->
id;
64
65 if ($value) {
66 $db = Factory::getDbo();
67 $query = $db->getQuery(true)
68 ->select($db->quoteName('name'))
69 ->from($db->quoteName('#__foos_details'))
70 ->where($db->quoteName('id') . ' = ' . (int) $value);
71 $db->setQuery($query);
72
73 try {
74 $title = $db->loadResult();
75 } catch (\RuntimeException $e) {
76 Factory::getApplication()->enqueueMessage($e->
getMessage(), 'error');
77 }
78 }
79
80 $title = empty($title) ? Text::_('COM_FOOS_SELECT_A_FOO') :
htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
81
82 // The current foo display field.
83 $html = '';
84
85 if ($allowSelect || $allowNew || $allowEdit || $allowClear) {
86 $html .= '<span class="input-group">';
87 }
88
89 $html .= '<input class="form-control" id="' . $this->id . '
_name" type="text" value="' . $title . '" readonly size
="35">';
90
91 // Select foo button
92 if ($allowSelect) {
93 $html .= '<button'
94 . ' class="btn btn-primary hasTooltip' . ($value ? '
hidden' : '') . '"'
95 . ' id="' . $this->id . '_select"'
96 . ' data-bs-toggle="modal"'
97 . ' type="button"'
The programme code for the form field is adapted to Bootstrap 5a . This framework was integrated
into Joomla in the pull request 32037b .
a
getbootstrap.com
b
github.com/joomla/joomla-cms/pull/32037
Make sure that you use the correct names. If nothing happens later during testing when you select a
single Foo, it is usually due to a typing error. Background: JavaScript is being executed. You add this
script in two places. First, you create the function jSelectFoo_... using variables.
1 ...
2 if (!isset($scriptSelect[$this->id])) {
3 $wa->addInlineScript("
4 window.jSelectFoo_" . $this->id . " = function (id, title, object)
{
5 window.processModalSelect('Foo', '" . $this->id . "', id, title
, '', object);
6 }",
7 [],
8 ['type' => 'module']
9 );
10
11 $scriptSelect[$this->id] = true;
12 }
13
14 ...
In the source code of the Joomla administration area, the following code is added:
1 <script nonce="sampleId=">
1 ...
2 $urlSelect = $linkFoos . '&function=jSelectFoo_' . $this->id;
3 ...
In an early sample code version for the field “FooField” we do not use the Webasset Manager. The
necessary changes can be found herea .
a
github.com/joomla/joomla-cms/commit/04f844ad4a6d0432ec4b770bbb2a33243ded16d9
We open the selection in a modal window via the FooField. As address we have inserted in the
field $linkFoos = 'index.php?option=com_foos&view=foos&layout=modal
&tmpl=component&'. The following code shows you the template for this modal
window.
administrator/components/com_foos/tmpl/foos/modal.php
1
2 <?php
3
4 \defined('_JEXEC') or die;
5
6 use Joomla\CMS\Factory;
7 use Joomla\CMS\HTML\HTMLHelper;
8 use Joomla\CMS\Language\Text;
9 use Joomla\CMS\Router\Route;
10 use Joomla\CMS\Session\Session;
11
12 $app = Factory::getApplication();
13
14 $wa = $this->document->getWebAssetManager();
15 $wa->useScript('com_foos.admin-foos-modal');
16
17 $function = $app->input->getCmd('function', 'jSelectFoos');
18 $onclick = $this->escape($function);
19 ?>
20 <div class="container-popup">
21
64
65 <input type="hidden" name="task" value="">
66 <input type="hidden" name="forcedLanguage" value="<?php echo
$app->input->get('forcedLanguage', '', 'CMD'); ?>">
67 <?php echo HTMLHelper::_('form.token'); ?>
68
69 </form>
70 </div>
A Modala is an area that opens in the foreground of a web page and changes its state. It is required
to actively close it. Modal dialogs lock the rest of the application as long as the dialog is displayed.
A modal is also called a dialog or lightbox.
a
en.wikipedia.org/wiki/dialog_box
10.1.1.3. media/com_foos/joomla.asset.json
We use the WebAssetManager. This time we add our own webasset using the file joomla.asset
.json. If you don’t include it correctly, you will get the following error when you select a foo item
for the menu item: There is no "com_foos.admin-foos-modal"asset of a "script"type
in the registry.. Reason: In the modal, the line $wa->useScript('com_foos.admin-foos
-modal'); calls the script com_foos.admin-foos-modal, which, however, was not registered cor-
rectly before. Therefore it is not found.
Because of the newly added file joomla.asset.json it is necessary that we reinstall the ex-
tension. We have used other files so far without a new installation in Joomla. This does not
work with the file joomla.asset.json. This file has to be registered once during an installation.
Furthermore, changes can be made in it. These are recognised without a new installation.
It is not mandatory to create the file joomla.asset.json if you want to use the WebAsset-
Managera . In the documentation you will find possibilities to register webassets in the code
afterwards.
a
docs.joomla.org/j4.x:web_assets
media/com_foos/joomla.asset.json
1 /* https://codeberg.org/astrid/j4examplecode/raw/branch/t7/src/media/
com_foos/joomla.asset.json */
2
3 {
4 "$schema": "https://developer.joomla.org/schemas/json-schema/
web_assets.json",
5 "name": "com_foos",
6 "version": "1.0.0",
7 "description": "Joomla CMS",
8 "license": "GPL-2.0-or-later",
9 "assets": [
10 {
11 "name": "com_foos.admin-foos-modal",
12 "type": "script",
13 "uri": "com_foos/admin-foos-modal.js",
14 "dependencies": ["core"],
15 "attributes": {
16 "defer": true
17 }
18 }
19 ]
20 }
For the media version the Web Asset Manager sets the default auto. This means that JHtml::_
('script', 'com_example/example.js', array('version'=> 'auto')); is called by de-
fault. What does this mean exactly? The media version is used to control the new loading of CSS and
JavaScript files. Specifically, the media version is reset during an update, installation, or uninstall. The
reason for this is that browsers cache CSS and JS files, so the following situation can occur: 1. A user
accesses a Joomla website, and the CSS and JS files are stored in the user’s browser. 2. Joomla is
updated, and in the update process, the contents of several CSS and JS files change. The file names
remain the same. 3. The user accesses the newly updated site, but the new CSS and JS files are not
reloaded because the user’s browser uses the cached versions instead. 4. if version'=> 'auto is
set, the src attribute of the <script> tag is different after the update, and the browser loads the new
file. For normal work with a Joomla website this setting is useful. When developing it might happen
that you want to reload web asset files more often. I use debug mode when developing, because this
way a new media version is forced on every HTTP request.
What does the attribute "defer": true mean? Scripts are loaded with async asynchronous or
parallel to other resources. defer promises the browser that the web page will not be changed
by instructions. More information at Mozilla.org.
The Joomla Web Assets Manager manages all assets in a Joomla installation. It is not mandatory to
include script files or stylesheets via this manager. All calls to HTMLHelper::_('stylesheet
or script ...) work, but these assets are appended after the Web Asset Manager assets. This
results in overriding styles that are set in the template. Thus, a user does not have the possibility
to manipulate by means of a user.css. However, it does have more advantages: If dependencies
are set correctly, no conflicts occur and necessary files are loaded by Joomla. For example, we
have set a dependency in the line "dependencies": ["core"],.
10.1.1.4. media/com_foos/js/admin-foos-modal.js
The following is the JavaScript code that causes a foo element to be selectable when a menu item is cre-
ated. We will assign the class select-link, which is the main element in the file, to the corresponding
button in the field later.
media/com_foos/js/admin-foos-modal.js
1 /* https://codeberg.org/astrid/j4examplecode/raw/branch/t7/src/media/
com_foos/js/admin-foos-modal.js */
2
3 ;(function () {
4 'use strict'
5
6 document.addEventListener('DOMContentLoaded', function () {
7 var elements = document.querySelectorAll('.select-link')
8
9 for (var i = 0, l = elements.length; l > i; i += 1) {
10 elements[i].addEventListener('click', function (event) {
11 event.preventDefault()
12 var functionName = event.target.getAttribute('data-function')
13
14 window.parent[functionName](
15 event.target.getAttribute('data-id'),
16 event.target.getAttribute('data-title'),
17 null,
18 null,
19 event.target.getAttribute('data-uri'),
20 event.target.getAttribute('data-language'),
21 null
22 )
23
24 if (window.parent.Joomla.Modal) {
25 window.parent.Joomla.Modal.getCurrent().close()
26 }
27 })
28 }
29 })
30 })()
10.1.2.1. administrator/components/com_foos/foos.xml
We have created a new JavaScript file. We place it in the media\js directory. So that it is copied when
the component is installed, we add the js folder in the section media of the installation manifest.
administrator/components/com_foos/foos.xml
1 <folder>src</folder>
2 <folder>tmpl</folder>
3 </files>
4 + <media folder="media/com_foos" destination="com_foos">
5 + <folder>js</folder>
6 + <filename>joomla.asset.json</filename>
7 + </media>
8 <!-- Back-end files -->
9 <administration>
10 <!-- Menu entries -->
Read in the preface why you choose the media directory ideally for assets like JavaScript files or
stylesheets.
10.1.2.2. components/com_foos/src/Model/FooModel.php
We no longer output static text. An item from the database is displayed. Therefore we rename the
getMsg method to getItem. We adjust the variable names and create a database query.
Make sure you update the DocBlock here. This sounds nit-picky and unimportant at the beginning.
In small extensions, it may still be minor. But later you may want to create documentation
automatically based on this information. Then you will be happy if they are correct.
components/com_foos/src/Model/FooModel.php
54 + }
55
56 - return $this->message;
57 + return $this->_item[$pk];
58 }
59 }
Joomla supports you in creating the database queries. If you use the available statementsa ,
Joomla will take care of security or different syntax in PostgreSQL and MySQL.
a
docs.joomla.org/accessing_the_database_using_jdatabase
10.1.2.3. components/com_foos/src/View/Foo/HtmlView.php
components/com_foos/src/View/Foo/HtmlView.php
We will customize the display of the name in the template. Here we access the item element and its
name property. In this way we can flexibly and easily add new properties in the future.
components/com_foos/tmpl/foo/default.php
1 \defined('_JEXEC') or die;
2 ?>
3
4 -Hello Foos: <?php echo $this->msg;
5 +<?php
6 +echo $this->item->name;
We create an entry in the default.xml file for the new form field. This way we enable the selection of a
Foo element at the menu item. Worth mentioning are the entries addfieldprefix="FooNamespace
\Component\Foos\Administrator\Field" and type="modal_foo":
components/com_foos/tmpl/foo/default.xml
1 </layout>
2 <!-- Add fields to the request variables for the layout. -->
3 <fields name="request">
4 - <fieldset name="request">
5 + <fieldset name="request"
6 + addfieldprefix="FooNamespace\Component\Foos\Administrator\
Field"
7 + >
8 <field
9 - name="show_text"
10 - type="text"
11 - label="COM_FOOS_FIELD_TEXT_SHOW_LABEL"
12 - default="Hi"
13 + name="id"
14 + type="modal_foo"
15 + label="COM_FOOS_SELECT_FOO_LABEL"
16 + required="true"
17 + select="true"
18 + new="true"
19 + edit="true"
20 + clear="true"
21 />
22 </fieldset>
23 </fields>
1. install your component in Joomla version 4 to test it: Copy the files in the administrator folder
into the administrator folder of your Joomla 4 installation. Copy the files in the components
folder into the components folder of your Joomla 4 installation. Copy the files in the media
folder into the media folder of your Joomla 4 installation. A new installation is required to register
the file joomla.asset.json.
2. open the menu manager to create a menu item. Click on Menu and then on All Menu Items.
Then click on the New button and fill in all the necessary fields. You can find the appropriate Menu
Item Type by clicking the Select button. Make sure that you see a selection field instead of the text
field for entering static text. The selection field also contains a Select button.
3. click on the second Select and select an item. Make sure that a selected item can be cleared
using Clear.
5. switch to the frontend and make sure that the menu item is created correctly. You will see the
title of the element you selected in the administration area.
Your goal was that descriptive text in the views are not mixed with the program code. So they are
uncomplicatedly changeable via the Joomla backend. This is possible for every user. Even the one
who is not familiar with the program code. By the way, this is the prerequisite for your extension to be
multilingual! For this reason you did not enter the real texts directly into the program code, but used
language strings instead. Specifically, by descriptive texts I mean the texts that are displayed in the
browser. You had prepared everything so that you use special files. These are easily changeable. So far
you have seen cryptic texts in the browser views. In this part we translate the unattractive language
strings into human readable words.
Even if your target audience speaks English and you only support this language it is important
to use a language file for texts you display in the front-end or back-end of the component. This
way it is possible for users to overwrite texts via language overridea without editing the source
code. Under some circumstances a user prefers to write first name instead of name in the column
header.
a
docs.joomla.org/j3.x:language_overrides_in_joomla
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t7...t8
The frontend view and the administration area each use their own language files. Unlike the frontend,
where there is only one, the backend needs two - *.sys.ini and *.ini. Briefly explained: The file
with the extension sys.ini is used to translate the XML installation file as well as the menu elements.
The ini is responsible for the rest. This has the advantage that during the installation and for the
construction of the menu only the loading of small text files is necessary. A disadvantage is that
some language strings have to be entered twice. You can find out more in the article International
Enhancements which has a section on the file *.sys.ini1 .
1
docs.joomla.org/international_enhancements_for_version_1.6#the_new_.sys.ini
The addition of the English language files is mandatory. All other languages are optional. The
reason for this is that if a file is missing, the English version is used by default. If a Frenchman
installs the extension - which contains German and English language files - on his Joomla with the
default language French, the texts will be displayed in English. Note: This only applies to missing
language files. A missing language key in a non-English language file will not be replaced with the
key from the English file. This means that it is mandatory that each file is complete. Joomla does
not search in different language version for a language string missing in a file.
Would you like to see exactly how the ini file is parsed? At php.neta you will find the description of
the function that does this work.
a
php.net/manual/en/function.parse-ini-file.php
11.1.1.1. Commenting
1 ; Joomla Project
2 ; (C) 2005 Open Source Matters, Inc. <https://www.joomla.org>
3 ; License GNU General Public License version 2 or later; see LICENSE.
txt
4 Note : All ini files need to be saved as UTF-8
5 ....
11.1.1.2. Escaping
There are characters that have a special meaning - for example, the inverted commas " that mark the
beginning and end of the translation. This meaning can be cancelled with a backslash \.
1 ...
2 COM_CONTACT_CONTACT_REQUIRED="<strong class=\"red\">*</strong> Required
field"
3
4 ...
Language strings in Joomla have in the past used the string “QQ” to avoid double quotes within
the language INI files. This was a short-term solution. Older PHP versions were not able to handle
double quotes. For more information, see the PR 19024.a
a
github.com/joomla/joomla-cms/issues/19024
11.1.1.3. Variables
Sometimes the output of the language string depends on a variable. The function Text::sprintf
ensures that you do not have to compose the text in a complicated way in the programme code. Instead
of the variable in the language file, enter a character with the prefix %. For example, you can use %s.
Are you interested in the exact structure of the function Text::sprintf? You can find it in
Joomla in the file libraries/src/Language/Text.php.
1 ...
2 COM_CONTACT_CHECKED_OUT_BY="Checked out by %s"
3 ...
1 ...
2 Text::sprintf('COM_CONTACT_CHECKED_OUT_BY', $checkoutUser->name)
3 ...
The value of $checkoutUser->name is inserted instead of the first variable in the language string.
Here in the example instead of %s.
Is there more than one variable? You can specify which variable belongs to which language
string. For example, use %1$s, %2$s as in JLIB_DATABASE_ERROR_STORE_FAILED="%1$s:
:store failed<br>%2$s". The values are assigned in the order indicated in the number after
the “%” character.
11.1.1.4. singular/singular
There is singular or singular and plural or plural and the Joomla language strings support this.
1 ...
2 $message = Text::plural('COM_FOOS_N_ITEMS_FEATURED', count($ids));
3 ...
as an example.
Depending on whether count($ids) has the value 1 or 2 the language string COM_FOOS_N_
ITEMS_FEATURED_1 or COM_FOOS_N_ ITEMS_FEATURED_2 is used. If count($ids) has neither 1
nor 2, COM_FOOS_N_ ITEMS_FEATURED is used.
1 ...
2 COM_FOOS_N_ITEMS_FEATURED="%d foos featured."
3 COM_FOOS_N_ITEMS_FEATURED_1="Foo featured."
4 COM_FOOS_N_ITEMS_FEATURED_2="Two foos featured."
5 ...
Create six files to support the German language in addition to English. Each file is structured as follows:
One language string is inserted per line. The left side of the equal sign in the language string, for exam-
ple COM_FOOS_ CONFIGURATION" in COM_FOOS_ CONFIGURATION="Foo Options", is always in
upper case. Normally the extension name is at the beginning, in our case it is COM_FOOS. After that you
ideally add a short description. Here you describe briefly what this string is used for. Make sure that you
do not use spaces. Only letters and underscores are allowed. The right side of the language string, for
example Foo Options" in COM_FOOS_ CONFIGURATION = "Foo Options", is the actual text that
will be displayed on the site. When your extension is translated into another language, the translator
only changes this right side of the language string in his language file. The right side is enclosed in
quotation marks.
We add the German language version for the administration area with the files “administrator/components/com_foos/lan
DE/com_foos.ini” and “administrator/components/com_foos/language/en-DE/com_foos.sys.ini”.
Don’t be confused if you see a lot of texts in the sample data. These are not all used at the moment.
I’m already inserting the text for the future chapters here.
administrator/components/com_foos/language/de-DE/com_foos.ini
1
2 COM_FOOS="[PROJECT_NAME]"
3 COM_FOOS_CONFIGURATION="Foo Optionen"
4 COM_FOOS_FOOS="Foos"
5 COM_FOOS_CATEGORIES="Kategorien"
6 ...
Naming conventions: Each language file is marked with an abbreviation, which is defined in
ISO-639a and ISO-3166b : The first two lower case letters name the language. For German this is
de and en for English. After the hyphen, the two capital letters indicate the country. For example,
Swiss German can be distinguished from DE by CH or Austrian by AT. A folder named de-CH
contains the translation for Switzerland and de-AT the Austrian variant.
a
en.wikipedia.org/wiki/iso_639
b
en.wikipedia.org/wiki/iso_3166
As mentioned before, you need two language files for the backend: one ending with .ini and one
ending with sys.ini. The sys.ini is primarily used during installation and for displaying the menu
items and the ini for everything else.
administrator/components/com_foos/language/de-DE/com_foos.sys.ini
1
2 COM_FOOS="[PROJECT_NAME]"
3 COM_FOOS_XML_DESCRIPTION="Foo Komponente"
4 ...
5 COM_FOOS_INSTALLERSCRIPT_PREFLIGHT="<p>Alles hier passiert vor der
Installation / Aktualisierung / Deinstallation der Komponente</p>"
6 COM_FOOS_INSTALLERSCRIPT_UPDATE="<p>TDie Komponente wurde aktualisiert
</p>"
7 COM_FOOS_INSTALLERSCRIPT_UNINSTALL="<p>Die Komponente wurde
deinstalliert</p>"
8 COM_FOOS_INSTALLERSCRIPT_INSTALL="<p>Die Komponente wurde installiert</
p>"
9 COM_FOOS_INSTALLERSCRIPT_POSTFLIGHT="<p>Alles hier passiert nach der
Installation / Aktualisierung / Deinstallation der Komponente</p>"
10 ...
I had already written it: The English versions of the language files should always be available as a
fallback.
administrator/components/com_foos/language/en-GB/com_foos.ini
1
2 COM_FOOS="[PROJECT_NAME]"
3 COM_FOOS_CONFIGURATION="Foo Options"
4 COM_FOOS_FOOS="Foos"
5 COM_FOOS_CATEGORIES="Categories"
6 ...
administrator/components/com_foos/language/en-GB/com_foos.sys.ini
1
2 COM_FOOS="[PROJECT_NAME]"
3 COM_FOOS_CONFIGURATION="Foo Options"
4 ...
5 COM_FOOS_INSTALLERSCRIPT_PREFLIGHT="<p>Anything here happens before the
installation/update/uninstallation of the component</p>"
6 COM_FOOS_INSTALLERSCRIPT_UPDATE="<p>The component has been updated</p>"
7 COM_FOOS_INSTALLERSCRIPT_UNINSTALL="<p>The component has been
uninstalled</p>"
8 COM_FOOS_INSTALLERSCRIPT_INSTALL="<p>The component has been installed</
p>"
9 COM_FOOS_INSTALLERSCRIPT_POSTFLIGHT="<p>Anything here happens after the
installation/update/uninstallation of the component</p>"
10 ...
In the frontend there is only the .ini - so no sys.ini. The file components/com_foos/language
/en-DE/com_foos.ini implements the German language.
components/com_foos/language/de-DE/com_foos.ini
1
2 COM_FOOS_NAME="Vorame: "
3 ...
components/com_foos/language/en-GB/com_foos.ini
1
2 COM_FOOS_NAME="Surname: "
3 ...
In the next chapters more language strings will be added. I will not mention them separately there.
I have already integrated most of them into the sample files in this lesson. This way I avoid that
the files appear in the diff view and blow it up unnecessarily. Specifically, I mean the diff view of
the program code of the different chapters on Github, which I refer to here in the text.
11.1.3.1. administrator/components/com_foos/foos.xml
To make sure that the language files are copied to Joomla Core when the extension is installed, we add
the <folder>language</folder> entry for the frontend and the backend to the manifest.
administrator/components/com_foos/foos.xml
1
2 </uninstall>
3 <!-- Frond-end files -->
4 <files folder="components/com_foos">
5 + <folder>language</folder>
6 <folder>src</folder>
7 <folder>tmpl</folder>
8 </files>
9
10 <files folder="administrator/components/com_foos">
11 <filename>foos.xml</filename>
12 <folder>forms</folder>
13 + <folder>language</folder>
14 <folder>services</folder>
15 <folder>sql</folder>
16 <folder>src</folder>
11.1.3.1.1. Where are the language files ideally stored? Joomla’s own components store the files
for the administration area in the folder /administrator/language/en-GB/ and those for the
site in the folder /language/en-GB/. This is the first place Joomla looks for the language files.
For this reason, it was common for extension developers to put their files here. Sometimes it is
more straightforward to put them in your own component folder. In our example, this is the folder
administrator/components/com_foos/language/en-GB/ for the backend and components/
com_foos/language/en-GB/ for the frontend. This is the place where Joomla looks for the language
file if it doesn’t find anything suitable in the Joomla Core directories /administrator/language/
en-GB / and / language/en-GB respectively.
You want to store your language files in the same directory as the Joomla core extensions? To place
your files together with Joomla’s own language files, you add the <language> tag to the installation
file. Here is an example from com_contact
1 ...
2 <files folder="components/com_contact">
3 ...
4 </files>
5
6 <languages folder="site">
7 <language tag="en-GB">language/en-GB.com_contact.ini</language>
8 </languages>
9
10 <administration>
11 ...
12 <languages folder="admin">
13 <language tag="en-GB">language/en-GB.com_contact.ini</
language>
14 <language tag="en-GB">language/en-GB.com_contact.sys.ini</
language>
15 </languages>
16 </administration>
17 ...
where you need to adjust the value of the folder parameter to your structure:
1 ...
2 <files folder="components/com_foos">
3 ...
4 </files>
5
6 <languages folder="language">
7 <language tag="en-GB">language/en-GB.com_foos.ini</language>
8 </languages>
9
10 <administration>
11 ...
12 <languages folder="administrator/language">
13 <language tag="en-GB">language/en-GB.com_foos.ini</language
>
14 <language tag="en-GB">language/en-GB.com_foos.sys.ini</
language>
15 </languages>
16 </administration>
17 ...
One last step is still missing. The own use of the language strings. So far we have printed the name
without a label in the frontend via echo $this->item->name;. Now we add a label that takes differ-
ent languages into account. The following code causes the string that is entered in the corresponding
language file to be printed in the frontend. This is done by the command Text::_('COM_FOOS_NAME
'). If there is a Spanish language file with the entry COM_FOOS_FIELD_NAME_LABEL="Nombre" and
the Spanish language is active in the frontend, then Nombre is printed. If the German language is
set and there is a German language file with the entry COM_FOOS_FIELD_NAME_LABEL="Name", the
word Name is displayed. If the Spanish language is active without a Spanish language file, the English
language file is used.
components/com_foos/tmpl/foo/default.php
1 \defined('_JEXEC') or die;
2 -?>
3
4 -<?php
5 -echo $this->item->name;
6 +use Joomla\CMS\Language\Text;
7 +
8 +echo Text::_('COM_FOOS_NAME') . $this->item->name;
1. install your component in Joomla version 4 to test it: Copy the files in the administrator folder
into the administrator folder of your Joomla 4 installation. Copy the files in the components
folder into the components folder of your Joomla 4 installation. A new installation is not
necessary. Continue using the files from the previous part. If you do a new installation, you will
notice that the hints in the installation script are now translated.
2. open the view of your component in the administration area and frontend and make sure that
the texts are readable and not cryptic anymore.
3. try out the new feature. Create language files for different languages and change the default
language in Joomla. Make sure that Joomla translates correctly.
12. Configuration
Are there things you plan to offer configurable? Then this part is important for you. Here I show you how
to add a configuration to your component in the Joomla typical way. We create the global configuration
for our component!
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t8...t9
We add the config.xml file. This implements the configuration parameters. In this XML file you can
use all standard form field types1 as usual or implement your own types analogous to the already
created modal field FieldFoo.
We use a selection field of type type="list". We minimise the translation work by using the global
language strings JNO and JYES. All texts that Joomla translates in the file language/en-GB/joomla
.ini can be used globally.
administrator/components/com_foos/config.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <config>
4 <fieldset
5 name="foo"
6 label="COM_FOOS_FIELD_CONFIG_INDIVIDUAL_FOO_DISPLAY"
7 description="COM_FOOS_FIELD_CONFIG_INDIVIDUAL_FOO_DESC"
8 >
9 <field
1
docs.joomla.org/form_field
10 name="show_foo_name_label"
11 type="list"
12 label="COM_FOOS_FIELD_FOO_SHOW_CATEGORY_LABEL"
13 default="1"
14 >
15 <option value="0">JNO</option>
16 <option value="1">JYES</option>
17 </field>
18 </fieldset>
19 </config>
12.1.2.1. administrator/components/com_foos/foos.xml
The addition in the foos.xml file ensures that the config.xml file is copied during installation and
Joomla can thus access it later.
administrator/components/com_foos/foos.xml
1 </submenu>
2 <files folder="administrator/components/com_foos">
3 <filename>foos.xml</filename>
4 + <filename>config.xml</filename>
5 <folder>forms</folder>
6 <folder>language</folder>
7 <folder>services</folder>
administrator/components/com_foos/src/View/Foos/HtmlView.php
1 ToolbarHelper::title(Text::_('COM_FOOS_MANAGER_FOOS'), 'address
foo');
2
3 $toolbar->addNew('foo.add');
4 +
5 + $toolbar->preferences('com_foos');
6 }
7
8 }
12.1.2.3. components/com_foos/src/Model/FooModel.php
The populateState method ensures that the State object is correctly filled and available to all code.
We add the new parameter here for the site part.
populateState() is called automatically when we use getState() for the first time. If we need
something special in the method, we override it in our own model - as in the following code example.
You may wonder which populateState() method is called when nothing is implemented in
our own extension. Quite simple: FooModel (components/com_foos/src/Model/FooModel
.php) extends BaseDatabaseModel (libraries/src/MVC/Model/BaseDatabaseModel
.php), which in turn extends BaseModel (libraries/src/MVC/Model/BaseModel.
php). The latter implements StateBehaviorTrait (libraries/src/MVC/Model/
StateBehaviorTrait.php) in which you find the method protected function
populateState(){}. This method is empty and does nothing. But: It is callable. It is
extremely helpful to follow up on such questions. This is how you get to know Joomla.
components/com_foos/src/Model/FooModel.php
1 return $this->_item[$pk];
2 }
3 +
4 + protected function populateState()
5 + {
6 + $app = Factory::getApplication();
7 +
8 + $this->setState('foo.id', $app->input->getInt('id'));
9 + $this->setState('params', $app->getParams());
10 + }
11 }
components/com_foos/tmpl/foo/default.php
1 use Joomla\CMS\Language\Text;
2
3 -echo Text::_('COM_FOOS_NAME') . $this->item->name;
4 +if ($this->get('State')->get('params')->get('show_foo_name_label'))
5 +{
6 + echo Text::_('COM_FOOS_NAME');
7 +}
8 +
9 +echo $this->item->name;
1. install your component in Joomla version 4 to test it: Copy the files in the administrator folder
into the administrator folder of your Joomla 4 installation. Copy the files in the components
folder into the components folder of your Joomla 4 installation. A new installation is not
necessary. Continue using the files from the previous part.
2. open the view of your component in the administration area and make sure that you see the
button Options in the upper right corner.
3. click on Options and adjust the display of the label according to your wishes.
4. open as last, the view in the frontend. Does the display of the label behave as you have set it in
the administration area?
Not everyone has the right to edit all content. For this purpose Joomla offers an access control list, the
ACL. With this you manage user rights in your component.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t9...t10
First, we set all possible permissions in an XML file. Each component can define individual permissions.
I orientate myself here on the usual actions in Joomla. core.admin thereby determines which groups
are allowed to configure the permissions at component level via the options button in the toolbar.
core.manage determines which groups are allowed to access the backend of the component.
administrator/components/com_foos/access.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <access component="com_foos">
4 <section name="component">
5 <action name="core.admin" title="JACTION_ADMIN" />
6 <action name="core.options" title="JACTION_OPTIONS" />
7 <action name="core.manage" title="JACTION_MANAGE" />
8 <action name="core.create" title="JACTION_CREATE" />
9 <action name="core.delete" title="JACTION_DELETE" />
10 <action name="core.edit" title="JACTION_EDIT" />
11 <action name="core.edit.state" title="JACTION_EDITSTATE" />
Joomla stores the permissions in the database. Regarding the database, only changes are relevant
during a Joomla update. We enter these in the file administrator/components/com_foos/sql
/updates/mysql/VERSIONSNUMMER.sql, here this is specifically administrator/components/
com_foos/sql/updates/mysql/10.0.0.sql. This file is only called during an update. In case of a
new installation the database will be set up correctly via the main file administrator/components
/com_foos/sql/install.mysql.utf8.sql.
administrator/components/com_foos/sql/updates/mysql/10.0.0.sql
1
2 ALTER TABLE `#__foos_details ` ADD COLUMN `access ` int(10) unsigned NOT
NULL DEFAULT 0 AFTER `alias`;
3
4 ALTER TABLE `#__foos_details ` ADD KEY `idx_access ` ( `access`);
We set the permissions for the entire component in the configuration. For this we integrate a special
form field. Joomla offers the type rules for this.
administrator/components/com_foos/config.xml
1 <option value="1">JYES</option>
2 </field>
3 </fieldset>
4 + <fieldset
5 + name="permissions"
6 + label="JCONFIG_PERMISSIONS_LABEL"
7 + description="JCONFIG_PERMISSIONS_DESC"
8 + >
9 + <field
10 + name="rules"
11 + type="rules"
12 + label="JCONFIG_PERMISSIONS_LABEL"
13 + validate="rules"
14 + filter="rules"
15 + component="com_foos"
16 + section="component"
17 + />
18 + </fieldset>
19 </config>
13.1.2.2. administrator/components/com_foos/foos.xml
To make sure that everything runs smoothly during the installation, we add the new file and folder
sql/updates/mysql and access.xml here.
administrator/components/com_foos/foos.xml
We extend the form for creating a new Foo item with the possibility to set permissions for a single item.
We add the field name="access".
administrator/components/com_foos/forms/foo.xml
1 size="45"
2 hint="JFIELD_ALIAS_PLACEHOLDER"
3 />
4 +
5 + <field
6 + name="access"
7 + type="accesslevel"
8 + label="JFIELD_ACCESS_LABEL"
9 + size="1"
10 + />
11 </fieldset>
12 </form>
The SQL script for a new installation of the component is also extended with the necessary fields. In
this way we ensure that the database is also completely set up for a new installation.
administrator/components/com_foos/sql/install.mysql.utf8.sql
1 ('Nina'),
2 ('Astrid'),
3 ('Elmar');
4 +
5 +ALTER TABLE `#__foos_details ` ADD COLUMN `access ` int(10) unsigned
NOT NULL DEFAULT 0 AFTER `alias`;
6 +
7 +ALTER TABLE `#__foos_details ` ADD KEY `idx_access ` ( `access`);
If you are not familiar with SQL, the database query in the model will now seem complex. It is now
necessary to combine data from two database tables. One table is #__viewlevels which manages
the permissions of com_user. The other table is that of our example component which is named
#__foos_details. Don’t feel discouraged by this. Joomla supports you in creating the queries.
administrator/components/com_foos/src/Model/FoosModel.php
1
2 // Select the required fields from the table.
3 $query->select(
4 - $db->quoteName(array('id', 'name', 'alias'))
5 + $db->quoteName(array('a.id', 'a.name', 'a.alias', 'a.access
'))
6 );
7 - $query->from($db->quoteName('#__foos_details'));
8 +
9 + $query->from($db->quoteName('#__foos_details', 'a'));
10 +
11 + // Join over the asset groups.
12 + $query->select($db->quoteName('ag.title', 'access_level'))
13 + ->join(
14 + 'LEFT',
As a reminder, Joomla supports you in creating the database queries. If you use the available
statementsa , Joomla will take care of security or different syntax in PostgreSQL and MySQL for
you.
a
docs.joomla.org/accessing_the_database_using_jdatabase
A button to create an element is only useful if this is allowed. Therefore we change the view - $canDo
is added. The call $canDo = ContentHelper::getActions('com_foos'); gets the actions you
defined in the file administrator/components/com_foos/access.xml at the beginning of this
chapter.
administrator/components/com_foos/src/View/Foos/HtmlView.php
1
2 \defined('_JEXEC') or die;
3
4 +use Joomla\CMS\Helper\ContentHelper;
5 use Joomla\CMS\Language\Text;
6 use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
7 use Joomla\CMS\Toolbar\Toolbar;
8
9 protected function addToolbar()
10 {
11 + $canDo = ContentHelper::getActions('com_foos');
12 +
13 // Get the toolbar object instance
14 $toolbar = Toolbar::getInstance('toolbar');
15
16 ToolbarHelper::title(Text::_('COM_FOOS_MANAGER_FOOS'), 'address
foo');
17
18 - $toolbar->addNew('foo.add');
19 + if ($canDo->get('core.create'))
20 + {
21 + $toolbar->addNew('foo.add');
22 + }
23
24 - $toolbar->preferences('com_foos');
25 + if ($canDo->get('core.options'))
26 + {
27 + $toolbar->preferences('com_foos');
28 + }
29 }
30 -
31 }
administrator/components/com_foos/tmpl/foo/edit.php
Last but not least, we include a column in the overview for the authorization display.
administrator/components/com_foos/tmpl/foos/default.php
9 </th>
10
11 <?php echo $editIcon; ?><?php
echo $this->escape($item->
name); ?></a>
12
13 </th>
14 + <td class="small d-none d-md-table-cell
">
15 + <?php echo $item->access_level; ?>
16 + </td>
17 <td class="d-none d-md-table-cell">
18 <?php echo $item->id; ?>
19 </td>
Note that I have not covered all cases here where permissions need to be handled. This description
is intended as a best practice.
1. install your component in Joomla version 4 to test it: Copy the files in the administrator
folder to the administrator folder of your Joomla 4 installation. Install your component as
described in part one, after copying all files. Joomla will update the database for you during the
installation.
2. create a new item in your component. Make sure that you are offered a checkbox for saving a
permission. The value you enter here will be saved with the item and can be queried when it is
displayed in a list.
3. consider how you use the permission set in point 2 for your purposes? So far, the value is only
stored, we do not use it.
5. Open the options of the global configuration. Here you have the possibility to set the permissions
for the use of the component globally.
6. Play with the settings. Allow the administrator to use the component in the backend. But remove
his right to create new elements in the extension. After that, log in as a simple administrator and
make sure that the “New” button has disappeared.
13.3. Links
1
docs.joomla.org/j3.x:access_control_list_tutorial
Your component is user-friendly. User experience (UX) or user experience is on everyone’s lips. If a
user enters incorrect data, it’s important to you that they get an explanation. This is where we use
validation.
In server-side validation, the input submitted by the user is sent to the server and validated using
the scripting language. In the case of Joomla, this is PHP. After the validation process on the server
side, the feedback is sent back to the client from a new dynamically generated web page. It is safe
to validate user input from the server. Malicious attackers have no easy game this way. Client-side
scripting languages are easier to trick. Intruders bypass them to send malicious input to the server.
Since both validation methods (server and client) have their own importance, it is recommended
to use them simultaneously. Server-side validation is more secure. The client-side one is more
user-friendly!
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t10...t11a
The main goal here is not to learn meaningful validation. Rather, I’m showing you how to integrate your
rules into Joomla. That’s why you see here only a rudimentary example: In the name it is forbidden to
insert a number from now on. In concrete terms, this means: Astrid is allowed. Astrid9 is not allowed.
For this we create the file LetterRule.php.
Here in the example I only use the regular expression to be checked in the class LetterRule.php.
Of course it is possible to integrate complex checks using functions.
administrator/components/com_foos/src/Rule/LetterRule.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Administrator\Rule;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\Form\FormRule;
9
10 class LetterRule extends FormRule
11 {
12 protected $regex = '^([a-z]+)$';
13
14 protected $modifiers = 'i';
15 }
It is not necessary to implement the test method in your file. You inherit it from the class FormRule
, which is implemented in the file /libraries/src/Form/FormRule.php. In it you will find the
code
1 ...
2 protected $regex;
3 ...
4 public function test(\SimpleXMLElement $element, $value, $group = null,
Registry $input = null, Form $form = null)
5 {
6 ...
7 // Test the value against the regular expression.
8 if (preg_match(\chr(1) . $this->regex . \chr(1) . $this->modifiers,
$value)) {
9 return true;
10 }
11
12 return false;
13 }
To make Joomla apply the rule in the LetterRule.php file to the text field for entering the name, we
modify the form file.
administrator/components/com_foos/forms/foo.xml
To show you what is possible I add below two examples from Joomla Core as inspiration.
14.2.1. Username
For the username, the Joomla database is used to check if the name already exists. In this case false
is returned. Otherwise the test is successful.
1 <?php
2
3 namespace Joomla\CMS\Form\Rule;
4
5 use Joomla\CMS\Form\Form;
6 use Joomla\CMS\Form\FormRule;
7 use Joomla\Database\DatabaseAwareInterface;
8 use Joomla\Database\DatabaseAwareTrait;
9 use Joomla\Database\ParameterType;
10 use Joomla\Registry\Registry;
11
12 class UsernameRule extends FormRule implements DatabaseAwareInterface
13 {
14 use DatabaseAwareTrait;
15
16 public function test(\SimpleXMLElement $element, $value, $group =
null, Registry $input = null, Form $form = null)
17 {
18 // Get the database object and a new query object.
19 $db = $this->getDatabase();
20 $query = $db->getQuery(true);
21
22 // Get the extra field check attribute.
23 $userId = ($form instanceof Form) ? (int) $form->getValue('id')
: 0;
24
25 // Build the query.
26 $query->select('COUNT(*)')
27 ->from($db->quoteName('#__users'))
28 ->where(
29 [
30 $db->quoteName('username') . ' = :username',
31 $db->quoteName('id') . ' <> :userId',
32 ]
33 )
34 ->bind(':username', $value)
35 ->bind(':userId', $userId, ParameterType::INTEGER);
36
37 // Set and query the database.
38 $db->setQuery($query);
39 $duplicate = (bool) $db->loadResult();
40
41 if ($duplicate) {
42 return false;
43 }
44
45 return true;
46 }
47 }
14.2.2. URL
The URL field does not require a regular expression. Various requirements are queried successively. If a
requirement is not given, false is returned. Otherwise, the test is successful.
1 <?php
2
3 namespace Joomla\CMS\Form\Rule;
4
5 use Joomla\CMS\Form\Form;
6 use Joomla\CMS\Form\FormRule;
7 use Joomla\CMS\Language\Text;
8 use Joomla\Registry\Registry;
9 use Joomla\String\StringHelper;
10 use Joomla\Uri\UriHelper;
11
12 class UrlRule extends FormRule
13 {
14 public function test(\SimpleXMLElement $element, $value, $group =
null, Registry $input = null, Form $form = null)
15 {
16 // If the field is empty and not required, the field is valid.
17 $required = ((string) $element['required'] === 'true' || (
string) $element['required'] === 'required');
18
19 if (!$required && empty($value)) {
20 return true;
21 }
22
23 $urlParts = UriHelper::parse_url($value);
24
25 // See https://www.w3.org/Addressing/URL/url-spec.txt
26 // Use the full list or optionally specify a list of permitted
schemes.
27 if ($element['schemes'] == '') {
28 $scheme = array('http', 'https', 'ftp', 'ftps', 'gopher', '
mailto', 'news', 'prospero', 'telnet', 'rlogin', 'sftp',
'tn3270', 'wais',
29 'mid', 'cid', 'nntp', 'tel', 'urn', 'ldap', 'file', '
fax', 'modem', 'git');
30 } else {
31 $scheme = explode(',', $element['schemes']);
32 }
33
34 /*
35 if ($urlParts === false || !\array_key_exists('scheme',
$urlParts)) {
36 /*
37 if ($urlParts === false || !$element['relative']) {
38 $element->addAttribute('message', Text::sprintf('
JLIB_FORM_VALIDATE_FIELD_URL_SCHEMA_MISSING', $value
, implode(', ', $scheme)));
39
40 return false;
41 }
42
43 // The best we can do for the rest is make sure that the
1. install your component in Joomla version 4 to test it: Copy the files in the administrator
folder into the administrator folder of your Joomla 4 installation. A new installation is not
necessary. Continue using the ones from the previous part.
2. Open the view of your component in the administration area and create a new item or edit an
existing one. Enter a number in the text field for the name.
3. Then edit another field, for example set the access to Registered.
4. make sure that you don’t get any warning at this time.
5. try to save your input at the end. This is not possible. You will see a warning.
Did you notice it? You may see the warning only after you have made a lot of changes in the form
and want to save all changes. In this small extension it does not matter. In large forms, the hint at
the end can be frustrating. A user may want to see it immediately after the incorrect input. So it is
possible to act immediately and avoid unnecessary work. It is also frustrating to have to do all
the input again. This is where client-side validation comes into play. We will look at this in the
next part.
Our goal in this part: when we enter a number in the name field, an error message is displayed
immediately after we leave the field. In server-side validation, the message was not issued until after
the form was sent to the server via the Save button.
In client-side validation, we provide a better user experience by responding quickly at the browser level.
Here, all inputs in the user’s browser are validated immediately. Client-side validation does not require
a query to the server, thus reducing the load on the server and the network. This type of validation
works on the browser side using scripting languages such as JavaScript or with HTML5 attributes.
For example, if the user enters an invalid email format, we issue an error message immediately after
the user moves to the next field. This allows a correction to be made in a timely manner.
Most of the time, client-side validation depends on JavaScript being enabled in the browser. If
JavaScript is disabled, user input is sent to the server unchecked. It is possible that this is mali-
cious data! Therefore, client-side validation does not protect your component’s users from malicious
attacks.
Again: Since both validation methods (server and client) have their own importance, it is recom-
mended to use them side by side. Server-side validation is more secure - client-side validation is
more user-friendly!
For impatient people: Look at the changed program code in the Diff Viewa and apply these changes
to your development version.
a
codeberg.org/astrid/j4examplecode/compare/t11a...t11b
15.1.1.1. media/com_foos/js/admin-foos-letter.js
Again, it is about the principle, just like in the previous chapter. The quality of the validation is in this
tutorial secondary and I choose a simple example. Numbers are forbidden in the text field for the name.
Astrid is allowed. Astrid9 is not allowed.
In the example I use a regular expressiona . regex.test(value) returns true if regex is equal
to /^([a-z]+)$/i and value does not contain a number. For more information on the test
method, see developer.mozilla.orgb . It is not mandatory to use a regular expression. It is only
important that true is returned for a pass and false for a fail.
a
en.wikipedia.org/wiki/regular_expression
b
developer.mozilla.org/en/docs/web/javascript/reference/global_objects/regexp/test
media/com_foos/js/admin-foos-letter.js
1
2 document.addEventListener('DOMContentLoaded', function(){
3 "use strict";
4 setTimeout(function() {
5 if (document.formvalidator) {
6 document.formvalidator.setHandler('letter', function (value
) {
7
8 var returnedValue = false;
9
10 var regex = /^([a-z]+)$/i;
11
12 if (regex.test(value))
13 returnedValue = true;
14
15 return returnedValue;
16 });
17 //console.log(document.formvalidator);
18 }
19 }, (1000));
20 });
Note: The variable name returnedValue is only meant as an example. The name of a variable
should explain in real code why it exists, what it does and how it is used.
15.1.2.1. administrator/components/com_foos/foos.xml
administrator/components/com_foos/foos.xml
1 <folder>tmpl</folder>
2 </files>
3 <media folder="media/com_foos" destination="com_foos">
4 + <filename>joomla.asset.json</filename>
5 <folder>js</folder>
6 </media>
7 <!-- Back-end files -->
1
2 $wa = $this->document->getWebAssetManager();
3 $wa->useScript('keepalive')
4 - ->useScript('form.validate');
5 + ->useScript('form.validate')
6 + ->useScript('com_foos.admin-foos-letter');
7
8 $layout = 'edit';
9 $tmpl = $input->get('tmpl', '', 'cmd') === 'component' ? '&tmpl=
component' : '';
We add class="validate-letter", so Joomla knows which CSS class should be checked. Joomla
sets this class when creating the field. See for yourself by opening the form in the backend and checking
out the source code.
administrator/components/com_foos/forms/foo.xml
1 name="name"
2 type="text"
3 validate="Letter"
4 + class="validate-letter"
5 label="COM_FOOS_FIELD_NAME_LABEL"
6 size="40"
7 required="true"
15.1.2.4. media/com_foos/joomla.asset.json
Last but not least, we register the new file under the name com_foos.admin-foos-letter in the
webasset manager.
media/com_foos/joomla.asset.json
1
2 "description": "Joomla CMS",
3 "license": "GPL-2.0-or-later",
4 "assets": [
5 + {
6 + "name": "com_foos.admin-foos-letter",
7 + "type": "script",
8 + "uri": "com_foos/admin-foos-letter.js",
9 + "dependencies": [
10 + "core"
11 + ],
12 + "attributes": {
13 + "defer": true
14 + }
15 + },
16 {
17 "name": "com_foos.admin-foos-modal",
18 "type": "script",
1. install your component in Joomla version 4 to test it: Copy the files in the administrator folder
into the administrator folder of your Joomla 4 installation. Copy the files in the media folder
into the media folder of your Joomla 4 installation. A new installation is not necessary. Continue
using the files from the previous part. 2.
2. Open the view of your component in the administration area and create a new item or edit an
existing one. Enter a number in the text field for the title. 3.
3. Then edit another field, for example set the access to Registered.
Almost every website divides its content into categories. Joomla offers this useful feature as well. The
current part of the tutorial shows you how to ideally integrate categories into a Joomla component.
Don’t reinvent the wheel yourself. Use what Joomla offers you.
Categories are a way of organising content in Joomla A category contains posts and other cate-
gories. A post can only be in one category. If a category is contained in another, it is a subcategory
of the category.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t11b...t12
We store the data in the database that is necessary to classify an element into a category. Therefore,
in case of an update, it is important to add a column to the database. To do this, we create the
file administrator/components/com_foos/sql/updates/mysql/12.0.0.sql and enter the
necessary SQL statement in it. We choose the name because we are currently working on version 12 of
our extension.
administrator/components/com_foos/sql/updates/mysql/12.0.0.sql
1
2 ALTER TABLE `#__foos_details ` ADD COLUMN `catid ` int(11) NOT NULL
DEFAULT 0 AFTER `alias`;
3
4 ALTER TABLE `#__foos_details ` ADD KEY `idx_catid ` ( `catid`);
The entries in the file access.xml marked below with a plus sign are necessary to set permissions for
the categories. The new code causes the display of a tab for setting user permissions per category in
the administration area.
administrator/components/com_foos/access.xml
1
2 <action name="core.edit" title="JACTION_EDIT" />
3 <action name="core.edit.state" title="JACTION_EDITSTATE" />
4 <action name="core.edit.own" title="JACTION_EDITOWN" />
5 + <action name="core.edit.value" title="JACTION_EDITVALUE" />
6 + </section>
7 + <section name="category">
8 + <action name="core.create" title="JACTION_CREATE" />
9 + <action name="core.delete" title="JACTION_DELETE" />
10 + <action name="core.edit" title="JACTION_EDIT" />
11 + <action name="core.edit.state" title="JACTION_EDITSTATE" />
12 + <action name="core.edit.own" title="JACTION_EDITOWN" />
13 </section>
14 </access>
16.1.2.2. administrator/components/com_foos/foos.xml
1
2 <menu view="foos">COM_FOOS</menu>
3 <submenu>
4 <menu link="option=com_foos">COM_FOOS</menu>
5 + <menu link="option=com_categories&extension=com_foos">
JCATEGORY</menu>
6 </submenu>
7 <files folder="administrator/components/com_foos">
8 <filename>access.xml</filename>
We add a selection field with matching categories to the form used to create a Foo item. We use the
Joomla own field categoryedit for this. Note the line extension="com_foos". This ensures that
administrator/components/com_foos/forms/foo.xml
1 hint="JFIELD_ALIAS_PLACEHOLDER"
2 />
3
4 + <field
5 + name="catid"
6 + type="categoryedit"
7 + label="JCATEGORY"
8 + extension="com_foos"
9 + addfieldprefix="Joomla\Component\Categories\Administrator\
Field"
10 + required="true"
11 + default=""
12 + />
13 +
14 <field
15 name="access"
16 type="accesslevel"
16.1.2.4. administrator/components/com_foos/script.php
To ensure that a category already exists at the beginning, we add the script that is called during the
installation. Using the install method, we create a category with the title Uncategorised for the
component during a new installation. We store these directly in the database. To be able to specify
a user as the creator of the category, we request the ID of the administrator in the getAdminId()
method.
administrator/components/com_foos/script.php
1
2 \defined('_JEXEC') or die;
3 +
4 +use Joomla\CMS\Application\ApplicationHelper;
5 +use Joomla\CMS\Factory;
6 use Joomla\CMS\Installer\InstallerAdapter;
7 use Joomla\CMS\Language\Text;
8 use Joomla\CMS\Log\Log;
9 +use Joomla\CMS\Table\Table;
10
11
12 {
13 echo Text::_('COM_FOOS_INSTALLERSCRIPT_INSTALL');
14
15 + $db = Factory::getDbo();
16 + $alias = ApplicationHelper::stringURLSafe('FooUncategorised')
;
17 +
18 + // Initialize a new category.
19 + $category = Table::getInstance('Category');
20 +
21 + $data = array(
22 + 'extension' => 'com_foos',
23 + 'title' => 'FooUncategorised',
24 + 'alias' => $alias . '(en-GB)',
25 + 'description' => '',
26 + 'published' => 1,
27 + 'access' => 1,
28 + 'params' => '{"target":"","image":""}',
29 + 'metadesc' => '',
30 + 'metakey' => '',
31 + 'metadata' => '{"page_title":"","author":"","robots":""}',
32 + 'created_time' => Factory::getDate()->toSql(),
33 + 'created_user_id' => (int) $this->getAdminId(),
34 + 'language' => 'en-GB',
35 + 'rules' => array(),
36 + 'parent_id' => 1,
37 + );
38 +
39 + $category->setLocation(1, 'last-child');
40 +
41 + // Bind the data to the table
42 + if (!$category->bind($data))
43 + {
44 + return false;
45 + }
46 +
47 + // Check to make sure our data is valid.
48 + if (!$category->check())
49 + {
50 + return false;
51 + }
52 +
53 + // Store the category.
54 + if (!$category->store(true))
55 + {
56 + return false;
57 + }
58 +
59 return true;
60 }
61
62
63
64 return true;
65 }
66 +
67 + private function getAdminId()
68 + {
69 + $db = Factory::getDbo();
70 + $query = $db->getQuery(true);
71 +
72 + // Select the admin user ID
73 + $query
74 + ->clear()
75 + ->select($db->quoteName('u') . '.' . $db->quoteName('id'))
76 + ->from($db->quoteName('#__users', 'u'))
77 + ->join(
78 + 'LEFT',
79 + $db->quoteName('#__user_usergroup_map', 'map')
80 + . ' ON ' . $db->quoteName('map') . '.' . $db->quoteName
('user_id')
81 + . ' = ' . $db->quoteName('u') . '.' . $db->quoteName('
id')
82 + )
83 + ->join(
84 + 'LEFT',
85 + $db->quoteName('#__usergroups', 'g')
86 + . ' ON ' . $db->quoteName('map') . '.' . $db->quoteName
('group_id')
87 + . ' = ' . $db->quoteName('g') . '.' . $db->quoteName('
id')
88 + )
89 + ->where(
90 + $db->quoteName('g') . '.' . $db->quoteName('title')
91 + . ' = ' . $db->quote('Super Users')
92 + );
93 +
94 + $db->setQuery($query);
95 + $id = $db->loadResult();
96 +
97 + if (!$id || $id instanceof \Exception)
98 + {
99 + return false;
100 + }
101 +
102 + return $id;
103 + }
104 }
administrator/components/com_foos/services/provider.php
1
2 \defined('_JEXEC') or die;
3
4 +use Joomla\CMS\Categories\CategoryFactoryInterface;
5 use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
6 use Joomla\CMS\Extension\ComponentInterface;
7 use Joomla\CMS\Extension\Service\Provider\CategoryFactory;
8
9
10 $component->setRegistry($container->get(Registry::class
));
11 $component->setMVCFactory($container->get(
MVCFactoryInterface::class));
12 + $component->setCategoryFactory($container->get(
CategoryFactoryInterface::class));
13
14 return $component;
15 }
In order to create the table column in which the category of a Foo element is stored during a new
installation, we add the necessary SQL command in the SQL file that is called during the installation.
administrator/components/com_foos/sql/install.mysql.utf8.sql
Additionally, implementations are required in the component class to use Joomla’s own functions. The
method countItems is necessary so that an overview of assigned items appears in the category view.
The method getTableNameForSection ensures that the correct database table is always queried.
administrator/components/com_foos/src/Extension/FoosComponent.php
1 use Joomla\CMS\HTML\HTMLRegistryAwareTrait;
2 use FooNamespace\Component\Foos\Administrator\Service\HTML\
AdministratorService;
3 use Psr\Container\ContainerInterface;
4 +use Joomla\CMS\Helper\ContentHelper;
5
6
7 {
8 $this->getRegistry()->register('foosadministrator', new
AdministratorService);
9 }
10 +
11 + public function countItems(array $items, string $section)
12 + {
13 + try
14 + {
15 + $config = (object) array(
16 + 'related_tbl' => $this->getTableNameForSection(
$section),
17 + 'state_col' => 'published',
18 + 'group_col' => 'catid',
19 + 'relation_type' => 'category_or_group',
20 + );
21 +
22 + ContentHelper::countRelations($items, $config);
23 + }
24 + catch (\Exception $e)
25 + {
26 + // Ignore it
27 + }
28 + }
29 +
30 + protected function getTableNameForSection(string $section = null)
31 + {
32 + return ($section === 'category' ? 'categories' : 'foos_details'
);
33 +
34 + }
35 }
In the model we add to the database query the table where Joomla stores categories. Thus, in the
administration area, when a category is selected, only the elements belonging to it are displayed.
administrator/components/com_foos/src/Model/FoosModel.php
1
2 // Select the required fields from the table.
3 $query->select(
4 - $db->quoteName(array('a.id', 'a.name', 'a.alias', 'a.access
'))
5 + $db->quoteName(array('a.id', 'a.name', 'a.alias', 'a.access
', 'a.catid'))
6 );
7
8 $query->from($db->quoteName('#__foos_details', 'a'));
9
10 $db->quoteName('#__viewlevels', 'ag') . ' ON ' . $db->
quoteName('ag.id') . ' = ' . $db->quoteName('a.
access')
11 );
12
13 + // Join over the categories.
14 + $query->select($db->quoteName('c.title', 'category_title'))
15 + ->join(
16 + 'LEFT',
17 + $db->quoteName('#__categories', 'c') . ' ON ' . $db->
quoteName('c.id') . ' = ' . $db->quoteName('a.catid')
18 + );
19 +
20 return $query;
21 }
22 }
We add the category field to the form for editing an element. It is rendered using the information in the
XML form administrator/components/com_foos/forms/foo.xml, which we worked on earlier
in this chapter.
administrator/components/com_foos/tmpl/foo/edit.php
In the overview table of the view in the backend, we add a column for displaying the category.
administrator/components/com_foos/tmpl/foos/default.php
1
2 <a class="hasTooltip" href="<?php echo Route::_('index.php?
option=com_foos&task=foo.edit&id=' . (int) $item->id); ?>"
title="<?php echo Text::_('JACTION_EDIT'); ?> <?php echo
$this->escape(addslashes($item->name)); ?>">
3 <?php echo $editIcon; ?><?php echo $this->escape($item->
name); ?></a>
4
5 + <div class="small">
6 + <?php echo Text::_('JCATEGORY') . ': ' . $this->escape(
$item->category_title); ?>
7 + </div>
8 </th>
9 <td class="small d-none d-md-table-cell">
10 <?php echo $item->access_level; ?>
The categories help you to display your data in a structured way in the frontend. We will create
the frontend views in the further course of this tutorial.
1. install your component in Joomla version 4 to test it: Copy the files in the administrator
folder to the administrator folder of your Joomla 4 installation. Install your component as
described in part one, after copying all files. Joomla will update the database for you during the
installation. On the right-hand side of the table is an overview that lists how many elements are
published or unpublished. This does not work yet. We will work on publishing and unpublishing
in the next part.
3. in the sidebar you will see a new menu item. This offers you everything you need to create and
edit the categories of your component.
4. next open an element. Make sure that it is possible to assign a category to it.
5. make sure that the foo specific categories do not appear in other components, for example in
com_contact.
If you worked with Joomla, you know it from other components: Items have a status that can be
changed. This section shows you how to
• unpublish,
• publish,
• schedule,
• archive and
• trash items.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t13...t12
In case of an update, the database is updated to the latest version for version 13 using the file
administrator/components/com_foos/sql/updates/mysql/13.0.0.sql. Specifically,
columns are added for saving the data for publication.
administrator/components/com_foos/sql/updates/mysql/13.0.0.sql
1
2 ALTER TABLE `#__foos_details ` ADD COLUMN `published ` tinyint(1) NOT
NULL DEFAULT 0 AFTER `alias`;
3
4 ALTER TABLE `#__foos_details ` ADD COLUMN `publish_up ` datetime AFTER `
alias`;
5
6 ALTER TABLE `#__foos_details ` ADD COLUMN `publish_down ` datetime AFTER
`alias`;
7
8 ALTER TABLE `#__foos_details ` ADD KEY `idx_state ` ( `published`);
Now Joomla needs the class AdminController. Therefore, we create the class FoosController,
which inherits from AdminController. At the moment, FoosController does not contain any
implementations of its own. The controller only calls methods of the parent class.
administrator/components/com_foos/src/Controller/FoosController.php
1 <?php
2
3 <?php
4
5 namespace FooNamespace\Component\Foos\Administrator\Controller;
6
7 \defined('_JEXEC') or die;
8
9 use Joomla\CMS\Application\CMSApplication;
10 use Joomla\CMS\MVC\Controller\AdminController;
11 use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
12 use Joomla\Input\Input;
13
14 class FoosController extends AdminController
15 {
16 public function __construct($config = [], MVCFactoryInterface
$factory = null, $app = null, $input = null)
17 {
18 parent::__construct($config, $factory, $app, $input);
19 }
20
21 public function getModel($name = 'Foo', $prefix = 'Administrator',
$config = ['ignore_request' => true])
22 {
23 return parent::getModel($name, $prefix, $config);
24 }
25 }
Three fields are added to the form. One, in which the status is set and two, through which a scheduled
publication is achieved with the help of a calendar.
administrator/components/com_foos/forms/foo.xml
1
2 hint="JFIELD_ALIAS_PLACEHOLDER"
3 />
4
5 + <field
6 + name="published"
7 + type="list"
8 + label="JSTATUS"
9 + default="1"
10 + id="published"
11 + class="custom-select-color-state"
12 + size="1"
13 + >
14 + <option value="1">JPUBLISHED</option>
15 + <option value="0">JUNPUBLISHED</option>
16 + <option value="2">JARCHIVED</option>
17 + <option value="-2">JTRASHED</option>
18 + </field>
19 +
20 + <field
21 + name="publish_up"
22 + type="calendar"
23 + label="COM_FOOS_FIELD_PUBLISH_UP_LABEL"
24 + translateformat="true"
25 + showtime="true"
26 + size="22"
27 + filter="user_utc"
28 + />
29 +
30 + <field
31 + name="publish_down"
32 + type="calendar"
33 + label="COM_FOOS_FIELD_PUBLISH_DOWN_LABEL"
34 + translateformat="true"
35 + showtime="true"
36 + size="22"
37 + filter="user_utc"
38 + />
39 +
40 <field
41 name="catid"
42 type="categoryedit"
administrator/components/com_foos/sql/install.mysql.utf8.sql
The component class receives the new function “getStateColumnForSection”. This is used to show in
the category view how many items are published or hidden. Remember. We introduced categories in
the previous part. Then this part did not work in the category view. Now it is counted correctly. See for
yourself after you have added this function to the component in Joomla.
administrator/components/com_foos/src/Extension/FoosComponent.php
We extend the model so that the information about the status is retrieved from the database when the
list view is created for the backend.
administrator/components/com_foos/src/Model/FoosModel.php
We need store($updateNulls = true) because the parent class Table sets the variable
$updateNulls to false. This causes form fields that hold the value null not to be changed in the
database. Most of the time this is correct. The most common case is probably that a value is not set
from the beginning and has not been changed in the form when editing the element. Because an
empty date field is stored in the database with null, it is necessary in our case to force the storage of
null values. This is done by setting the variable $updateNulls to true.
administrator/components/com_foos/src/Table/FooTable.php
15 +
16 + // Check the publish down date is not earlier than publish up.
17 + if ($this->publish_down > $this->_db->getNullDate() && $this->
publish_down < $this->publish_up) {
18 + $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH
'));
19 +
20 + return false;
21 + }
22 +
23 + // Set publish_up, publish_down to null if not set
24 + if (!$this->publish_up) {
25 + $this->publish_up = null;
26 + }
27 +
28 + if (!$this->publish_down) {
29 + $this->publish_down = null;
30 + }
31 +
32 + return true;
33 + }
34 +
35 + public function store($updateNulls = true)
36 + {
37 + return parent::store($updateNulls);
38 + }
39 }
In the form for editing an element, we make sure that the new fields are rendered.
administrator/components/com_foos/tmpl/foo/edit.php
Finally, we add to the overview list in the backend. We create a column for displaying the publication
status.
Are you wondering about the the tags <td> and <th>. This seems to be a mistake at first sight.
But it is correct. You can find more information in the Github-Issue 24546 a .
a
github.com/joomla/joomla-cms/pull/24546
administrator/components/com_foos/tmpl/foos/default.php
1. install your component in Joomla version 4 to test it: Copy the files in the administrator folder
to the administrator folder of your Joomla 4 installation.
2. the database has been changed again, so it is necessary to update it. Uninstalling and reinstalling
is time-consuming. That’s why I’ll tell you an easier method.
4. select your component and click on “Update Structure”. That was it! Now you have updated the
database.
5. open the view of your component in the administration area and make sure that you see a
column here that is overwritten with status. Click on an icon in it and change the status of the
corresponding element from “published” to “hidden” and vice versa.
6. open an element and check that the status is also editable in this view. It is also possible to
specify a date, so that items are hidden or published according to the date.
Custom Fields (Own Fields) are very popular since Joomla version 3.7. They offer many additional
possibilities. Therefore, there is no question that we integrate them into our component.
This part shows you how to program the support in the administration area. In the next chapter we will
integrate Custom Fields in the frontend.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t13...t14a
We have not created a new file in this part, we have only changed files.
18.1.2.1. administrator/components/com_foos/access.xml
administrator/components/com_foos/access.xml
18.1.2.2. administrator/components/com_foos/config.xml
The configuration config.xml uses a paramter to define whether the extension uses custom fields.
administrator/components/com_foos/config.xml
1 <option value="0">JNO</option>
2 <option value="1">JYES</option>
3 </field>
4 +
5 + <field
6 + name="custom_fields_enable"
7 + type="radio"
8 + label="JGLOBAL_CUSTOM_FIELDS_ENABLE_LABEL"
9 + layout="joomla.form.field.radio.switcher"
10 + default="1"
11 + >
12 + <option value="0">JNO</option>
13 + <option value="1">JYES</option>
14 + </field>
15 </fieldset>
16 <fieldset
17 name="permissions"
A tip for the type radio with the layout joomla.form.field.radio.switcher. Do you want to
determine yourself how the colours are set in the layout? Is it important to you that when you set the
option “yes”, the field is coloured green and when you set the option “no”, a grey background appears?
By default, Joomla colours the options based on the order of the options. Example: Your field looks
like the next picture with the following code.
18.1.2.3. administrator/components/com_foos/foos.xml
In the navigation menu on the left in the Joomla administration area we add two links. The first new
link leads to the view where custom fields are created for the component. The other one leads to the
view where field groups are created.
administrator/components/com_foos/foos.xml
1 <submenu>
2 <menu link="option=com_foos">COM_FOOS</menu>
3 <menu link="option=com_categories&extension=com_foos">
JCATEGORY</menu>
4 + <menu link="option=com_fields&context=com_foos.foo">
JGLOBAL_FIELDS</menu>
5 + <menu link="option=com_fields&view=groups&context=
com_foos.foo">JGLOBAL_FIELD_GROUPS</menu>
6 </submenu>
7 <files folder="administrator/components/com_foos">
8 <filename>access.xml</filename>
The form through which a Foo element can be edited now has tabs. To ensure that the data is not
lost within the session when switching between tabs, we change the loadFormData() method in
the file administrator/components/com_foos/src/Model/FooModel.php. It is not necessary
that we cache data ourselves. The method $app->getUserState() does this for us. At the same
time we make sure that a default value is set for the category if a new element is loaded and therefore
$this->getState('foo.id')== 0 equals true.
administrator/components/com_foos/src/Model/FooModel.php
1
2 {
3 $app = Factory::getApplication();
4
5 - $data = $this->getItem();
6 + // Check the session for previously entered form data.
7 + $data = $app->getUserState('com_foos.edit.foo.data', []);
8 +
9 + if (empty($data)) {
10 + $data = $this->getItem();
11 +
12 + // Prime some default values.
13 + if ($this->getState('foo.id') == 0) {
14 + $data->set('catid', $app->input->get('catid', $app->
getUserState('com_foos.foos.filter.category_id'), 'int'));
15 + }
16 + }
17
18 $this->preprocessData($this->typeAlias, $data);
To make editing the custom fields work the same way as in Joomla’s own extensions, we use UiTab1 .
$this->useCoreUI = true; ensures that the Helper2 flexibly provides the correct tab implemen-
tation.
1
libraries/src/html/helpers/uitab.php
2
layouts/joomla/edit/params.php#l20
A comparison between previously most used bootstrap.tab and uitab is provided by Pull
Request PR 21805a .
a
github.com/joomla/joomla-cms/pull/21805
administrator/components/com_foos/tmpl/foo/edit.php
1 use Joomla\CMS\Factory;
2 use Joomla\CMS\HTML\HTMLHelper;
3 use Joomla\CMS\Router\Route;
4 +use Joomla\CMS\Language\Text;
5 +use Joomla\CMS\Layout\LayoutHelper;
6
7 $app = Factory::getApplication();
8 $input = $app->input;
9
10 +$this->useCoreUI = true;
11 +
12 $wa = $this->document->getWebAssetManager();
13 $wa->useScript('keepalive')
14 ->useScript('form.validate')
15 ?>
16
17 <form action="<?php echo Route::_('index.php?option=com_foos&layout='
. $layout . $tmpl . '&id=' . (int) $this->item->id); ?>" method="
post" name="adminForm" id="foo-form" class="form-validate">
18 - <?php echo $this->getForm()->renderField('name'); ?>
19 - <?php echo $this->getForm()->renderField('alias'); ?>
20 - <?php echo $this->getForm()->renderField('access'); ?>
21 - <?php echo $this->getForm()->renderField('catid'); ?>
22 - <?php echo $this->getForm()->renderField('published'); ?>
23 - <?php echo $this->getForm()->renderField('publish_up'); ?>
24 - <?php echo $this->getForm()->renderField('publish_down'); ?>
25 + <div>
26 + <?php echo HTMLHelper::_('uitab.startTabSet', 'myTab', ['active
' => 'details']); ?>
27 +
28 + <?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'details',
empty($this->item->id) ? Text::_('COM_FOOS_NEW_FOO') : Text::_('
COM_FOOS_EDIT_FOO')); ?>
29 + <div class="row">
30 + <div class="col-md-9">
31 + <div class="row">
32 + <div class="col-md-6">
33 + <?php echo $this->getForm()->renderField('name'
); ?>
34 + <?php echo $this->getForm()->renderField('alias
'); ?>
35 + <?php echo $this->getForm()->renderField('
access'); ?>
1. install your component in Joomla version 4 to test it: Copy the files in the administrator
folder into the administrator folder of your Joomla 4 installation. A new installation is not
necessary. Continue using the files from the previous part.
2. Open the view of your component in the administration area. You will see entries in the navigation
on the left. Click on the menu item “Fields” in this new menu.
4. make sure that when you edit a foo item, this custom field is also offered for editing.
5. make sure that the custom fields can be turned on and off in the global configuration.
Very few use custom fields only in the administration area. As usual, an output in the frontend is
required. We will address this question in the current part of the article series. How and where are
custom fields in Joomla displayed in the frontend?
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t14a...t14b
19.1.2.1. components/com_foos/src/View/Foo/HtmlView.php
Custom Fields display data in the frontend using events. The custom fields are displayed in three
different places on the website. By default, the data is displayed before the content. This setting can
be changed. Therefore we save the results of onContentAfterTitle, onContentBeforeDisplay
and onContentAfterDisplay. We do this in the View.
• onContentAfterTitle1 ,
• onContentBeforeDisplay2 und
• onContentAfterDisplay3 are triggered and the result is stored in a variable.
1
docs.joomla.org/Plugin/Events/Content#onContentAfterTitle
2
docs.joomla.org/Plugin/Events/Content#onContentBeforeDisplay
3
docs.joomla.org/Plugin/Events/Content#onContentAfterDisplay
Joomla uses the observer design patterna for event handling. This is a software design pattern
where an object maintains a list of its dependents called observers and automatically notifies
them of state changes, usually by calling one of their methods.
a
en.wikipedia.org/wiki/Observer_pattern
components/com_foos/src/View/Foo/HtmlView.php
1 \defined('_JEXEC') or die;
2
3 use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
4 +use Joomla\CMS\Factory;
5
6 class HtmlView extends BaseHtmlView
7 public function display($tpl = null)
8 {
9 - $this->item = $this->get('Item');
10 + $item = $this->item = $this->get('Item');
11 +
12 + Factory::getApplication()->triggerEvent('onContentPrepare', ['
com_foos.foo', &$item, &$item->params]);
13 +
14 + // Store the events for later
15 + $item->event = new \stdClass;
16 + $results = Factory::getApplication()->triggerEvent('
onContentAfterTitle', ['com_foos.foo', &$item, &$item->params]);
17 + $item->event->afterDisplayTitle = trim(implode("\n", $results))
;
18 +
19 + $results = Factory::getApplication()->triggerEvent('
onContentBeforeDisplay', ['com_foos.foo', &$item, &$item->params]);
20 + $item->event->beforeDisplayContent = trim(implode("\n",
$results));
21 +
22 + $results = Factory::getApplication()->triggerEvent('
onContentAfterDisplay', ['com_foos.foo', &$item, &$item->params]);
23 + $item->event->afterDisplayContent = trim(implode("\n", $results
));
24
25 return parent::display($tpl);
26 }
Are you wondering why we set &$item->params as parameters for the event methods
• onContentPrepare,
• onContentAfterTitle,
• onContentBeforeDisplay and
• onContentAfterDisplay,
although we have not yet explicitly implemented &$item->params in the Foo extension? Implicitly,
the populateState method of the file /components/com_foos/src/Model/FooModel.php en-
sures that &$item->params is available. For our example, we do not need this third parameter so far.
However, it is possible that errors may occur in combination with other extensions if this parameter is
not set. Therefore, we set the three mandatory parameters ['com_foos.foo', &$item, &$item
->params] for all event methods.
IIn the template we display our custom fields. In our case, this is not complex, so we write all the stored
texts one after the other. In a more complex file, the events are inserted at the correct place.
components/com_foos/tmpl/foo/default.php
1 }
2
3 echo $this->item->name;
4 +
5 +echo $this->item->event->afterDisplayTitle;
6 +echo $this->item->event->beforeDisplayContent;
7 +echo $this->item->event->afterDisplayContent;
Basically, all custom fields that belong to the current item are available in the template components/
com_foos/tmpl/foo/default.php. The call is made via a property of the variable $this->item
with the name jcfields. The variable $this->item->jcfields is an array which contains for
example the following data per field:
1 stdClass Object
2 (
3 [id] => 1
4 [alias] => nina
5 [publish_down] =>
6 [publish_up] =>
7 [published] => 0
8 [state] => 0
9 [catid] => 8
10 [access] => 1
11 [name] => Nina
12 [params] =>
13 [jcfields] => Array
14 (
15 [1] => stdClass Object
16 (
17 [id] => 1
18 [title] => Text Custom Field
19 [name] => text-custom-field
20 ...
21 )
22
23 )
24
25 [event] => stdClass Object
26 (
27 [afterDisplayTitle] =>
28 [beforeDisplayContent] =>
29
30
31
32 Text Custom Field:
33 Custom Field Value
34
35
36 [afterDisplayContent] =>
37 )
38
39 )
To add individual fields to the content, first select specific names for the custom fields. This way it is
possible to directly address the field in the override code of the file components/com_foos/tmpl
/foo/default.php using the field name. In the Joomla backend set the automatic display for the
field to No. This way you prevent it from being automatically displayed at one of the default positions.
Add the following code at the beginning of the template file components/com_foos/tmpl/foo/
default.php or its override to use direct access by name to fields in the overrides.
1 <?php
2 foreach($item->jcfields as $jcfield) {
3 $item->jcFields[$jcfield->name] = $jcfield;
4 }
5 ?>
Finally, add the insert statement for the custom field where you want the field label or value to appear.
1. install your component in Joomla version 4 to test it: Copy the files in the components folder
into the components folder of your Joomla 4 installation. A new installation is not necessary.
Continue using the files from the previous part.
3. after that create a custom field of type “text”, if you didn’t do it in the previous chapter.
4. edit a published foo item. Make sure you add a value to the custom field.
Figure 19.1.: Integrate Joomla Custom Fields into a custom component | Assign a value to the field in
the backend.
5. at the end open the detail view of the just edited Foo item. Create a menu item for this. You will
see next to the previously existing values now additionally the text you entered in the custom
field.
Figure 19.2.: Integrate Joomla Custom Fields into a custom component | Display value of the field in
the frontend.
With Joomla it is possible to set up a multilingual website without installing third party extensions. In
this tutorial, I’ll show you how to program your component to support Joomlas multilingual feature.
Multilingualism and language links: Multilingual content, menu items and language switches are
set up with a standard Joomla installation without any additional extensions. Until version 3.7,
Joomla required switching between views to translate content. Since 3.7 there is an improvement
in usability, the so-called Multilingual Associations. With this extension, multilingual content can
be created and linked in a user-friendly way. Thereby one remains in one view. The language links
show incidentally which multilingual content is missing.
The chapter is one of the most extensive in this series. For that it covers all areas of multilingualism
and language links in Joomla.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t14b...t15a
So that the language is saved to the element, we add a column to the database table. When updating
the component, the script 15.0.0.sql is the one that is executed for version 15.0.0.
administrator/components/com_foos/sql/updates/mysql/15.0.0.sql
1
2 ALTER TABLE `#__foos_details ` ADD COLUMN `language ` char(7) NOT NULL
DEFAULT '*' AFTER `alias`;
3
The helper file AssociationsHelper.php is the interface to the language associations component
com_associations. AssociationsHelper.php exists in the frontend and the backend - we’ll look
at the latter first, the frontend version comes later in this chapter.
In this helper file we provide the details that are specific to our component, so that Joomla’s own
routines can find their way around in our component.
administrator/components/com_foos/src/Helper/AssociationsHelper.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Administrator\Helper;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\Association\AssociationExtensionHelper;
9 use Joomla\CMS\Language\Associations;
10 use Joomla\CMS\Table\Table;
11 use FooNamespace\Component\Foos\Site\Helper\AssociationHelper;
12
13 class AssociationsHelper extends AssociationExtensionHelper
14 {
15 protected $extension = 'com_foos';
16
17 protected $itemTypes = ['foo', 'category'];
18
19 protected $associationsSupport = true;
20
21 public function getAssociationsForItem($id = 0, $view = null)
22 {
23 return AssociationHelper::getAssociations($id, $view);
24 }
25
26 public function getAssociations($typeName, $id)
27 {
28 $type = $this->getType($typeName);
29
30 $context = $this->extension . '.item';
31 $catidField = 'catid';
32
33 if ($typeName === 'category') {
34 $context = 'com_categories.item';
35 $catidField = '';
36 }
37
38 // Get the associations.
39 $associations = Associations::getAssociations(
40 $this->extension,
41 $type['tables']['a'],
42 $context,
43 $id,
44 'id',
45 'alias',
46 $catidField
47 );
48
49 return $associations;
50 }
51
52 public function getItem($typeName, $id)
53 {
54 if (empty($id)) {
55 return null;
56 }
57
58 $table = null;
59
60 switch ($typeName) {
61 case 'foo':
62 $table = Table::getInstance('FooTable', 'FooNamespace\\
Component\\Foos\\Administrator\\Table\\');
63 break;
64
65 case 'category':
66 $table = Table::getInstance('Category');
67 break;
68 }
69
70 if (empty($table)) {
71 return null;
72 }
73
74 $table->load($id);
75
76 return $table;
77 }
78
79 public function getType($typeName = '')
80 {
81 $fields = $this->getFieldsTemplate();
82 $tables = [];
83 $joins = [];
84 $support = $this->getSupportTemplate();
85 $title = '';
86
87 if (in_array($typeName, $this->itemTypes)) {
88 switch ($typeName) {
89 case 'foo':
90 $fields['title'] = 'a.name';
91 $fields['state'] = 'a.published';
92
93 $support['state'] = true;
94 $support['acl'] = true;
95 $support['category'] = true;
96 $support['save2copy'] = true;
97
98 $tables = [
99 'a' => '#__foos_details'
100 ];
101
102 $title = 'foo';
103 break;
104
105 case 'category':
106 $fields['created_user_id'] = 'a.created_user_id';
107 $fields['ordering'] = 'a.lft';
108 $fields['level'] = 'a.level';
109 $fields['catid'] = '';
110 $fields['state'] = 'a.published';
111
112 $support['state'] = true;
113 $support['acl'] = true;
114 $support['checkout'] = false;
115 $support['level'] = false;
116
117 $tables = [
118 'a' => '#__categories'
119 ];
120
121 $title = 'category';
122 break;
123 }
124 }
125
126 return [
127 'fields' => $fields,
128 'support' => $support,
129 'tables' => $tables,
130 'joins' => $joins,
131 'title' => $title
132 ];
133 }
134
135 protected function getFieldsTemplate()
136 {
137 return [
138 'id' => 'a.id',
139 'title' => 'a.title',
140 'alias' => 'a.alias',
141 'ordering' => 'a.id',
142 'menutype' => '',
143 'level' => '',
144 'catid' => 'a.catid',
145 'language' => 'a.language',
146 'access' => 'a.access',
147 'state' => 'a.state',
148 'created_user_id' => '',
149 'checked_out' => '',
150 'checked_out_time' => ''
151 ];
152 }
153 }
20.1.1.3. components/com_foos/src/Helper/AssociationHelper.php
The AssociationsHelper.php helper file is the interface to the com_associations language as-
sociations component. In it we configure the information that is specific to our component. Once this
is done, Joomla’s own routines take over and we don’t reinvent the wheel.
Attention: I had already written it: The class AssociationsHelper.php exists in the frontend
and in the backend: src/components/com_foos/src/Helper/AssociationHelper.php
and src/ administrator /components/com_foos/src/Helper/AssociationHelper.
php. We had already looked at the file for the backend before.
components/com_foos/src/Helper/AssociationHelper.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Site\Helper;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\Factory;
9 use Joomla\CMS\Language\Associations;
10 use Joomla\Component\Categories\Administrator\Helper\
CategoryAssociationHelper;
11 use FooNamespace\Component\Foos\Site\Helper\RouteHelper;
12
13 abstract class AssociationHelper extends CategoryAssociationHelper
14 {
15 public static function getAssociations($id = 0, $view = null)
16 {
17 $jinput = Factory::getApplication()->input;
18 $view = $view ?? $jinput->get('view');
19 $id = empty($id) ? $jinput->getInt('id') : $id;
20
21 if ($view === 'foos') {
22 if ($id) {
23 $associations = Associations::getAssociations('com_foos
', '#__foos_details', 'com_foos.item', $id);
24
25 $return = [];
26
27 foreach ($associations as $tag => $item) {
28 $return[$tag] = RouteHelper::getFoosRoute($item->id
, (int) $item->catid, $item->language);
29 }
30
31 return $return;
32 }
33 }
34
35 if ($view === 'category' || $view === 'categories') {
36 return self::getCategoryAssociations($id, 'com_foos');
37 }
38
39 return [];
40 }
41 }
20.1.1.4. components/com_foos/src/Helper/RouteHelper.php
We create the class RouteHelper to correctly compose the links we create in this chapter. Within the
link there is one more piece of information as a parameter: the language.
components/com_foos/src/Helper/RouteHelper.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Site\Helper;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\Categories\CategoryNode;
9 use Joomla\CMS\Language\Multilanguage;
10
11 abstract class RouteHelper
12 {
13 public static function getFoosRoute($id, $catid, $language = 0)
14 {
15 // Create the link
16 $link = 'index.php?option=com_foos&view=foos&id=' . $id;
17
18 if ($catid > 1) {
19 $link .= '&catid=' . $catid;
20 }
21
22 if ($language && $language !== '*' && Multilanguage::isEnabled
()) {
23 $link .= '&lang=' . $language;
24 }
25
26 return $link;
27 }
28
29 public static function getFooRoute($id, $catid, $language = 0)
30 {
31 // Create the link
32 $link = 'index.php?option=com_foos&view=foo&id=' . $id;
33
34 if ($catid > 1) {
35 $link .= '&catid=' . $catid;
36 }
37
38 if ($language && $language !== '*' && Multilanguage::isEnabled
()) {
39 $link .= '&lang=' . $language;
40 }
41
42 return $link;
43 }
44
45 public static function getCategoryRoute($catid, $language = 0)
46 {
47 if ($catid instanceof CategoryNode) {
48 $id = $catid->id;
49 } else {
50 $id = (int) $catid;
51 }
52
53 if ($id < 1) {
54 $link = '';
55 } else {
56 // Create the link
57 $link = 'index.php?option=com_foos&view=category&id=' . $id
;
58
59 if ($language && $language !== '*' && Multilanguage::
isEnabled()) {
60 $link .= '&lang=' . $language;
61 }
62 }
63
64 return $link;
65 }
66 }
We create a field through which an author selects the language link. This is the field name="language".
In order for Joomla to find this field, we add the path in the form addfieldprefix= "FooNamespace
\Component\Foos\Administrator\Field" as a parameter in the <fieldset>.
administrator/components/com_foos/forms/foo.xml
available.
administrator/components/com_foos/services/provider.php
1 use Joomla\DI\Container;
2 use Joomla\DI\ServiceProviderInterface;
3 use FooNamespace\Component\Foos\Administrator\Extension\FoosComponent;
4 +use FooNamespace\Component\Foos\Administrator\Helper\
AssociationsHelper;
5 +use Joomla\CMS\Association\AssociationExtensionInterface;
6
7
8 public function register(Container $container)
9 {
10 + $container->set(AssociationExtensionInterface::class, new
AssociationsHelper);
11 +
12 $container->registerServiceProvider(new CategoryFactory('\\
FooNamespace\\Component\\Foos'));
13 $container->registerServiceProvider(new MVCFactory('\\
FooNamespace\\Component\\Foos'));
14 $container->registerServiceProvider(new
ComponentDispatcherFactory('\\FooNamespace\\Component\\Foos'
));
15 function (Container $container) {
16 $component->setRegistry($container->get(Registry::class
));
17 $component->setMVCFactory($container->get(
MVCFactoryInterface::class));
18 $component->setCategoryFactory($container->get(
CategoryFactoryInterface::class));
19 + $component->setAssociationExtension($container->get(
AssociationExtensionInterface::class));
20
21 return $component;
22 }
administrator/components/com_foos//install.mysql.utf8.sql
In order for the language to be saved to the element, we add a column in the database table. For new
installations, the script install.mysql.utf8.sql is the one that is called.
1
libraries/src/association/associationextensioninterface.php
Traits are a code reuse mechanism used in programming languages with simple inheritance like
PHP.
administrator/components/com_foos/src/Extension/FoosComponent.php
1
2
3 defined('JPATH_PLATFORM') or die;
4
5 +use Joomla\CMS\Association\AssociationServiceInterface;
6 +use Joomla\CMS\Association\AssociationServiceTrait;
7 use Joomla\CMS\Categories\CategoryServiceInterface;
8 use Joomla\CMS\Categories\CategoryServiceTrait;
9 use Joomla\CMS\Extension\BootableExtensionInterface;
10
11 -class FoosComponent extends MVCComponent implements
BootableExtensionInterface, CategoryServiceInterface
12 +class FoosComponent extends MVCComponent implements
BootableExtensionInterface, CategoryServiceInterface,
AssociationServiceInterface
13 {
14 use CategoryServiceTrait;
15 + use AssociationServiceTrait;
16 use HTMLRegistryAwareTrait;
We previously used the modal to select a Foo item using a popup when creating a menu item. Now we
use it again to select a language link. To make sure that only the matching languages are displayed, we
extend the URL with the language information.
administrator/components/com_foos/src/Field/Modal/FooField.php
Are you confused by the characters —a or &b ? They are quite harmless. —
is nothing more than a dash[en.wikipedia.org/wiki/Dash#En_dash] -. & stands for the
ampersand character &. In HTML, the latter stands for the beginning of an entity reference. Thus
it is a special character. If you use such a character in a text that is checked for security reasons,
you should use the encoded entity & - more technical stuff on w3c.orgc . For the dash -, we
use Unicoded . The goal in this case is to unify the use of different and incompatible encodings in
different countries or cultures.
a
unicode-table.com/en/2014/
b
unicode-table.com/de/0026/
c
w3.org/tr/xhtml1/guidelines.html#c_12
d
en.wikipedia.org/wiki/unicode
administrator/components/com_foos/src/Model/FooModel.php
1 \defined('_JEXEC') or die;
2
3 use Joomla\CMS\Factory;
4 +use Joomla\CMS\Language\Associations;
5 use Joomla\CMS\MVC\Model\AdminModel;
6 +use Joomla\CMS\Language\LanguageHelper;
7
8 class FooModel extends AdminModel
9 public $typeAlias = 'com_foos.foo';
10
11 + protected $associationsContext = 'com_foos.item';
12 +
13 protected function loadFormData()
14 return $data;
15 }
16
17 + public function getItem($pk = null)
18 + {
19 + $item = parent::getItem($pk);
20 +
21 + // Load associated foo items
22 + $assoc = Associations::isEnabled();
23 +
24 + if ($assoc) {
25 + $item->associations = [];
26 +
27 + if ($item->id != null) {
28 + $associations = Associations::getAssociations('com_foos
', '#__foos_details', 'com_foos.item', $item->id, 'id', null);
29 +
30 + foreach ($associations as $tag => $association) {
31 + $item->associations[$tag] = $association->id;
32 + }
33 + }
34 + }
35 +
36 + return $item;
37 + }
38 +
39 + protected function preprocessForm(\JForm $form, $data, $group = '
content')
40 + {
41 + if (Associations::isEnabled()) {
42 + $languages = LanguageHelper::getContentLanguages(false,
true, null, 'ordering', 'asc');
43 +
44 + if (count($languages) > 1) {
45 + $addform = new \SimpleXMLElement('<form />');
46 + $fields = $addform->addChild('fields');
47 + $fields->addAttribute('name', 'associations');
48 + $fieldset = $fields->addChild('fieldset');
49 + $fieldset->addAttribute('name', 'item_associations');
50 +
51 + foreach ($languages as $language) {
52 + $field = $fieldset->addChild('field');
53 + $field->addAttribute('name', $language->lang_code);
54 + $field->addAttribute('type', 'modal_foo');
55 + $field->addAttribute('language', $language->
lang_code);
56 + $field->addAttribute('label', $language->title);
57 + $field->addAttribute('translate_label', 'false');
58 + $field->addAttribute('select', 'true');
59 + $field->addAttribute('new', 'true');
60 + $field->addAttribute('edit', 'true');
61 + $field->addAttribute('clear', 'true');
62 + }
63 +
64 + $form->load($addform, false);
65 + }
66 + }
67 +
68 + parent::preprocessForm($form, $data, $group);
69 + }
70 +
Note: FooModel.php is the model which calculates the data for an element. FoosModel.php -
note the s - is the list view model - it handles data for a group of elements.
In the model of the list, besides adding the language information, it is important to update the state via
populateState. Otherwise the correct language will not be active at each time. The state includes
the information which language is active.
administrator/components/com_foos/src/Model/FoosModel.php
1 \defined('_JEXEC') or die;
2
3 use Joomla\CMS\MVC\Model\ListModel;
4 +use Joomla\CMS\Language\Associations;
5 +use Joomla\CMS\Factory;
6
7
8
9 // Select the required fields from the table.
10 $query->select(
11 - $db->quoteName(array('a.id', 'a.name', 'a.alias', 'a.access
', 'a.catid', 'a.published', 'a.publish_up', 'a.publish_down'))
12 + $db->quoteName(
13 + array(
14 + 'a.id', 'a.name', 'a.alias', 'a.access',
15 + 'a.catid', 'a.published', 'a.publish_up', 'a.
publish_down',
16 + 'a.language'
17 + )
18 + )
19 );
20
21 $query->from($db->quoteName('#__foos_details', 'a'));
22
23 $db->quoteName('#__categories', 'c') . ' ON ' . $db->
quoteName('c.id') . ' = ' . $db->quoteName('a.catid'
)
24 );
25
26 + // Join over the language
27 + $query->select($db->quoteName('l.title', 'language_title'))
28 + ->select($db->quoteName('l.image', 'language_image'))
29 + ->join(
30 + 'LEFT',
31 + $db->quoteName('#__languages', 'l') . ' ON ' . $db->
quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')
32 + );
33 +
34 + // Join over the associations.
35 + if (Associations::isEnabled())
36 + {
37 + $subQuery = $db->getQuery(true)
38 + ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1
')
39 + ->from($db->quoteName('#__associations', 'asso1'))
40 + ->join('INNER', $db->quoteName('#__associations', '
asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2
.key'))
41 + ->where(
42 + [
43 + $db->quoteName('asso1.id') . ' = ' . $db->
quoteName('a.id'),
44 + $db->quoteName('asso1.context') . ' = ' . $db->
quote('com_foos.item'),
45 + ]
46 + );
47 +
48 + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('
association'));
49 + }
50 +
51 + // Filter on the language.
52 + if ($language = $this->getState('filter.language'))
53 + {
54 + $query->where($db->quoteName('a.language') . ' = ' . $db->
quote($language));
55 + }
56 +
57 return $query;
58 }
59 +
60 + protected function populateState($ordering = 'a.name', $direction =
'asc')
61 + {
62 + $app = Factory::getApplication();
63 + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd')
;
64 +
65 + // Adjust the context to support modal layouts.
66 + if ($layout = $app->input->get('layout'))
67 + {
68 + $this->context .= '.' . $layout;
69 + }
70 +
71 + // Adjust the context to support forced languages.
72 + if ($forcedLanguage)
73 + {
74 + $this->context .= '.' . $forcedLanguage;
75 + }
76 +
77 + // List state information.
78 + parent::populateState($ordering, $direction);
79 +
80 + // Force a language.
81 + if (!empty($forcedLanguage))
82 + {
83 + $this->setState('filter.language', $forcedLanguage);
84 + }
85 + }
86 }
administrator/components/com_foos/src/Service/HTML/AdministratorService.php
1
2 defined('JPATH_BASE') or die;
3
4 +use Joomla\CMS\Factory;
5 +use Joomla\CMS\Language\Associations;
6 +use Joomla\CMS\Language\Text;
7 +use Joomla\CMS\Layout\LayoutHelper;
8 +use Joomla\CMS\Router\Route;
9
10 class AdministratorService
11 {
12
13 + public function association($fooid)
14 + {
15 + // Defaults
16 + $html = '';
17 +
18 + // Get the associations
19 + if ($associations = Associations::getAssociations('com_foos', '
#__foos_details', 'com_foos.item', $fooid, 'id', null)) {
20 + foreach ($associations as $tag => $associated) {
21 + $associations[$tag] = (int) $associated->id;
22 + }
23 +
24 + // Get the associated foo items
25 + $db = Factory::getDbo();
26 + $query = $db->getQuery(true)
27 + ->select('c.id, c.name as title')
28 + ->select('l.sef as lang_sef, lang_code')
29 + ->from('#__foos_details as c')
30 + ->select('cat.title as category_title')
31 + ->join('LEFT', '#__categories as cat ON cat.id=c.catid'
)
32 + ->where('c.id IN (' . implode(',', array_values(
$associations)) . ')')
33 + ->where('c.id != ' . $fooid)
34 + ->join('LEFT', '#__languages as l ON c.language=l.
lang_code')
35 + ->select('l.image')
36 + ->select('l.title as language_title');
37 + $db->setQuery($query);
38 +
39 + try {
40 + $items = $db->loadObjectList('id');
41 + } catch (\RuntimeException $e) {
42 + throw new \Exception($e->getMessage(), 500, $e);
43 + }
44 +
45 + if ($items) {
46 + foreach ($items as &$item) {
47 + $text = strtoupper($item->lang_sef);
48 + $url = Route::_('index.php?option=com_foos&task=foo
.edit&id=' . (int) $item->id);
49 + $tooltip = '<strong>' . htmlspecialchars($item->
language_title, ENT_QUOTES, 'UTF-8') . '</strong><br>'
50 + . htmlspecialchars($item->title, ENT_QUOTES, '
UTF-8') . '<br>' . Text::sprintf('JCATEGORY_SPRINTF', $item->
category_title);
51 + $classes = 'badge bg-secondary';
52 +
If only one language is possible or changing it is not desired, we set the value of the language selection
field and protected it from write access. Also, only categories of this language are selectable.
administrator/components/com_foos/src/View/Foo/HtmlView.php
1 $this->form = $this->get('Form');
2 $this->item = $this->get('Item');
3
4 + // If we are forcing a language in modal (used for associations
).
5 + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory
::getApplication()->input->get('forcedLanguage', '', 'cmd')) {
6 + // Set the language field to the forcedLanguage and disable
changing it.
7 + $this->form->setValue('language', null, $forcedLanguage);
8 + $this->form->setFieldAttribute('language', 'readonly', '
true');
9 +
10 + // Only allow to select categories with All language or
with the forced language.
11 + $this->form->setFieldAttribute('catid', 'language', '*,' .
$forcedLanguage);
12 + }
13 +
14 $this->addToolbar();
15
16 return parent::display($tpl);
The view of the list should contain the sidebar and the toolbar if it is not a modal view or a popup. If
the view is modal, the toolbar and sidebar will confuse. In that case we filter the items automatically
according to the currently active language.
administrator/components/com_foos/src/View/Foos/HtmlView.php
In the form for editing an element we add a form field for specifying the language. For this we use the lay-
out administrator/components/com_foos/tmpl/foo/edit_associations.php created ear-
lier in this part.
Why the layout edit_associations.php is called in the file edit.php with the name
associations, you might already think. In the part about the layouts, I go into this in more
detail.
administrator/components/com_foos/tmpl/foo/edit.php
1 use Joomla\CMS\Factory;
2 use Joomla\CMS\HTML\HTMLHelper;
3 +use Joomla\CMS\Language\Associations;
4 use Joomla\CMS\Router\Route;
5 use Joomla\CMS\Language\Text;
6 use Joomla\CMS\Layout\LayoutHelper;
7
8 $app = Factory::getApplication();
9 $input = $app->input;
10
11 +$assoc = Associations::isEnabled();
12 +
13 +$this->ignore_fieldsets = ['item_associations'];
14 $this->useCoreUI = true;
15
16 +$isModal = $input->get('layout') === 'modal';
17
18 $wa = $this->document->getWebAssetManager();
19
20 <?php echo $this->getForm()->renderField('
publish_up'); ?>
21 <?php echo $this->getForm()->renderField('
publish_down'); ?>
22 <?php echo $this->getForm()->renderField('catid
'); ?>
23 + <?php echo $this->getForm()->renderField('
language'); ?>
24 </div>
25 </div>
26 </div>
27 </div>
28 <?php echo HTMLHelper::_('uitab.endTab'); ?>
29
30 + <?php if (!$isModal && $assoc) : ?>
31 + <?php echo HTMLHelper::_('uitab.addTab', 'myTab
', 'associations', Text::_('JGLOBAL_FIELDSET_ASSOCIATIONS')); ?>
32 + <fieldset id="fieldset-associations" class="
options-form">
33 + <legend><?php echo Text::_('
JGLOBAL_FIELDSET_ASSOCIATIONS'); ?></legend>
34 + <div>
35 + <?php echo LayoutHelper::render
('joomla.edit.associations', $this); ?>
36 + </div>
37 + </fieldset>
38 + <?php echo HTMLHelper::_('uitab.endTab'); ?>
39 + <?php elseif ($isModal && $assoc) : ?>
40 + <div class="hidden"><?php echo LayoutHelper::
render('joomla.edit.associations', $this); ?></div>
41 + <?php endif; ?>
42
43 <?php echo LayoutHelper::render('joomla.edit.params', $this);
?>
44
45 <?php echo HTMLHelper::_('uitab.endTabSet'); ?>
In the components overview in the administration area, we add columns to display the language infor-
mation. We display these columns only when it is required. This is the case when language associations
and multilingualism are enabled. To find this out we use Joomla’s own functions Associations::
isEnabled() and Multilanguage::isEnabled().
administrator/components/com_foos/tmpl/foos/default.php
1 use Joomla\CMS\HTML\HTMLHelper;
2 use Joomla\CMS\Language\Text;
3 use Joomla\CMS\Router\Route;
4 +use Joomla\CMS\Language\Multilanguage;
5 +use Joomla\CMS\Language\Associations;
6 +use Joomla\CMS\Layout\LayoutHelper;
7 +
8 +$assoc = Associations::isEnabled();
9 +
10 ?>
11 <form action="<?php echo Route::_('index.php?option=com_foos'); ?>"
method="post" name="adminForm" id="adminForm">
12 <div class="row">
13
14 <th scope="col" style="width:10%" class
="d-none d-md-table-cell">
15 <?php echo TEXT::_('
JGRID_HEADING_ACCESS') ?>
16 </th>
17 + <?php if ($assoc) : ?>
18 + <th scope="col" style="width:10%">
19 + <?php echo Text::_('
COM_FOOS_HEADING_ASSOCIATION'); ?>
20 + </th>
21 + <?php endif; ?>
22 + <?php if (Multilanguage::isEnabled()) :
?>
1. install your component in Joomla version 4 to test it: Copy the files in the administrator folder
into the administrator folder of your Joomla 4 installation. Copy the files in the components
folder into the components folder of your Joomla 4 installation.
2. the database has been changed, so it is necessary to update it. Open the System |
3. install at least one more language via System | Install | Languages. I chose the german
and the persian language.
Persiana is together with Arabic, Hebrew, Pashto, Urdu, and Sindhi one of the most widely used
RTLb writing systems of modern times and can therefore be used to test the RTL integration in
Joomla. In a right-to-left, top-to-bottom scriptc (often abbreviated as right-to-left or RTL), one
writes from right to left on a page, with new lines written from top to bottom. This is in contrast to
the left-to-right writing system, where writing starts from the left and continues to the right.
a
wikipedia.org/wiki/persian_language
b
wikipedia.org/wiki/right-to-left_script
c
4. make sure via System | Manage | Plugins that the plugin System - Language Filter
is published.
5. open the view of an item of your component in the administration area and make sure that the
Language is editable. Change it from All to any language.
7. play with the language associations and make sure that everything is associated correctly. Pay
attention to the settings for the assigned categories.
8. extend the tests to the component “Multilingual Associations”. This supports your extension as
well.
Filtering, sorting and searching - now we organize the Joomla 4 component! Joomla offers view
filters and search tools with which you can limit the number of visible items. If the status filter is set
accordingly, only items whose status is published will be displayed. Beside the status filter the search
tools offer the search by title or content and the possibility to sort the table, i.e. to change the order.
For impatient people: Look at the changed program code in the Diff viewa and take over these
changes into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t15a...t16
First, we create the form through which the filters will be set.
administrator/components/com_foos/forms/filter_foos.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <form>
4
5 <fields name="filter">
6
7 <field
8 name="search"
9 type="text"
10 inputmode="search"
11 label="COM_FOOS_FILTER_SEARCH_LABEL"
12 description="COM_FOOS_FILTER_SEARCH_DESC"
13 hint="JSEARCH_FILTER"
14 />
15
16 <field
17 name="featured"
18 type="list"
19 onchange="this.form.submit();"
20 default=""
21 >
22 <option value="">JOPTION_SELECT_FEATURED</option>
23 <option value="0">JUNFEATURED</option>
24 <option value="1">JFEATURED</option>
25 </field>
26
27 <field
28 name="published"
29 type="status"
30 label="JOPTION_SELECT_PUBLISHED"
31 onchange="this.form.submit();"
32 >
33 <option value="">JOPTION_SELECT_PUBLISHED</option>
34 </field>
35
36 <field
37 name="category_id"
38 type="category"
39 label="JCATEGORY"
40 multiple="true"
41 extension="com_foos"
42 layout="joomla.form.field.list-fancy-select"
43 hint="JOPTION_SELECT_CATEGORY"
44 onchange="this.form.submit();"
45 published="0,1,2"
46 />
47
48 <field
49 name="access"
50 type="accesslevel"
51 label="JOPTION_SELECT_ACCESS"
52 onchange="this.form.submit();"
53 >
54 <option value="">JOPTION_SELECT_ACCESS</option>
55 </field>
56
57 <field
58 name="language"
59 type="contentlanguage"
60 label="JOPTION_SELECT_LANGUAGE"
61 onchange="this.form.submit();"
62 >
63 <option value="">JOPTION_SELECT_LANGUAGE</option>
64 <option value="*">JALL</option>
65 </field>
66
67 </fields>
68
69 <fields name="list">
70
71 <field
72 name="fullordering"
73 type="list"
74 label="JGLOBAL_SORT_BY"
75 default="a.name ASC"
76 onchange="this.form.submit();"
77 >
78 <option value="">JGLOBAL_SORT_BY</option>
79 <option value="a.ordering ASC">JGRID_HEADING_ORDERING_ASC</
option>
80 <option value="a.ordering DESC">JGRID_HEADING_ORDERING_DESC
</option>
81 <option value="a.published ASC">JSTATUS_ASC</option>
82 <option value="a.published DESC">JSTATUS_DESC</option>
83 <option value="a.name ASC">JGLOBAL_TITLE_ASC</option>
84 <option value="a.name DESC">JGLOBAL_TITLE_DESC</option>
85 <option value="category_title ASC">JCATEGORY_ASC</option>
86 <option value="category_title DESC">JCATEGORY_DESC</option>
87 <option value="access_level ASC">JGRID_HEADING_ACCESS_ASC</
option>
88 <option value="access_level DESC">JGRID_HEADING_ACCESS_DESC
</option>
89 <option value="association ASC" requires="associations">
JASSOCIATIONS_ASC</option>
90 <option value="association DESC" requires="associations">
JASSOCIATIONS_DESC</option>
91 <option value="language_title ASC" requires="multilanguage"
>JGRID_HEADING_LANGUAGE_ASC</option>
92 <option value="language_title DESC" requires="multilanguage
">JGRID_HEADING_LANGUAGE_DESC</option>
93 <option value="a.id ASC">JGRID_HEADING_ID_ASC</option>
94 <option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
95 </field>
96
97 <field
98 name="limit"
99 type="limitbox"
100 label="JGLOBAL_LIST_LIMIT"
101 default="25"
102 onchange="this.form.submit();"
103 />
104 </fields>
105 </form>
featured is included here as a filter field for the sake of completeness, although we do not
support this in the extension yet.
In case of an update of your component, the file 16.0.0.sql adds a column to store the sequence.
administrator/components/com_foos/sql/updates/mysql/16.0.0.sql
1 -- https://codeberg.org/astrid/j4examplecode/raw/branch/t16/src/
administrator/components/com_foos/sql/updates/mysql/16.0.0.sql
2
3 ALTER TABLE `#__foos_details ` ADD COLUMN `ordering ` int(11) NOT NULL
DEFAULT 0 AFTER `alias`;
The form used to create or modify an element is extended with a field for specifying the order.
administrator/components/com_foos/forms/foo.xml
1 label="JFIELD_ACCESS_LABEL"
2 size="1"
3 />
4 +
5 + <field
6 + name="ordering"
7 + type="ordering"
8 + label="JFIELD_ORDERING_LABEL"
9 + content_type="com_foos.foo"
10 + />
11 </fieldset>
12 </form>
In case of a new installation, the script in the file install.mysql.utf8.sql creates the database.
Here we add a column to store the order.
administrator/components/com_foos/sql/install.mysql.utf8.sql
1
2 ALTER TABLE `#__foos_details ` ADD COLUMN `language ` char(7) NOT NULL
DEFAULT '*' AFTER `alias`;
3
4 ALTER TABLE `#__foos_details ` ADD KEY `idx_language ` ( `language`);
5 +
There are a lot of changes in the model for the list. In the constructor we first save the filter fields to the
configuration.
In the getListQuery() method we adjust the database query to respect the filters and sorting. This
way the data is immediately in the form in which we display it.
administrator/components/com_foos/src/Model/FoosModel.php
1 use Joomla\CMS\MVC\Model\ListModel;
2 use Joomla\CMS\Language\Associations;
3 use Joomla\CMS\Factory;
4 +use Joomla\Utilities\ArrayHelper;
5
6
7 public function __construct($config = array())
8 {
9 +
10 + if (empty($config['filter_fields']))
11 + {
12 + $config['filter_fields'] = array(
13 + 'id', 'a.id',
14 + 'name', 'a.name',
15 + 'catid', 'a.catid', 'category_id', 'category_title',
16 + 'published', 'a.published',
17 + 'access', 'a.access', 'access_level',
18 + 'ordering', 'a.ordering',
19 + 'language', 'a.language', 'language_title',
20 + 'publish_up', 'a.publish_up',
21 + 'publish_down', 'a.publish_down',
22 + );
23 +
24 + $assoc = Associations::isEnabled();
25 +
26 + if ($assoc)
27 + {
28 + $config['filter_fields'][] = 'association';
29 + }
30 + }
31 +
32 parent::__construct($config);
33 }
34
35 array(
36 'a.id', 'a.name', 'a.alias', 'a.access',
81 + {
82 + if (stripos($search, 'id:') === 0)
83 + {
84 + $query->where('a.id = ' . (int) substr($search, 3));
85 + }
86 + else
87 + {
88 + $search = $db->quote('%' . str_replace(' ', '%', $db->
escape(trim($search), true) . '%'));
89 + $query->where(
90 + '(' . $db->quoteName('a.name') . ' LIKE ' . $search
. ')'
91 + );
92 + }
93 + }
94 +
95 + // Add the list ordering clause.
96 + $orderCol = $this->state->get('list.ordering', 'a.name');
97 + $orderDirn = $this->state->get('list.direction', 'asc');
98 +
99 + if ($orderCol == 'a.ordering' || $orderCol == 'category_title')
100 + {
101 + $orderCol = $db->quoteName('c.title') . ' ' . $orderDirn .
', ' . $db->quoteName('a.ordering');
102 + }
103 +
104 + $query->order($db->escape($orderCol . ' ' . $orderDirn));
105 +
106 return $query;
107 }
administrator/components/com_foos/src/View/Foos/HtmlView.php
1 \defined('_JEXEC') or die;
2
3 +use Joomla\CMS\Component\ComponentHelper;
4 +use Joomla\CMS\Helper\ContentHelper;
5 +use Joomla\CMS\Language\Associations;
6 use Joomla\CMS\Factory;
7 use Joomla\CMS\Language\Text;
8 use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
9
10 use Joomla\CMS\Toolbar\ToolbarHelper;
11 use FooNamespace\Component\Foos\Administrator\Helper\FooHelper;
12 use Joomla\CMS\Factory;
13 +use Joomla\CMS\MVC\View\GenericDataException;
14
15
16 protected $items;
17
18 + protected $state;
19 +
20 + /**
21 + public $filterForm;
22 +
23 + public $activeFilters;
24 +
25
26 {
27 $this->items = $this->get('Items');
28
29 + $this->filterForm = $this->get('FilterForm');
30 + $this->activeFilters = $this->get('ActiveFilters');
31 + $this->state = $this->get('State');
32 +
33 + // Check for errors.
34 + if (count($errors = $this->get('Errors')))
35 + {
36 + throw new GenericDataException(implode("\n", $errors), 500)
;
37 + }
38 +
39 + // Preprocess the list of items to find ordering divisions.
40 + // TODO: Complete the ordering stuff with nested sets
41 + foreach ($this->items as &$item)
42 + {
43 + $item->order_up = true;
44 + $item->order_dn = true;
45 + }
46 +
47 // We don't need toolbar in the modal window.
48 if ($this->getLayout() !== 'modal')
49 {
50
51 {
52 // If the language is forced we can't allow to select
the language, so transform the language selector
filter into a hidden field.
53 $languageXml = new \SimpleXMLElement('<field name="
language" type="hidden" default="' . $forcedLanguage
. '" />');
54 + $this->filterForm->setField($languageXml, 'filter',
true);
55 +
The code below shows all the essentials for using searchtools in the list view of the backend.
In the case of the header, I replaced <?php echo TEXT::_('JGRID_HEADING_ACCESS')?>
with <?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ACCESS', '
access_level', $listDirn, $listOrder); ?>. This way the header of the table is marked
with a small arrow when a sort is active in a column.
The code, which enables selection and deselection of column views via the code snippet
1 $wa = $this->document->getWebAssetManager();
2 $wa->useScript('table.columns');
administrator/components/com_foos/tmpl/foos/default.php
1 use Joomla\CMS\Language\Multilanguage;
2 use Joomla\CMS\Language\Associations;
3 use Joomla\CMS\Layout\LayoutHelper;
4 +use Joomla\CMS\Session\Session;
5
6 +$wa = $this->document->getWebAssetManager();
7 +$wa->useScript('table.columns');
8
9 +$canChange = true;
1
github.com/joomla/joomla-cms/pull/36591
10 $assoc = Associations::isEnabled();
11 +$listOrder = $this->escape($this->state->get('list.ordering'));
12 +$listDirn = $this->escape($this->state->get('list.direction'));
13 +$saveOrder = $listOrder == 'a.ordering';
14
15 +if ($saveOrder && !empty($this->items)) {
16 + $saveOrderingUrl = 'index.php?option=com_foos&task=foos.
saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1';
17 +}
18 ?>
19 <form action="<?php echo Route::_('index.php?option=com_foos'); ?>"
method="post" name="adminForm" id="adminForm">
20 <div class="row">
21
22 echo 'col-md-12';
23 } ?>">
24 <div id="j-main-container" class="j-main-container">
25 + <?php echo LayoutHelper::render('joomla.searchtools.
default', ['view' => $this]); ?>
26 <?php if (empty($this->items)) : ?>
27 <div class="alert alert-warning">
28 <?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS
'); ?>
29 </div>
30 <?php else : ?>
31 <table class="table" id="fooList">
32 + <caption id="captionTable" class="sr-only">
33 + <?php echo Text::_('COM_FOOS_TABLE_CAPTION
'); ?>, <?php echo Text::_('JGLOBAL_SORTED_BY'); ?>
34 + </caption>
35 <thead>
36 <tr>
37 + <th scope="col" style="width:1%" class=
"text-center d-none d-md-table-cell">
38 + <?php echo HTMLHelper::_('
searchtools.sort', '', 'a.ordering', $listDirn, $listOrder, null, '
asc', 'JGRID_HEADING_ORDERING', 'icon-menu-2'); ?>
39 + </th>
40 <td style="width:1%" class="text-center
">
41 <?php echo HTMLHelper::_('grid.
checkall'); ?>
42 </td>
43 <th scope="col" style="width:1%" class=
"text-center d-none d-md-table-cell"
>
44 - <?php echo Text::_('
COM_FOOS_TABLE_TABLEHEAD_NAME'); ?>
45 - </th>
46 - <th scope="col" style="width:1%; min-
width:85px" class="text-center">
78 ?>
79 <tr class="row<?php echo $i % 2; ?>">
80 + <td class="order text-center d-none d-
md-table-cell">
81 + <?php
82 + $iconClass = '';
83 + if (!$canChange) {
84 + $iconClass = ' inactive';
85 + } else if (!$saveOrder) {
86 + $iconClass = ' inactive tip-top
hasTooltip" title="' . HTMLHelper::_('tooltipText', '
JORDERINGDISABLED');
87 + }
88 + ?>
89 + <span class="sortable-handler<?php
echo $iconClass; ?>">
90 + <span class="icon-menu" aria-
hidden="true"></span>
91 + </span>
92 + <?php if ($canChange && $saveOrder)
: ?>
93 + <input type="text" style="
display:none" name="order[]" size="5"
94 + value="<?php echo $item->
ordering; ?>" class="width-20 text-area-order">
95 + <?php endif; ?>
96 + </td>
97 <td class="text-center">
98 <?php echo HTMLHelper::_('grid.id',
$i, $item->id); ?>
99 </td>
100
101
102 <div class="small">
103 <?php echo Text::_('JCATEGORY')
. ': ' . $this->escape(
$item->category_title); ?>
104 - </div>
105 + </div>
106 </th>
107 <td class="text-center">
108 <?php
109 - echo HTMLHelper::_('jgrid.published
', $item->published, $i, 'foos.', true, 'cb', $item->publish_up,
$item->publish_down);
110 + echo HTMLHelper::_('jgrid.published
', $item->published, $i, 'foos.', $canChange, 'cb', $item->
publish_up, $item->publish_down);
111 ?>
112 </td>
113 <td class="small d-none d-md-table-cell
">
Icons show us if a column is sorted and in which direction. To make the sorting clear to someone who
doesn’t see these markers, we add a <caption> element. This is not displayed, it is read out.
The class visually-hidden hides an element for all devices except screen readers.
administrator/components/com_foos/tmpl/foos/modal.php
1- install your component in Joomla version 4 to test it: Copy the files in the administrator folder
into the administrator folder of your Joomla 4 installation.
2. the database has been changed, so it is necessary to update it. Open the System |
Information | Database section as described in part Publish and Unpublish. Select
your component and click on Update Structure.
3. open the view of your component in the administration area and filter, sort and search for items
in your component.
You don’t develop the extension as an end in itself. It helps with the completion of tasks. In order for the
people working with the component to always have an overview of the possible work steps, it makes
sense to have a toolbar. In this part of the tutorial we will extend the already existing toolbar with
the standard actions. Here we will access a variety of ready-made methods. Again, for the standard,
it is not necessary to invent the wheel yourself. Later on, for special tasks, it makes sense to use the
standard as an example.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t16...t17
I’ll show you here how to integrate the standard functions into the toolbar. Each component has its
own functions. Just like the standard ones in Joomla, you add the special ones via buttons in the
toolbar. Look here at the standard functions.
The following code shows you which functions you use when creating or editing an element. The
ToolbarHelper1 class provides a lot of helpful functions. For example
We add permission checking here. A button is displayed only if the user is authorized to use it. The
ContentHelper::getActions function collects the permissions implemented in the access.xml
file, which are allowed to the currently logged in user. If this is the case, then $canDo->get('...')
equals true. A concrete example: $canDo->get('core.create') is true if the user is allowed to
create content.
administrator/components/com_foos/src/View/Foo/HtmlView.php
1 {
2 Factory::getApplication()->input->set('hidemainmenu', true);
3
4 + $user = Factory::getUser();
5 + $userId = $user->id;
6 +
7 $isNew = ($this->item->id == 0);
8
9 ToolbarHelper::title($isNew ? Text::_('COM_FOOS_MANAGER_FOO_NEW
') : Text::_('COM_FOOS_MANAGER_FOO_EDIT'), 'address foo');
10
11 - ToolbarHelper::apply('foo.apply');
12 - ToolbarHelper::cancel('foo.cancel', 'JTOOLBAR_CLOSE');
13 + // Since we don't track these assets at the item level, use the
category id.
14 + $canDo = ContentHelper::getActions('com_foos', 'category',
$this->item->catid);
15 +
16 + // Build the actions for new and existing records.
17 + if ($isNew)
18 + {
19 + // For new records, check the create permission.
20 + if ($isNew && (count($user->getAuthorisedCategories('
com_foos', 'core.create')) > 0))
21 + {
22 + ToolbarHelper::apply('foo.apply');
23 + ToolbarHelper::saveGroup(
24 + [
25 + ['save', 'foo.save'],
26 + ['save2new', 'foo.save2new']
27 + ],
28 + 'btn-success'
29 + );
30 + }
31 +
32 + ToolbarHelper::cancel('foo.cancel');
33 + }
34 + else
35 + {
36 + // Since it's an existing record, check the edit permission
, or fall back to edit own if the owner.
37 + $itemEditable = $canDo->get('core.edit') || ($canDo->get('
core.edit.own') && $this->item->created_by == $userId);
38 + $toolbarButtons = [];
39 +
40 + // Can't save the record if it's not editable
41 + if ($itemEditable)
42 + {
43 + ToolbarHelper::apply('foo.apply');
44 + $toolbarButtons[] = ['save', 'foo.save'];
45 +
46 + // We can save this record, but check the create
permission to see if we can return to make a new one.
47 + if ($canDo->get('core.create'))
48 + {
49 + $toolbarButtons[] = ['save2new', 'foo.save2new'];
50 + }
51 + }
52 +
53 + // If checked out, we can still save
54 + if ($canDo->get('core.create'))
55 + {
56 + $toolbarButtons[] = ['save2copy', 'foo.save2copy'];
57 + }
58 +
59 + ToolbarHelper::saveGroup(
60 + $toolbarButtons,
61 + 'btn-success'
62 + );
63 +
64 + if (Associations::isEnabled() && ComponentHelper::isEnabled
('com_associations'))
65 + {
66 + ToolbarHelper::custom('foo.editAssociations', 'contract
', 'contract', 'JTOOLBAR_ASSOCIATIONS', false, false);
67 + }
68 +
69 + ToolbarHelper::cancel('foo.cancel', 'JTOOLBAR_CLOSE');
70 + }
71 }
72 }
Here you can see an example of the List View toolbar - the view that gives you an overview of your
items. Permission checking has also been added here.
administrator/components/com_foos/src/View/Foos/HtmlView.php
42 + }
43 + }
44 +
45 + if ($this->state->get('filter.published') == -2 && $canDo->get(
'core.delete'))
46 + {
47 + $toolbar->delete('foos.delete')
48 + ->text('JTOOLBAR_EMPTY_TRASH')
49 + ->message('JGLOBAL_CONFIRM_DELETE')
50 + ->listCheck(true);
51 + }
52 +
53 + if ($user->authorise('core.admin', 'com_foos') || $user->
authorise('core.options', 'com_foos'))
54 {
55 $toolbar->preferences('com_foos');
56 }
Copy the files in the administrator folder to the administrator folder of your Joomla 4 installa-
tion.
A new installation is not necessary. Continue using the ones from the previous part.
2. Open the view of your component in the administration area. In the toolbar you will see a
dropdown list to trigger different actions.
3. Open the detail view for an item. Here you will also have a toolbar.
23. Parameter
Are there settings that apply to all items in your component that a user can customize to their needs?
For example, do you display digital maps and do you want to allow the user to determine the display
of the license for all his maps? In Joomla there are parameters for this purpose.
For the menu item we had already set a parameter. For the component, you can find it in the options
of the configuration. We will look at the item in particular in this section.
For impatient people: Look at the changed program code in the Diff Viewa and take over these
changes into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t17...t18
The code with which the assignment of a parameter is calculated, was for a long time differently
integrated in the Joomla core components. Shortly before the release of Joomla 4 there were efforts
to simplify and unify this. Example pull requests are PR 348941 and PR 325382 , from which one can be
1
github.com/joomla/joomla-cms/pull/34894
2
github.com/joomla/joomla-cms/pull/32538
In order to create the params column in the database where the parameters are stored when the com-
ponent is updated, we need the SQL file administrator/components/com_foos/sql/updates/
mysql/18.0.0.sql.
administrator/components/com_foos/sql/updates/mysql/18.0.0.sql
1 /* https://codeberg.org/astrid/j4examplecode/raw/branch
/39598941015020537d51ccb6ca4098f019d76b04/src/administrator/
components/com_foos/sql/updates/mysql/18.0.0.sql */
2
3 ALTER TABLE `#__foos_details ` ADD COLUMN `params ` text NOT NULL AFTER
`alias`;
23.1.2.1. administrator/components/com_foos/config.xml
In the configuration, the parameter is saved to set a default value. We add a field show_name to
the configuration. Then we create the possibility to override it for a single element administrator
/components/com_foos/forms/foo.xml or a menu item components/com_foos/tmpl/foo/
default.xml.
administrator/components/com_foos/config.xml
1 <option value="0">JNO</option>
2 <option value="1">JYES</option>
3 </field>
4 +
5 + <field
6 + name="show_name"
7 + type="radio"
8 + label="COM_FOOS_FIELD_PARAMS_NAME_LABEL"
9 + default="1"
10 + layout="joomla.form.field.radio.switcher"
11 + >
12 + <option value="0">JHIDE</option>
13 + <option value="1">JSHOW</option>
14 + </field>
15 </fieldset>
16 <fieldset
17 name="permissions"
In the form we use to edit an element, we add the params field. So show_name is also configurable for
a single element.
administrator/components/com_foos/forms/foo.xml
1 content_type="com_foos.foo"
2 />
3 </fieldset>
4 + <fields name="params" label="JGLOBAL_FIELDSET_DISPLAY_OPTIONS">
5 + <fieldset name="display" label="
JGLOBAL_FIELDSET_DISPLAY_OPTIONS">
6 + <field
7 + name="show_name"
8 + type="list"
9 + label="COM_FOOS_FIELD_PARAMS_NAME_LABEL"
10 + useglobal="true"
11 + >
12 + <option value="0">JHIDE</option>
13 + <option value="1">JSHOW</option>
14 + </field>
15 + </fieldset>
16 + </fields>
17 </form>
In Joomla there is the possibility to set the parmeter to the value global. The benefit is that when
you configure it, it shows what is set globally. Use useglobal="true" like /administrator/com-
ponents/com_contact/forms/contact.xml.
To create the column where the parameters will be stored during a new installation, we add a line to
the SQL file administrator/components/com_foos/sql/install.mysql.utf8.sql.
administrator/components/com_foos/sql/install.mysql.utf8.sql
5 +ALTER TABLE `#__foos_details ` ADD COLUMN `params ` text NOT NULL AFTER
`alias`;
In the class that handels the table, we make sure that the parameters are stored in the correct form. We
use the registry design pattern3 . This uses the ability to override properties in PHP. We add properties
using
1 $foo = $registry->foo;
administrator/components/com_foos/src/Table/FooTable.php
1 use Joomla\CMS\Application\ApplicationHelper;
2 use Joomla\CMS\Table\Table;
3 use Joomla\Database\DatabaseDriver;
4 +use Joomla\CMS\Language\Text;
5 +use Joomla\Registry\Registry;
6
7 public function check()
8 public function store($updateNulls = true)
9 {
10 + // Transform the params field
11 + if (is_array($this->params)) {
12 + $registry = new Registry($this->params);
13 + $this->params = (string) $registry;
14 + }
15 +
16 return parent::store($updateNulls);
17 }
18 }
23.1.2.5. components/com_foos/src/View/Foo/HtmlView.php
The view combines the data on the parameters so that the display fits. In Joomla it is usual that the
setting at the menu item overwrites everything. If there is no parameter here, the value that was saved
for the element is used. Last but not least the value of the configuration is used. You query the active
menu item via $active = $app->getMenu()->getActive();.
3
martinfowler.com/eaacatalog/registry.html
Sometimes it is more intuitive to use the display at the element as priority. This is what I implemented
here. $state->get('params') returns the value stored at the menu item. $item->params is the
parameter that was stored at the element. The code below shows how you combine the two so that
the value at the item is applied.
components/com_foos/src/View/Foo/HtmlView.php
1
2 use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
3 use Joomla\CMS\Factory;
4 +use Joomla\Registry\Registry;
5
6 class HtmlView extends BaseHtmlView
7 {
8 + protected $params = null;
9 +
10 + protected $state;
11 +
12 public function display($tpl = null)
13 {
14 $item = $this->item = $this->get('Item');
15
16 + $state = $this->state = $this->get('State');
17 + $params = $this->params = $state->get('params');
18 + $itemparams = new Registry(json_decode($item->params));
19 +
20 + $temp = clone $params;
21 +
22 + /**
23 + * $item->params are the foo params, $temp are the menu item
params
24 + * Merge so that the menu item params take priority
25 + *
26 + * $itemparams->merge($temp);
27 + */
28 +
29 + // Merge so that foo params take priority
30 + $temp->merge($itemparams);
31 + $item->params = $temp;
32 +
33 Factory::getApplication()->triggerEvent('onContentPrepare', ['
com_foos.foo', &$item]);
34
35 // Store the events for later
At the end we use the parameter when handling the display in the template components/com_foos/
tmpl/foo/default.php. If there is the parameter and it is set that the name should be displayed
if ($this->item->params->get('show_name')), then the name will be displayed. The label
$this->params->get('show_foo_name_label') will also be displayed only in that case:
components/com_foos/tmpl/foo/default.php
1 use Joomla\CMS\Language\Text;
2
3 -if ($this->get('State')->get('params')->get('show_foo_name_label')) {
4 - echo Text::_('COM_FOOS_NAME');
5 -}
6 +if ($this->item->params->get('show_name')) {
7 + if ($this->params->get('show_foo_name_label')) {
8 + echo Text::_('COM_FOOS_NAME');
9 + }
10
11 -echo $this->item->name;
12 + echo $this->item->name;
13 +}
14
15 echo $this->item->event->afterDisplayTitle;
16 echo $this->item->event->beforeDisplayContent;
components/com_foos/tmpl/foo/default.xml
To make it possible to store the parameter at the menu item, we add a field in the XML file. It is
important that it is placed under fields and is called params - at least for using the Joomla standard
functions.!
1 />
2 </fieldset>
3 </fields>
4 + <!-- Add fields to the parameters object for the layout. -->
5 + <fields name="params">
6 + <fieldset name="basic" label="JGLOBAL_FIELDSET_DISPLAY_OPTIONS"
>
7 + <field
8 + name="show_name"
9 + type="radio"
10 + label="COM_FOOS_FIELD_PARAMS_NAME_LABEL"
11 + layout="joomla.form.field.radio.switcher"
12 + default="1"
13 + class=""
14 + >
15 + <option value="0">JHIDE</option>
16 + <option value="1">JSHOW</option>
17 + </field>
18 + </fieldset>
19 + </fields>
20 </metadata>
The html form element input with the type radio has a typical look in Joomla. It is called
switcher and you create the look using the layout joomla.form.field.radio.switcher.
1 <field
2 name="show_name"
3 type="radio"
4 label="COM_FOOS_FIELD_PARAMS_NAME_LABEL"
5 layout="joomla.form.field.radio.switcher"
6 default="1"
7 class=""
8 >
9 <option value="0">JHIDE</option>
10 <option value="1">JSHOW</option>
11 </field>
1. install your component in Joomla version 4 to test it: Copy the files in the administrator folder
into the administrator folder of your Joomla 4 installation. Copy the files in the components
folder into the components folder of your Joomla 4 installation.
2. the database has been changed, so it is necessary to update it. Open the System |
Information | Database section as described in part Publish and Unpublish. Select
your component and click on Update Structure.
3. Open the view of your component in the administration area. When editing an item, there is now
the Display tab and the Show Name parameter.
4. Open the global options of your component in the administration area. Here is now also the
parameter Show Name.
5. Open the menu manager to create a menu item. To do this, click on Menu in the left sidebar
and then on All Menu Items. Then click on the New button and fill in all necessary fields. You
can find the appropriate Menu Item Type by clicking the Select button. Now there is the tab
Display and the parameter Show Name.
6. Set the Show Name parameter in different combinations and make sure that the display in the
frontend is correct.
24. Pagination
There is a lot of content soon. Displaying all elements on one page is not useful. It has a negative effect
on the layout and performance. Therefore, we divide the elements into sub-pages and add pagination
or page numbering. With this, navigation through the pages is possible. Links are inserted for this
purpose. Usually, these are located at the bottom of the page.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t18...t19
We do not have any special requests. To display the default pagination, more or less two lines are
enough. In the view you call $this->pagination = $this->get('Pagination'); to set the
variable $this->pagination.
administrator/components/com_foos/src/View/Foos/HtmlView.php
1 protected $items;
2
3 + protected $pagination;
4 +
5
6 public function display($tpl = null): void
7 {
8 $this->items = $this->get('Items');
9 -
10 + $this->pagination = $this->get('Pagination');
11 $this->filterForm = $this->get('FilterForm');
12 $this->activeFilters = $this->get('ActiveFilters');
13 $this->state = $this->get('State');
In the template we use the getListFooter method of the variable $this->pagination. That was
all!
administrator/components/com_foos/tmpl/foos/default.php
1 </tbody>
2 </table>
3
4 + <?php echo $this->pagination->getListFooter(); ?>
5 +
6 <?php endif; ?>
7 <input type="hidden" name="task" value="">
8 <input type="hidden" name="boxchecked" value="0">
In the global configuration you can set the number of elements that will be displayed by default.
Normally this is set to 20 elements.
Do you feel that something is missing in this chapter? Are you wondering where all the calculations
are that create the page links? Then take a look at the two files: libraries/src/Pagination/
Pagination.php and libraries/src/MVC/Model/ListModel.php. Joomla does all the work
for you if you use the default, so if specifically in our case the model extends the file libraries/src/
MVC/Model/ListModel.php.
Copy the files in the administrator folder to the administrator folder of your Joomla 4 installa-
tion.
A new installation is not necessary. Continue using the ones from the previous part.
2. open the view of your component in the administration area and create so many items that
they are no longer displayed on one page. In the lower part you will see a navigation to browse
through the contents.
25. Layouts
Sometimes it is necessary to design the display differently in the frontend. This is basically the respon-
sibility of the template. A component is responsible for the output of content, no more and no less.
The template ensures a uniform appearance. Nevertheless, there are use cases for different layouts.
How you incorporate them for a view is the topic of the following article.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t19...t20
components/com_foos/tmpl/foo/withhead.php
1 <?php
2
3 \defined('_JEXEC') or die;
4
5 use Joomla\CMS\Language\Text;
6
7 echo "<hr>Hier kannst du einen Headertext anzeigen.<hr>";
8
9 if ($this->item->params->get('show_name')) {
10 if ($this->params->get('show_foo_name_label')) {
11 echo Text::_('COM_FOOS_NAME') . $this->item->name;
1
github.com/astridx/boilerplate/blob/t6b/src/administrator/components/com_foos/tmpl/foos/emptystate.php
12 } else {
13 echo $this->item->name;
14 }
15 }
16
17 echo $this->item->event->afterDisplayTitle;
18 echo $this->item->event->beforeDisplayContent;
19 echo $this->item->event->afterDisplayContent;
components/com_foos/tmpl/foo/withhead.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <metadata>
4 <layout title="COM_FOOS_FOO_VIEW_WITHHEAD_TITLE">
5 <message>
6 <![CDATA[COM_FOOS_FOO_VIEW_WITHHEAD_DESC]]>
7 </message>
8 </layout>
9 <!-- Add fields to the request variables for the layout. -->
10 <fields name="request">
11 <fieldset name="request"
12 addfieldprefix="FooNamespace\Component\Foos\Administrator\
Field"
13 >
14 <field
15 name="id"
16 type="modal_foo"
17 label="COM_FOOS_SELECT_FOO_LABEL"
18 required="true"
19 select="true"
20 new="true"
21 edit="true"
22 clear="true"
23 />
24 </fieldset>
25 </fields>
26 </metadata>
components/com_foos/tmpl/foo/withheadandfoot.php
1
2 <?php
3
4 \defined('_JEXEC') or die;
5
6 use Joomla\CMS\Language\Text;
7
8 echo "<hr>Hier kannst du einen Headertext anzeigen.<hr>";
9
10 if ($this->item->params->get('show_name')) {
11 if ($this->Params->get('show_foo_name_label')) {
12 echo Text::_('COM_FOOS_NAME') . $this->item->name;
13 } else {
14 echo $this->item->name;
15 }
16 }
17
18 echo "<hr>Hier kannst du eine Fußzeile anzeigen.<hr>";
19
20 echo $this->item->event->afterDisplayTitle;
21 echo $this->item->event->beforeDisplayContent;
22 echo $this->item->event->afterDisplayContent;
administrator/components/com_foos/forms/foo.xml
1 <option value="0">JHIDE</option>
2 <option value="1">JSHOW</option>
3 </field>
4 +
5 + <field
6 + name="foos_layout"
7 + type="componentlayout"
8 + label="JFIELD_ALT_LAYOUT_LABEL"
9 + class="custom-select"
10 + extension="com_foos"
11 + view="foo"
12 + useglobal="true"
13 + />
14 </fieldset>
15 </fields>
16 </form>
25.1.2.2. components/com_foos/src/Model/FooModel.php
This is what happens during development. Basically we would not have to change the file components
/com_foos/src/Model/FooModel.php. In this chapter I noticed that a use entry is missing. There-
fore a change is made after all.
components/com_foos/src/Model/FooModel.php
1 use Joomla\CMS\Factory;
2 use Joomla\CMS\MVC\Model\BaseDatabaseModel;
3 +use Joomla\CMS\Language\Text;
25.1.2.3. components/com_foos/src/View/Foo/HtmlView.php
In the case of a menu item, I think it is important that it - or the content and design - is always displayed
consistently. That is why we query the active menu item. If, for example, elements are displayed via
a category view, then a uniform layout is possible with the help of this information. If the content is
displayed as a single element, a different layout can be used.
components/com_foos/src/View/Foo/HtmlView.php
1 $temp->merge($itemparams);
2 $item->params = $temp;
3
4 + $active = Factory::getApplication()->getMenu()->getActive();
5 +
6 + // Override the layout only if this is not the active menu item
7 + // If it is the active menu item, then the view and item id
will match
8 + if ((!$active) || ((strpos($active->link, 'view=foo') === false
) || (strpos($active->link, '&id=' . (string) $this->item->id) ===
false)))
9 + {
10 + if (($layout = $item->params->get('foos_layout')))
11 + {
12 + $this->setLayout($layout);
13 + }
14 + }
15 + elseif (isset($active->query['layout']))
16 + {
17 + // We need to set the layout in case this is an alternative
menu item (with an alternative layout)
18 + $this->setLayout($active->query['layout']);
19 + }
20 +
21 Factory::getApplication()->triggerEvent('onContentPrepare',
array ('com_foos.foo', &$item));
22
23 // Store the events for later
1. perform a new installation. To do this, uninstall your previous installation and copy all files again.
In a freshly installed system the explanation of the layouts is more uncomplicated.
Copy the files in the administrator folder into the administrator folder of your Joomla 4 installa-
tion.
Copy the files in the components folder into the components folder of your Joomla 4 installation.
Install your component as described in part one, after copying all the files.
2. set a special layout for an item. I set withhead at the item with ID 2.
An item without XML file is not selectable in the administration area. Nevertheless such layouts
are useful! In the program code it is possible to assign it at any place: $this->setLayout('
withheadandfoot');
Since the view is expected to be uniform when controlling via a menu item, the layout set in the
menu item is preferred.
The checkout function avoids unexpected results that occur when two users edit the same item at the
same time. Checking out locks an item when a user opens it for editing. It is then unlocked again when
saved or closed. This is a useful function that we are integrating into our sample extension in this part
of the article series.
Sometimes it happens that an item is marked as checked out, although no one has opened it
for editing at the same time. This usually happens when a previous opening was not finished
correctly. For example, the web browser was closed even though the item was open for editing,
or the back button in the browser menu was clicked instead of closing the item properly.
For impatient people: Look at the changed programme code in the Diff Viewa and copy these
changes into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t20...t21
Like all properties of a Foo element, the checkout state is stored in the database. We create two columns.
Below you can see the script that is called during a Joomla update.
administrator/components/com_foos/sql/install.mysql.utf8.sql
1
2 ALTER TABLE `#__foos_details ` ADD COLUMN `checked_out ` int(10) unsigned
NOT NULL DEFAULT 0 AFTER `alias`;
3
4 ALTER TABLE `#__foos_details ` ADD COLUMN `checked_out_time ` datetime
AFTER `alias`;
5
6 ALTER TABLE `#__foos_details ` ADD KEY `idx_checkout ` ( `checked_out`);
In the form we add the fields for saving the state. We hide them with the attribute hidden, as they are
not changed by the user here. Joomla sets the values automatically in the background.
administrator/components/com_foos/forms/foo.xml
1 size="1"
2 />
3
4 + <field
5 + name="checked_out"
6 + type="hidden"
7 + filter="unset"
8 + />
9 +
10 + <field
11 + name="checked_out_time"
12 + type="hidden"
13 + filter="unset"
14 + />
15 +
16 <field
17 name="ordering"
18 type="ordering"
We add the database changes that we entered above for the update in the separate SQL file to the SQL
script that is called during a new installation.
administrator/components/com_foos/sql/install.mysql.utf8.sql
In the model, we adjust everything so that the two new columns are loaded correctly.
administrator/components/com_foos/src/Model/FoosModel.php
38 {
In the list view we do not insert a separate column. A symbol is displayed by the name if
the element is locked. To display this, I choose the function that Joomla uses in its own
extensions: echo HTMLHelper::_('jgrid.checkedout', $i, $item->editor, $item
->checked_out_time, 'foos.', true). At the same time, this takes over the check whether the
contribution is released or not.
administrator/components/com_foos/tmpl/foos/default.php
I have kept it uncomplicated here. I do not check whether someone is authorised to release a
checked-out post again. The components in Joomla make this more restrictive. In com_contact,
for example, the relevant line looks like this: <?php echo HTMLHelper::*('jgrid.
checkedout', $i, $item->editor, $item->checked_out_time, 'contacts.',
$canCheckin); ?>. If you also don’t allow everyone to unlock and want to implement this, look
at the implementation in com_contact - there you find the code that computes$canCheckin.
Copy the files in the administrator folder into the administrator folder of your Joomla 4 installa-
tion.
2. the database has been changed, so it is necessary to update it. Open the System |
Information | Database section as described in part Publish and Unpublish. Select
4. Switch to another browser window and try to edit the item again.
5. Make sure you see an icon that tells you that the item is locked and that an authorised user can
unlock it.
27. Batch
Joomla offers a number of functions that enable administrators to process several items at once. We
now add this batch processing to the component. Based on this, it is possible for you to add your own
“batch processing” functions.
For impatient people: Look at the changed programme code in the Diff Viewa and copy these
changes into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t21...t22
The following file creates the middle part of the form that is displayed to trigger batch processing.
administrator/components/com_foos/tmpl/foos/default_batch_body.php
1
2 <?php
3 \defined('_JEXEC') or die;
4
5 use Joomla\CMS\Layout\LayoutHelper;
6
7 $published = $this->state->get('filter.published');
8 $noUser = true;
9 ?>
10
11 <div class="p-3">
12 <div class="row">
13 <div class="form-group col-md-6">
14 <div class="controls">
15 <?php echo LayoutHelper::render('joomla.html.batch.
language', []); ?>
16 </div>
17 </div>
The following file creates the footer of the form that is displayed to trigger batch processing.
The “JCANCEL” button clears all values in the form fields using document.getElementById('
ELEMENT_ID').value=''. I have included all possible fields here, although we don’t use them all
yet. For example, batch-user-id and batch-tag-id are not yet used in our form. The button
JGLOBAL_BATCH_PROCESS starts the batch processing.
It is important that you create the batch form as described above in the file administrator/
components/com_foos/tmpl/foos/default_batch_body.php. LayoutHelper in combi-
nation with the appropriate layout ensures that all variables and IDs are set so that the standard
functions run correctly.
administrator/components/com_foos/tmpl/foos/default_batch_footer.php
1
2 <?php
3 \defined('_JEXEC') or die;
4
5 use Joomla\CMS\Language\Text;
6
7 ?>
8 <button type="button" class="btn btn-secondary" onclick="document.
getElementById('batch-category-id').value='';document.getElementById
('batch-access').value='';document.getElementById('batch-language-id
').value='';document.getElementById('batch-user-id').value='';
document.getElementById('batch-tag-id').value=''" data-bs-dismiss="
modal">
In the controller we implement the method batch. If we look at it closely, we add nothing more than
the specifics: The name of the model used for data processing and the address to forward to after
processing. At the end we call the implementation of Joomla with return parent::batch($model
);. Done! For the standard functions, the wheel has already been invented by Joomla.
administrator/components/com_foos/src/Controller/FooController.php
1
2 \defined('_JEXEC') or die;
3
4 use Joomla\CMS\MVC\Controller\FormController;
5 +use Joomla\CMS\Router\Route;
6
7
8 class FooController extends FormController
9 {
10 + public function batch($model = null)
11 + {
12 + $this->checkToken();
13 +
14 + $model = $this->getModel('Foo', 'Administrator', array());
15 +
16 + // Preset the redirect
17 + $this->setRedirect(Route::_('index.php?option=com_foos&view=
foos' . $this->getRedirectToListAppend(), false));
18 +
19 + return parent::batch($model);
20 + }
21 }
In the model we specify whether copying and moving is supported. In case of false the command is
not provided by the batch processing. We also specify the properties that are editable using the batch
function.
administrator/components/com_foos/src/Model/FooModel.php
To make the batch processing usable via a button, we add an entry to the toolbar.
administrator/components/com_foos/src/View/Foos/HtmlView.php
1 {
2 $childBar->trash('foos.trash')->listCheck(true);
3 }
4 - }
5
6 - if ($this->state->get('filter.published') == -2 && $canDo->get(
'core.delete'))
7 - {
8 - $toolbar->delete('foos.delete')
9 - ->text('JTOOLBAR_EMPTY_TRASH')
10 - ->message('JGLOBAL_CONFIRM_DELETE')
11 - ->listCheck(true);
12 + if ($this->state->get('filter.published') == -2 && $canDo->
get('core.delete'))
13 + {
14 + $childBar->delete('foos.delete')
15 + ->text('JTOOLBAR_EMPTY_TRASH')
16 + ->message('JGLOBAL_CONFIRM_DELETE')
17 + ->listCheck(true);
18 + }
19 +
20 + // Add a batch button
21 + if ($user->authorise('core.create', 'com_foos')
22 + && $user->authorise('core.edit', 'com_foos')
23 + && $user->authorise('core.edit.state', 'com_foos'))
24 + {
25 + $childBar->popupButton('batch')
26 + ->text('JTOOLBAR_BATCH')
27 + ->selector('collapseModal')
28 + ->listCheck(true);
29 + }
30 }
31
32 if ($user->authorise('core.admin', 'com_foos') || $user->
authorise('core.options', 'com_foos'))
We create the template that is used to create the form to trigger batch processing with the help of
HTMLHelper.
administrator/components/com_foos/tmpl/foos/default.php
Copy the files in the administrator folder into the administrator folder of your Joomla 4 installa-
tion.
A new installation is not necessary. Continue using the files from the previous part.
2. Open the view of your component in the administration area. In the toolbar you will see a
selection list for triggering various actions. Click the entry “Batch”.
Self-explanatory software is ideal. But which programme is? For this reason, help is always a useful
addition. Depending on the system, help pages cannot be found immediately or are even hidden.
Joomla offers a uniform procedure for this.
On the one hand, there is a button positioned in the same place in each component, which is used to
call up an external help page.
In addition, it is possible to show descriptions next to the fields in forms. Since Joomla 4.1, these
descriptions can be shown and hidden for a better overview. This feature was introduced with PR
356101 and called inline help.
For impatient people: Look at the changed programme code in the Diff Viewa and copy these
1
github.com/joomla/joomla-cms/pull/35610/
The first thing you need to do is to create the help pages for your extension and save them online.
Perhaps you would like to base the structure of your individual help pages on Joomla’s own.
You can find Joomla’s own help pages on the Internet at the address help.joomla.org/proxya . An
example page would be help.joomla.org/proxy?keyref=Help40:Articles&lang=en. Here, help.
joomla.org/proxy stands for the base address and ?keyref=Help40:Articles&lang=en
addresses the specific subpage.
a
help.joomla.org/proxy
Two lines per view are sufficient to display a button at the top right of the dashboard views that contains
a question mark as an icon and has an Internet address specified in the code as the link target. I have
chosen http://example.org as an example. The principle is clear. You have the possibility to create
a separate help site for each View and to link it in the view of the component - exactly where questions
usually arise. And another line is enough to turn descriptions into inline help, which means to make
them fade in and out or toggleable.
28.1.2.1. administrator/components/com_foos/config.xml
In the form for the config, we add a description as an example. This will be shown or hidden later as
inline help..
administrator/components/com_foos/forms/foo.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <config>
4 + <inlinehelp button="show"/>
5 <fieldset
6 name="foo"
7 label="COM_FOOS_FIELD_CONFIG_INDIVIDUAL_FOO_DISPLAY"
8 name="show_foo_name_label"
9 type="list"
10 label="COM_FOOS_FIELD_FOO_SHOW_CATEGORY_LABEL"
11 + description="
COM_FOOS_FIELD_FOO_SHOW_CATEGORY_DESC"
12 default="1"
13 >
14 <option value="0">JNO</option>
28.1.2.2. administrator/components/com_foos/forms/foo.xml
In the form, we add a description as an example. This will be shown or hidden later as inline help.
administrator/components/com_foos/forms/foo.xml
The toolbar helper supports us. The line ToolbarHelper::divider(); ensures that the follow-
ing buttons are displayed right-aligned. ToolbarHelper::inlinehelp(); inserts the button that
shows and hides the inline help. The text for this is searched behind description= in the form at the
field. ToolbarHelper::help('', false, 'http://example.org'); inserts the button that
redirects to the external help page. The address of the external page, here in the example http://
example.org, is given as a parameter.
administrator/components/com_foos/src/View/Foos/HtmlView.php
2
3 ToolbarHelper::cancel('foo.cancel', 'JTOOLBAR_CLOSE');
4 }
5 +
6 + ToolbarHelper::divider();
7 + ToolbarHelper::inlinehelp();
8 + ToolbarHelper::help('', false, 'http://example.org');
9 }
10 }
administrator/components/com_foos/src/View/Foos/HtmlView.php
administrator/components/com_foos/tmpl/foo/edit.php
1 $wa = $this->document->getWebAssetManager();
2 $wa->useScript('keepalive')
3 ->useScript('form.validate')
4 + ->useScript('inlinehelp')
5 ->useScript('com_foos.admin-foos-letter');
6
7 $isModal = $input->get('layout') === 'modal';
1. install your component in Joomla version 4 to test it: Copy the files in the administrator
folder into the administrator folder of your Joomla 4 installation. A new installation is not
necessary. Continue using the files from the previous part.
2. Open the view of your component in the administration area. Click on the help link and make
sure that you are redirected to the help page you entered.
3. Open the view of your component in the administration area and click several times on the
Inline Toggle Help button. Make sure that all texts that are available as description for a field are
toggled on and off.
29. Featured
Some items are special and for them there is a special attribute in Joomla: featured or main entry.
This part of the article series adds featured to our component.
In Joomla, elements marked with featured are displayed when the home page menu item is
linked to the featured layout. In this way, it is possible to show or hide an element only by
changing the “featured” property on a page - for example the start page. This has no effect on
other display properties - for example, displaying in a category blog.
For impatient people: Look at the changed programme code in the Diff Viewa and copy these
changes into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t23...t24
You already know this. We store the property featured in the database, so we extend the database
table by one column. We do this in the file 24.0.0.sql.
administrator/components/com_foos/sql/updates/mysql/24.0.0.sql
1
2 ALTER TABLE `#__foos_details ` ADD COLUMN `featured ` tinyint(3)
unsigned NOT NULL DEFAULT 0 COMMENT 'Set if foo is featured.';
3
4 ALTER TABLE `#__foos_details ` ADD KEY `idx_featured_catid ` ( `featured
` , `catid`);
29.1.1.2. components/com_foos/src/Model/FeaturedModel.php
components/com_foos/src/Model/FeaturedModel.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Site\Model;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\Component\ComponentHelper;
9 use Joomla\CMS\Factory;
10 use Joomla\CMS\Language\Multilanguage;
11 use Joomla\CMS\MVC\Model\ListModel;
12 use Joomla\Database\ParameterType;
13 use Joomla\Registry\Registry;
14
15 class FeaturedModel extends ListModel
16 {
17 public function __construct($config = [])
18 {
19 if (empty($config['filter_fields'])) {
20 $config['filter_fields'] = [
21 'id', 'a.id',
22 'name', 'a.name',
23 'ordering', 'a.ordering',
24 ];
25 }
26
27 parent::__construct($config);
28 }
29
30 public function getItems()
31 {
32 // Invoke the parent getItems method to get the main list
33 $items = parent::getItems();
34
35 // Convert the params field into an object, saving original in
_params
36 for ($i = 0, $n = count($items); $i < $n; $i++) {
37 $item = &$items[$i];
38
39 if (!isset($this->_params)) {
40 $item->params = new Registry($item->params);
41 }
42 }
43
44 return $items;
45 }
46
47 protected function getListQuery()
48 {
49 $user = Factory::getUser();
50 $groups = $user->getAuthorisedViewLevels();
51
52 // Create a new query object.
53 $db = $this->getDbo();
54 $query = $db->getQuery(true);
55
56 // Select required fields from the categories.
57 $query->select($this->getState('list.select', 'a.*'))
58 ->from($db->quoteName('#__foos_details', 'a'))
59 ->where($db->quoteName('a.featured') . ' = 1')
60 ->whereIn($db->quoteName('a.access'), $groups)
61 ->innerJoin($db->quoteName('#__categories', 'c') . ' ON c.
id = a.catid')
62 ->whereIn($db->quoteName('c.access'), $groups);
63
64 // Filter by category.
65 if ($categoryId = $this->getState('category.id')) {
66 $query->where($db->quoteName('a.catid') . ' = :catid');
67 $query->bind(':catid', $categoryId, ParameterType::INTEGER)
;
68 }
69
70 $query->select('c.published as cat_published, c.published AS
parents_published')
71 ->where('c.published = 1');
72
73 // Filter by state
74 $state = $this->getState('filter.published');
75
76 if (is_numeric($state)) {
77 $query->where($db->quoteName('a.published') . ' = :
published');
78 $query->bind(':published', $state, ParameterType::INTEGER);
79
80 // Filter by start and end dates.
81 $nowDate = Factory::getDate()->toSql();
82
83 $query->where('(' . $db->quoteName('a.publish_up') .
84 ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <=
:publish_up)')
85 ->where('(' . $db->quoteName('a.publish_down') .
86 ' IS NULL OR ' . $db->quoteName('a.publish_down') .
' >= :publish_down)')
87 ->bind(':publish_up', $nowDate)
88 ->bind(':publish_down', $nowDate);
89 }
90
91 // Filter by language
92 if ($this->getState('filter.language')) {
93 $language = [Factory::getLanguage()->getTag(), '*'];
94 $query->whereIn($db->quoteName('a.language'), $language,
ParameterType::STRING);
95 }
96
97 // Add the list ordering clause.
98 $query->order($db->escape($this->getState('list.ordering', 'a.
ordering')) . ' ' . $db->escape($this->getState('list.
direction', 'ASC')));
99
100 return $query;
101 }
102
103 protected function populateState($ordering = null, $direction =
null)
104 {
105 $app = Factory::getApplication();
106 $params = ComponentHelper::getParams('com_foos');
107
108 // List state information
109 $limit = $app->getUserStateFromRequest('global.list.limit', '
limit', $app->get('list_limit'), 'uint');
110 $this->setState('list.limit', $limit);
111
112 $limitstart = $app->input->get('limitstart', 0, 'uint');
113 $this->setState('list.start', $limitstart);
114
115 $orderCol = $app->input->get('filter_order', 'ordering');
116
117 if (!in_array($orderCol, $this->filter_fields)) {
118 $orderCol = 'ordering';
119 }
120
121 $this->setState('list.ordering', $orderCol);
122
123 $listOrder = $app->input->get('filter_order_Dir', 'ASC');
124
125 if (!in_array(strtoupper($listOrder), ['ASC', 'DESC', ''])) {
126 $listOrder = 'ASC';
127 }
128
129 $this->setState('list.direction', $listOrder);
130
131 $user = Factory::getUser();
132
133 if ((!$user->authorise('core.edit.state', 'com_foos')) && (!
$user->authorise('core.edit', 'com_foos'))) {
134 // Limit to published for people who can't edit or edit.
state.
135 $this->setState('filter.published', 1);
136
137 // Filter by start and end dates.
29.1.1.3. components/com_foos/src/View/Featured/HtmlView.php
featured gets its own file to manage the display in the frontend.
You see here the first time the word slug in the line $item->slug = $item->alias ? ($item
->id . ':'. $item->alias): $item->id;. A slug is used to keep the code supporting
search engine friendly URLs as short as possible. It is composed of the ID of the element, a colon
and the alias.
components/com_foos/src/View/Featured/HtmlView.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Site\View\Featured;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\Factory;
9 use Joomla\CMS\HTML\HTMLHelper;
10 use Joomla\CMS\Language\Text;
11 use Joomla\CMS\Mail\MailHelper;
12 use Joomla\CMS\MVC\View\GenericDataException;
13 use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
14
15 class HtmlView extends BaseHtmlView
16 {
17 protected $state;
18
19 protected $items;
20
21 protected $pagination;
22
23 protected $params = null;
24
25 protected $pageclass_sfx = '';
26
27 public function display($tpl = null)
28 {
29 $app = Factory::getApplication();
30 $params = $app->getParams();
31
32 // Get some data from the models
33 $state = $this->get('State');
34 $items = $this->get('Items');
35 $category = $this->get('Category');
36 $children = $this->get('Children');
37 $parent = $this->get('Parent');
38 $pagination = $this->get('Pagination');
39
40 // Flag indicates to not add limitstart=0 to URL
41 $pagination->hideEmptyLimitstart = true;
42
43 // Check for errors.
44 if (count($errors = $this->get('Errors'))) {
45 throw new GenericDataException(implode("\n", $errors), 500)
;
46 }
47
48 // Prepare the data.
49 // Compute the foos slug.
50 for ($i = 0, $n = count($items); $i < $n; $i++) {
51 $item = &$items[$i];
52 $item->slug = $item->alias ? ($item->id . ':' . $item->
alias) : $item->id;
53 $temp = $item->params;
54 $item->params = clone $params;
55 $item->params->merge($temp);
56
57 if ($item->params->get('show_email', 0) == 1) {
58 $item->email_to = trim($item->email_to);
59
60 if (!empty($item->email_to) && MailHelper::
isEmailAddress($item->email_to)) {
61 $item->email_to = HTMLHelper::_('email.cloak',
$item->email_to);
62 } else {
63 $item->email_to = '';
64 }
65 }
66 }
67
68 // Escape strings for HTML output
69 $this->pageclass_sfx = htmlspecialchars($params->get('
pageclass_sfx'), ENT_COMPAT, 'UTF-8');
70
71 $maxLevel = $params->get('maxLevel', -1);
72 $this->maxLevel = &$maxLevel;
73 $this->state = &$state;
74 $this->items = &$items;
75 $this->category = &$category;
76 $this->children = &$children;
77 $this->params = &$params;
78 $this->parent = &$parent;
79 $this->pagination = &$pagination;
80
81 $this->_prepareDocument();
82
83 return parent::display($tpl);
84 }
85
86 protected function _prepareDocument()
87 {
88 $app = Factory::getApplication();
89 $menus = $app->getMenu();
90 $title = null;
91
92 // Because the application sets a default page title,
93 // we need to get it from the menu item itself
94 $menu = $menus->getActive();
95
96 if ($menu) {
97 $this->params->def('page_heading', $this->params->get('
page_title', $menu->title));
98 } else {
99 $this->params->def('page_heading', Text::_('
COM_FOOS_DEFAULT_PAGE_TITLE'));
100 }
101
102 $title = $this->params->get('page_title', '');
103
104 if (empty($title)) {
105 $title = $app->get('sitename');
106 } else if ($app->get('sitename_pagetitles', 0) == 1) {
107 $title = Text::sprintf('JPAGETITLE', $app->get('sitename'),
$title);
108 } else if ($app->get('sitename_pagetitles', 0) == 2) {
109 $title = Text::sprintf('JPAGETITLE', $title, $app->get('
sitename'));
110 }
111
112 $this->document->setTitle($title);
113
114 if ($this->params->get('menu-meta_description')) {
115 $this->document->setDescription($this->params->get('menu-
meta_description'));
116 }
117
118 if ($this->params->get('menu-meta_keywords')) {
119 $this->document->setMetaData('keywords', $this->params->get
('menu-meta_keywords'));
120 }
121
122 if ($this->params->get('robots')) {
123 $this->document->setMetaData('robots', $this->params->get('
robots'));
124 }
125 }
126 }
The display in the frontend is done as before via a template, which we implement in the file default.
php.
components/com_foos/tmpl/featured/default.php
1
2 <?php
3
4 \defined('_JEXEC') or die;
5
6 ?>
7 <div class="com-foos-featured blog-featured">
8 <?php if ($this->params->get('show_page_headings') != 0) : ?>
9 <h1>
10 <?php echo $this->escape($this->params->get('page_heading'));
?>
11 </h1>
12 <?php endif; ?>
13
14 <?php echo $this->loadTemplate('items'); ?>
15
16 <?php if ($this->params->def('show_pagination', 2) == 1 || ($this->
params->get('show_pagination') == 2 && $this->pagination->pagesTotal
> 1)) : ?>
17 <div class="com-foos-featured__pagination w-100">
18 <?php if ($this->params->def('show_pagination_results', 1)) :
?>
19 <p class="counter float-right pt-3 pr-2">
20 <?php echo $this->pagination->getPagesCounter(); ?>
21 </p>
22 <?php endif; ?>
23
24 <?php echo $this->pagination->getPagesLinks(); ?>
25 </div>
26 <?php endif; ?>
27 </div>
components/com_foos/tmpl/featured/default.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3
4 <metadata>
5 <layout title="COM_FOOS_FEATURED_VIEW_DEFAULT_TITLE">
6 <help
7 key = "JHELP_MENUS_MENU_ITEM_FOOS_FEATURED"
8 />
9 <message>
10 <![CDATA[COM_FOOS_FEATURED_VIEW_DEFAULT_DESC]]>
11 </message>
12 </layout>
13
14 <!-- Add fields to the parameters object for the layout. -->
15 <fields name="params">
16 <fieldset name="advanced" label="JGLOBAL_LIST_LAYOUT_OPTIONS">
17
18 <field
19 name="spacer"
20 type="spacer"
21 label="JGLOBAL_SUBSLIDER_DRILL_CATEGORIES_LABEL"
22 class="text"
23 />
24
25 <field
26 name="show_headings"
27 type="list"
28 label="JGLOBAL_SHOW_HEADINGS_LABEL"
29 useglobal="true"
30 class="custom-select-color-state"
31 >
32 <option value="0">JHIDE</option>
33 <option value="1">JSHOW</option>
34 </field>
35
36 <field
37 name="show_pagination"
38 type="list"
39 label="JGLOBAL_PAGINATION_LABEL"
40 useglobal="true"
41 class="custom-select-color-state"
42 >
43 <option value="0">JHIDE</option>
44 <option value="1">JSHOW</option>
45 <option value="2">JGLOBAL_AUTO</option>
46 </field>
47 </fieldset>
48 </fields>
49 </metadata>
When using subtemplates, it is important that they are located in the same directory as the actual
template and that their names match:
The call <?php echo $this->loadTemplate('NAME'); ?> loads the file
default_NAME.php if it is in the file default.php is executed.
components/com_foos/tmpl/featured/default_items.php
1
2 <?php
3
4 \defined('_JEXEC') or die;
5
6 use Joomla\CMS\HTML\HTMLHelper;
7 use Joomla\CMS\Language\Text;
8 use Joomla\CMS\Uri\Uri;
9
10 HTMLHelper::_('behavior.core');
11
12 $listOrder = $this->escape($this->state->get('list.ordering'));
13 $listDirn = $this->escape($this->state->get('list.direction'));
14
15 ?>
16
17 <div class="com-foos-featured__items">
18 <?php if (empty($this->items)) : ?>
19 <p class="com-foos-featured__message"> <?php echo Text::_('
COM_FOO_NO_FOOS'); ?> </p>
20 <?php else : ?>
21 <form action="<?php echo htmlspecialchars(Uri::getInstance()->
toString()); ?>" method="post" name="adminForm" id="adminForm">
22 <table class="com-foos-featured__table table">
23 <?php if ($this->params->get('show_headings')) : ?>
24 <thead class="thead-default">
25 <tr>
26 <th class="item-num">
We extend the form with which an element is created or changed by the field for setting the property
featured.
administrator/components/com_foos/forms/foo.xml
1 <option value="*">JALL</option>
2 </field>
3
4 + <field
5 + name="featured"
6 + type="radio"
7 + class="switcher"
8 + label="JFEATURED"
9 + default="0"
10 + >
11 + <option value="0">JNO</option>
12 + <option value="1">JYES</option>
13 + </field>
14 +
15 <field
16 name="published"
17 type="list"
In the case of a new installation, the script in the file install.mysql.utf8.sql creates the database.
Here we add a column to store the property featured.
administrator/components/com_foos/sql/install.mysql.utf8.sql
We implement the logic with which we set the featured property in the featured() function in the
FoosController.
administrator/components/com_foos/src/Controller/FoosController.php
1 \defined('_JEXEC') or die;
2
3 +use Joomla\CMS\Language\Text;
4 +use Joomla\Utilities\ArrayHelper;
5 use Joomla\CMS\Application\CMSApplication;
6 use Joomla\CMS\MVC\Controller\AdminController;
7 use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
8
9 public function __construct($config = array(), MVCFactoryInterface
$factory = null, $app = null, $input = null)
10 {
11 parent::__construct($config, $factory, $app, $input);
12 +
13 + $this->registerTask('unfeatured', 'featured');
14 + }
15 +
16 + public function featured()
17 + {
18 + // Check for request forgeries
19 + $this->checkToken();
20 +
21 + $ids = $this->input->get('cid', array(), 'array');
22 + $values = array('featured' => 1, 'unfeatured' => 0);
23 + $task = $this->getTask();
24 + $value = ArrayHelper::getValue($values, $task, 0, 'int');
25 +
26 + $model = $this->getModel();
27 +
28 + // Access checks.
29 + foreach ($ids as $i => $id)
30 + {
31 + $item = $model->getItem($id);
32 +
33 + if (!$this->app->getIdentity()->authorise('core.edit.state'
, 'com_foos.category.' . (int) $item->catid))
34 + {
35 + // Prune items that you can't change.
36 + unset($ids[$i]);
37 + $this->app->enqueueMessage(Text::_('
JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice');
38 + }
39 + }
40 +
41 + if (empty($ids))
42 + {
43 + $this->app->enqueueMessage(Text::_('
COM_FOOS_NO_ITEM_SELECTED'), 'warning');
44 + }
45 + else
46 + {
47 + // Publish the items.
48 + if (!$model->featured($ids, $value))
49 + {
50 + $this->app->enqueueMessage($model->getError(), 'warning
');
51 + }
52 +
53 + if ($value == 1)
54 + {
55 + $message = Text::plural('COM_FOOS_N_ITEMS_FEATURED',
count($ids));
56 + }
57 + else
58 + {
59 + $message = Text::plural('COM_FOOS_N_ITEMS_UNFEATURED',
count($ids));
60 + }
61 + }
62 +
63 + $this->setRedirect('index.php?option=com_foos&view=foos',
$message);
64 }
In the model of an element we implement the method with which the assignment of the property
(data) featured is saved and changed.
administrator/components/com_foos/src/Model/FooModel.php
1 use Joomla\CMS\Language\Associations;
2 use Joomla\CMS\MVC\Model\AdminModel;
3 use Joomla\CMS\Language\LanguageHelper;
4 +use Joomla\Database\ParameterType;
5 +use Joomla\Utilities\ArrayHelper;
6
7
8 return $item;
9 }
10
11 + public function featured($pks, $value = 0)
12 + {
13 + // Sanitize the ids.
14 + $pks = ArrayHelper::toInteger((array) $pks);
15 +
16 + if (empty($pks))
17 + {
18 + $this->setError(Text::_('COM_FOOS_NO_ITEM_SELECTED'));
19 +
20 + return false;
21 + }
22 +
23 + $table = $this->getTable();
24 +
25 + try
26 + {
27 + $db = $this->getDbo();
28 +
29 + $query = $db->getQuery(true);
30 + $query->update($db->quoteName('#__foos_details'));
31 + $query->set($db->quoteName('featured') . ' = :featured');
32 + $query->whereIn($db->quoteName('id'), $pks);
33 + $query->bind(':featured', $value, ParameterType::INTEGER);
34 +
35 + $db->setQuery($query);
36 +
37 + $db->execute();
38 + }
39 + catch (\Exception $e)
40 + {
41 + $this->setError($e->getMessage());
42 +
43 + return false;
44 + }
45 +
46 + $table->reorder();
47 +
48 + // Clean component's cache
49 + $this->cleanCache();
50 +
51 + return true;
52 + }
53 +
In the list view model, we make the necessary adjustments to the database query.
administrator/components/com_foos/src/Model/FoosModel.php
1 'published', 'a.published',
2 'access', 'a.access', 'access_level',
3 'ordering', 'a.ordering',
4 + 'featured', 'a.featured',
5 'language', 'a.language', 'language_title',
6 'publish_up', 'a.publish_up',
7 'publish_down', 'a.publish_down',
8
9
10 parent::__construct($config);
11 }
12 +
13
14 ', a.checked_out_time' .
15 ', a.language' .
16 ', a.ordering' .
17 + ', a.featured' .
18 ', a.state' .
19 ', a.published' .
20 ', a.publish_up, a.publish_down'
21
22 }
23 }
24
25 + // Filter by featured.
26 + $featured = (string) $this->getState('filter.featured');
27 +
28 + if (in_array($featured, ['0','1']))
29 + {
30 + $query->where($db->quoteName('a.featured') . ' = ' . (int)
$featured);
31 + }
32 +
33 // Add the list ordering clause.
34 $orderCol = $this->state->get('list.ordering', 'a.name');
35 $orderDirn = $this->state->get('list.direction', 'asc');
administrator/components/com_foos/src/Service/HTML/AdministratorService.php
1 use Joomla\CMS\Language\Text;
2 use Joomla\CMS\Layout\LayoutHelper;
3 use Joomla\CMS\Router\Route;
4 +use Joomla\Utilities\ArrayHelper;
5
6
7 $html = LayoutHelper::render('joomla.content.associations',
$items);
8 }
9
10 + return $html;
11 + }
12 + public function featured($value, $i, $canChange = true)
13 + {
14 + // Array of image, task, title, action
15 + $states = array(
16 + 0 => array('unfeatured', 'foos.featured', '
COM_CONTACT_UNFEATURED', 'JGLOBAL_ITEM_FEATURE'),
17 + 1 => array('featured', 'foos.unfeatured', 'JFEATURED', '
JGLOBAL_ITEM_UNFEATURE'),
18 + );
19 + $state = ArrayHelper::getValue($states, (int) $value, $states
[1]);
20 + $icon = $state[0] === 'featured' ? 'star featured' : 'star';
21 +
22 + if ($canChange)
23 + {
24 + $html = '<a href="#" onclick="return Joomla.listItemTask(\'
cb' . $i . '\',\'' . $state[1] . '\')" class="tbody-icon'
25 + . ($value == 1 ? ' active' : '') . '" aria-labelledby="
cb' . $i . '-desc">'
26 + . '<span class="fas fa-' . $icon . '" aria-hidden="true
"></span></a>'
27 + . '<div role="tooltip" id="cb' . $i . '-desc">' . Text
::_($state[3]);
28 + }
29 + else
30 + {
31 + $html = '<a class="tbody-icon disabled' . ($value == 1 ? '
active' : '')
32 + . '" title="' . Text::_($state[2]) . '"><span class="
fas fa-' . $icon . '" aria-hidden="true"></span></a>';
33 + }
34 +
35 return $html;
36 }
37 }
We add to the toolbar. featured should also be editable here via an action.
administrator/components/com_foos/src/View/Foos/HtmlView.php
1 $childBar = $dropdown->getChildToolbar();
2 $childBar->publish('foos.publish')->listCheck(true);
3 $childBar->unpublish('foos.unpublish')->listCheck(true);
4 +
5 + $childBar->standardButton('featured')
6 + ->text('JFEATURE')
7 + ->task('foos.featured')
8 + ->listCheck(true);
9 + $childBar->standardButton('unfeatured')
10 + ->text('JUNFEATURE')
11 + ->task('foos.unfeatured')
12 + ->listCheck(true);
13 +
14 $childBar->archive('foos.archive')->listCheck(true);
15
16 if ($user->authorise('core.admin'))
In the form for creating or editing an element, we insert the command that creates a field using the
XML file.
administrator/components/com_foos/tmpl/foo/edit.php
administrator/components/com_foos/tmpl/foos/default.php
Copy the files in the administrator folder into the administrator folder of your Joomla 4 installa-
tion.
Copy the files in the components folder into the components folder of your Joomla 4 installation.
2. the database has been changed, so it is necessary to update it. Open the System |
Information | Database section as described in part Publish and Unpublish. Select
your component and click on Update Structure.
3. Open the view of your component in the administration area. The list contains a column that is
overwritten with featured.
Open an item in the edit view and make sure that you are offered the attribute featured for editing.
5. switch to the frontend and make sure that only items are displayed under the menu item
’featured for which the attribute is set and for which the item and the corresponding category
is also published.
The administration area has filled up. I have inserted the individual parameters without structure so
far. It is user-friendly if the views in an application are uniform. That way, everyone can quickly find
their way around. It is not necessary to familiarise oneself with every new extension. In this part of
the tutorial we will tidy up the view in the administration area. Our aim is to adapt the display to the
standard views in the content management system. As in the following picture, your backend looks
tidy and “Joomla-like”.
For impatient people: Look at the changed programme code in the Diff Viewa and copy these
changes into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t24...t24b
Nothing new
We replace the previously rudimentary form fields. The result is a view that resembles the normal
Joomla extensions.
21 + </div>
22 + </div>
23 + </div>
24 + <div class="col-lg-3">
25 + <div class="card">
26 + <div class="card-body">
27 + <?php echo LayoutHelper::render('joomla.edit.
global', $this); ?>
28 </div>
29 </div>
30 </div>
31
32
33 <?php echo LayoutHelper::render('joomla.edit.params', $this);
?>
34
35 + <?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'publishing',
Text::_('JGLOBAL_FIELDSET_PUBLISHING')); ?>
36 + <div class="row">
37 + <div class="col-md-6">
38 + <fieldset id="fieldset-publishingdata" class="options-
form">
39 + <legend><?php echo Text::_('
JGLOBAL_FIELDSET_PUBLISHING'); ?></legend>
40 + <div>
41 + <?php echo LayoutHelper::render('joomla.edit.
publishingdata', $this); ?>
42 + </div>
43 + </fieldset>
44 + </div>
45 + </div>
46 + <?php echo HTMLHelper::_('uitab.endTab'); ?>
47 +
48 <?php echo HTMLHelper::_('uitab.endTabSet'); ?>
49 </div>
50 <input type="hidden" name="task" value="">
The main change is that we now use Joomla’s own layout joomla.edit.publishingdata. This is
in the directory /layouts/joomla/edit/publishingdata.php and you can check the content
in the following code example. Besides the uniform view, another advantage is that the layout file
is maintained by Joomla and you are therefore less likely to experience unpleasant surprises when
updating.
1 <?php
2
3 defined('_JEXEC') or die;
4
5 $form = $displayData->getForm();
6
7 $fields = $displayData->get('fields') ?: array(
8 'publish_up',
9 'publish_down',
10 'featured_up',
11 'featured_down',
12 array('created', 'created_time'),
13 array('created_by', 'created_user_id'),
14 'created_by_alias',
15 array('modified', 'modified_time'),
16 array('modified_by', 'modified_user_id'),
17 'version',
18 'hits',
19 'id'
20 );
21
22 $hiddenFields = $displayData->get('hidden_fields') ?: array();
23
24 foreach ($fields as $field) {
25 foreach ((array) $field as $f) {
26 if ($form->getField($f)) {
27 if (in_array($f, $hiddenFields)) {
28 $form->setFieldAttribute($f, 'type', 'hidden');
29 }
30
31 echo $form->renderField($f);
32 break;
33 }
34 }
35 }
Copy the files in the administrator folder into the administrator folder of your Joomla 4 installa-
tion.
A new installation is not necessary. Continue using the files from the previous part.
2. Open the view of your component in the administration area. Edit an item and make sure that
the display is as you expect it to be in Joomla. You can see an example in the picture at the
beginning of this chapter.
There are several reasons for allowing a user to edit in the frontend. For one thing, users feel that
working directly on the website is more user-friendly than logging into the backend. Or, it is important
for an administrator not to release access to the administration area. Therefore, in the next step, we
equip our component with the possibility to edit items in the frontend.
For impatient people: Look at the changed programme code in the Diff Viewa and copy these
changes into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t24b...t25
The following file contains all the information needed to display an icon used to open the edit in the
frontend - provided the viewer is allowed to edit.
administrator/components/com_foos/src/Service/HTML/Icon.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Administrator\Service\HTML;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\Application\CMSApplication;
9 use Joomla\CMS\Factory;
10 use Joomla\CMS\HTML\HTMLHelper;
11 use Joomla\CMS\Language\Text;
12 use Joomla\CMS\Layout\LayoutHelper;
13 use Joomla\CMS\Router\Route;
14 use Joomla\CMS\Uri\Uri;
15 use FooNamespace\Component\Foos\Site\Helper\RouteHelper;
16 use Joomla\Registry\Registry;
17
18 class Icon
19 {
20 private $application;
21
22 public function __construct(CMSApplication $application)
23 {
24 $this->application = $application;
25 }
26
27 public static function create($category, $params, $attribs = [])
28 {
29 $uri = Uri::getInstance();
30
31 $url = 'index.php?option=com_foos&task=foo.add&return=' .
base64_encode($uri) . '&id=0&catid=' . $category->id;
32
33 $text = LayoutHelper::render('joomla.content.icons.create', ['
params' => $params, 'legacy' => false]);
34
35 // Add the button classes to the attribs array
36 if (isset($attribs['class'])) {
37 $attribs['class'] .= ' btn btn-primary';
38 } else {
39 $attribs['class'] = 'btn btn-primary';
40 }
41
42 $button = HTMLHelper::_('link', Route::_($url), $text, $attribs
);
43
44 $output = '<span class="hasTooltip" title="' . HTMLHelper::_('
tooltipText', 'COM_FOOS_CREATE_FOO') . '">' . $button . '</
span>';
45
46 return $output;
47 }
48
49 public static function edit($foo, $params, $attribs = [], $legacy =
false)
50 {
51 $user = Factory::getUser();
52 $uri = Uri::getInstance();
53
54 // Ignore if in a popup window.
55 if ($params && $params->get('popup')) {
56 return '';
57 }
58
59 // Ignore if the state is negative (trashed).
60 if ($foo->published < 0) {
61 return '';
62 }
63
64 // Set the link class
65 $attribs['class'] = 'dropdown-item';
66
67 // Show checked_out icon if the foo is checked out by a
different user
68 if (property_exists($foo, 'checked_out')
69 && property_exists($foo, 'checked_out_time')
70 && $foo->checked_out > 0
71 && $foo->checked_out != $user->get('id')) {
72 $checkoutUser = Factory::getUser($foo->checked_out);
73 $date = HTMLHelper::_('date', $foo->
checked_out_time);
74 $tooltip = Text::_('JLIB_HTML_CHECKED_OUT') . ' :: ' .
Text::sprintf('COM_FOOS_CHECKED_OUT_BY', $checkoutUser
->name)
75 . ' <br /> ' . $date;
76
77 $text = LayoutHelper::render('joomla.content.icons.
edit_lock', ['tooltip' => $tooltip, 'legacy' => $legacy
]);
78
79 $output = HTMLHelper::_('link', '#', $text, $attribs);
80
81 return $output;
82 }
83
84 if (!isset($foo->slug)) {
85 $foo->slug = "";
86 }
87
88 $fooUrl = RouteHelper::getFooRoute($foo->slug, $foo->catid,
$foo->language);
89 $url = $fooUrl . '&task=foo.edit&id=' . $foo->id . '&
return=' . base64_encode($uri);
90
91 if ($foo->published == 0) {
92 $overlib = Text::_('JUNPUBLISHED');
93 } else {
94 $overlib = Text::_('JPUBLISHED');
95 }
96
97 if (!isset($foo->created)) {
98 $date = HTMLHelper::_('date', 'now');
99 } else {
100 $date = HTMLHelper::_('date', $foo->created);
101 }
102
103 if (!isset($created_by_alias) && !isset($foo->created_by)) {
We adapt the XML file that Joomla uses to build the form.
components/com_foos/forms/foo.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <form>
4 <fieldset
5 addruleprefix="FooNamespace\Component\Foos\Administrator\Rule"
6 addfieldprefix="FooNamespace\Component\Foos\Administrator\Field
"
7 >
8 <field
9 name="id"
10 type="number"
11 label="JGLOBAL_FIELD_ID_LABEL"
12 default="0"
13 class="readonly"
14 readonly="true"
15 />
16
17 <field
18 name="name"
19 type="text"
20 validate="Letter"
21 class="validate-letter"
22 label="COM_FOOS_FIELD_NAME_LABEL"
23 size="40"
24 required="true"
25 />
26
27 <field
28 name="alias"
29 type="text"
30 label="JFIELD_ALIAS_LABEL"
31 size="45"
32 hint="JFIELD_ALIAS_PLACEHOLDER"
33 />
34 </fieldset>
35 <fieldset name="language" label="JFIELD_LANGUAGE_LABEL">
36 <field
37 name="language"
38 type="contentlanguage"
39 label="JFIELD_LANGUAGE_LABEL"
40 >
41 <option value="*">JALL</option>
42 </field>
43 </fieldset>
44 <fieldset name="publishing" label="JGLOBAL_FIELDSET_PUBLISHING">
45 <field
46 name="featured"
47 type="list"
48 label="JFEATURED"
49 default="0"
50 validate="options"
51 >
52 <option value="0">JNO</option>
53 <option value="1">JYES</option>
54 </field>
55
56 <field
57 name="published"
58 type="list"
59 label="JSTATUS"
60 default="1"
61 id="published"
62 class="custom-select-color-state"
63 size="1"
64 >
65 <option value="1">JPUBLISHED</option>
66 <option value="0">JUNPUBLISHED</option>
67 <option value="2">JARCHIVED</option>
68 <option value="-2">JTRASHED</option>
69 </field>
70
71 <field
72 name="publish_up"
73 type="calendar"
74 label="COM_FOOS_FIELD_PUBLISH_UP_LABEL"
75 translateformat="true"
76 showtime="true"
77 size="22"
78 filter="user_utc"
79 />
80
81 <field
82 name="publish_down"
83 type="calendar"
84 label="COM_FOOS_FIELD_PUBLISH_DOWN_LABEL"
85 translateformat="true"
86 showtime="true"
87 size="22"
88 filter="user_utc"
89 />
90
91 <field
92 name="catid"
93 type="categoryedit"
94 label="JCATEGORY"
95 extension="com_foos"
96 addfieldprefix="Joomla\Component\Categories\Administrator\
Field"
97 required="true"
98 default=""
99 />
100
101 <field
102 name="access"
103 type="accesslevel"
104 label="JFIELD_ACCESS_LABEL"
105 size="1"
106 />
107
108 <field
109 name="checked_out"
110 type="hidden"
111 filter="unset"
112 />
113
114 <field
115 name="checked_out_time"
116 type="hidden"
117 filter="unset"
118 />
119 </fieldset>
120 <fields name="params" label="JGLOBAL_FIELDSET_DISPLAY_OPTIONS">
121 <fieldset name="display" label="
JGLOBAL_FIELDSET_DISPLAY_OPTIONS">
122 <field
123 name="show_name"
124 type="list"
125 label="COM_FOOS_FIELD_PARAMS_NAME_LABEL"
126 useglobal="true"
127 >
128 <option value="0">JHIDE</option>
129 <option value="1">JSHOW</option>
130 </field>
131
132 <field
133 name="foos_layout"
134 type="componentlayout"
135 label="JFIELD_ALT_LAYOUT_LABEL"
136 class="custom-select"
137 extension="com_foos"
138 view="foo"
139 useglobal="true"
140 />
141 </fieldset>
142 </fields>
143 </form>
31.1.1.3. components/com_foos/src/Controller/FooController.php
Note the function save. This is not usual in the FormController, because Joomla takes care of
everything for you. Since the ID is first created when an element is created and is therefore not
known, Joomla forwards to the overview page after creation. We have not yet created this in the
frontend. That is why I have changed this function here.
components/com_foos/src/Controller/FooController.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Site\Controller;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\Factory;
9 use Joomla\CMS\MVC\Controller\FormController;
10 use Joomla\CMS\Router\Route;
11 use Joomla\CMS\Uri\Uri;
12 use Joomla\Utilities\ArrayHelper;
13
14 class FooController extends FormController
15 {
16 protected $view_item = 'form';
17
18 public function getModel($name = 'form', $prefix = '', $config = ['
ignore_request' => true])
19 {
20 return parent::getModel($name, $prefix, ['ignore_request' =>
false]);
21 }
22
23 protected function allowAdd($data = [])
24 {
25 if ($categoryId = ArrayHelper::getValue($data, 'catid', $this->
input->getInt('catid'), 'int')) {
26 $user = Factory::getUser();
27
28 // If the category has been passed in the data or URL check
it.
29 return $user->authorise('core.create', 'com_foos.category.'
. $categoryId);
30 }
31
32 // In the absence of better information, revert to the
component permissions.
33 return parent::allowAdd();
34 }
35
36 protected function allowEdit($data = [], $key = 'id')
37 {
38 $recordId = (int) isset($data[$key]) ? $data[$key] : 0;
39
40 if (!$recordId) {
41 return false;
42 }
43
44 // Need to do a lookup from the model.
45 $record = $this->getModel()->getItem($recordId);
46 $categoryId = (int) $record->catid;
47
48 if ($categoryId) {
49 $user = Factory::getUser();
50
51 // The category has been set. Check the category
permissions.
52 if ($user->authorise('core.edit', $this->option . '.
category.' . $categoryId)) {
53 return true;
54 }
55
56 // Fallback on edit.own.
57 if ($user->authorise('core.edit.own', $this->option . '.
category.' . $categoryId)) {
58 return ($record->created_by == $user->id);
59 }
60
61 return false;
62 }
63
64 // Since there is no asset tracking, revert to the component
permissions.
65 return parent::allowEdit($data, $key);
66 }
67
68 public function save($key = null, $urlVar = null)
69 {
70 $result = parent::save($key, $urlVar = null);
71
72 $this->setRedirect(Route::_($this->getReturnPage(), false));
73
74 return $result;
75 }
76
77 public function cancel($key = null)
78 {
79 $result = parent::cancel($key);
80
81 $this->setRedirect(Route::_($this->getReturnPage(), false));
82
83 return $result;
84 }
85
86 protected function getRedirectToItemAppend($recordId = 0, $urlVar =
'id')
87 {
88 // Need to override the parent method completely.
89 $tmpl = $this->input->get('tmpl');
90
91 $append = '';
92
93 // Setup redirect info.
94 if ($tmpl) {
95 $append .= '&tmpl=' . $tmpl;
96 }
97
98 $append .= '&layout=edit';
99
100 $append .= '&' . $urlVar . '=' . (int) $recordId;
101
102 $itemId = $this->input->getInt('Itemid');
103 $return = $this->getReturnPage();
104 $catId = $this->input->getInt('catid');
105
106 if ($itemId) {
107 $append .= '&Itemid=' . $itemId;
108 }
109
110 if ($catId) {
111 $append .= '&catid=' . $catId;
112 }
113
114 if ($return) {
115 $append .= '&return=' . base64_encode($return);
116 }
117
118 return $append;
119 }
120
121 protected function getReturnPage()
122 {
123 $return = $this->input->get('return', null, 'base64');
124
125 if (empty($return) || !Uri::isInternal(base64_decode($return)))
{
126 return Uri::base();
127 }
128
129 return base64_decode($return);
130 }
131 }
31.1.1.4. components/com_foos/src/Model/FormModel.php
components/com_foos/src/Model/FormModel.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Site\Model;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\Factory;
9 use Joomla\CMS\Form\Form;
10 use Joomla\CMS\Language\Associations;
11 use Joomla\CMS\Language\Multilanguage;
12 use Joomla\Registry\Registry;
13 use Joomla\Utilities\ArrayHelper;
14
15 class FormModel extends \FooNamespace\Component\Foos\Administrator\
Model\FooModel
16 {
17 public $typeAlias = 'com_foos.foo';
18
19 protected $formName = 'form';
20
21 public function getForm($data = [], $loadData = true)
22 {
23 $form = parent::getForm($data, $loadData);
24
25 // Prevent messing with article language and category when
editing existing foo with associations
26 if ($id = $this->getState('foo.id') && Associations::isEnabled
()) {
27 $associations = Associations::getAssociations('com_foos', '
#__foos_details', 'com_foos.item', $id);
28
29 // Make fields read only
30 if (!empty($associations)) {
31 $form->setFieldAttribute('language', 'readonly', 'true'
);
32 $form->setFieldAttribute('language', 'filter', 'unset')
;
33 }
34 }
35
36 return $form;
37 }
38
39 public function getItem($itemId = null)
40 {
41 $itemId = (int) (!empty($itemId)) ? $itemId : $this->getState('
foo.id');
42
43 // Get a row instance.
44 $table = $this->getTable();
45
46 // Attempt to load the row.
47 try {
48 if (!$table->load($itemId)) {
49 return false;
50 }
51 } catch (Exception $e) {
52 Factory::getApplication()->enqueueMessage($e->getMessage())
;
53
54 return false;
55 }
56
57 $properties = $table->getProperties();
58 $value = ArrayHelper::toObject($properties, 'JObject');
59
60 // Convert field to Registry.
61 $value->params = new Registry($value->params);
62
63 return $value;
64 }
65
66 public function getReturnPage()
67 {
68 return base64_encode($this->getState('return_page'));
69 }
70
71 public function save($data)
72 {
73 // Associations are not edited in frontend ATM so we have to
inherit them
74 if (Associations::isEnabled() && !empty($data['id'])
75 && $associations = Associations::getAssociations('com_foos'
, '#__foos_details', 'com_foos.item', $data['id'])) {
76 foreach ($associations as $tag => $associated) {
77 $associations[$tag] = (int) $associated->id;
78 }
79
80 $data['associations'] = $associations;
81 }
82
83 return parent::save($data);
84 }
85
86 protected function populateState()
87 {
88 $app = Factory::getApplication();
89
90 // Load state from the request.
91 $pk = $app->input->getInt('id');
92 $this->setState('foo.id', $pk);
93
94 $this->setState('foo.catid', $app->input->getInt('catid'));
95
96 $return = $app->input->get('return', null, 'base64');
97 $this->setState('return_page', base64_decode($return));
98
99 // Load the parameters.
100 $params = $app->getParams();
101 $this->setState('params', $params);
102
103 $this->setState('layout', $app->input->getString('layout'));
104 }
105
106 protected function preprocessForm(Form $form, $data, $group = 'foo'
)
107 {
108 if (!Multilanguage::isEnabled()) {
109 $form->setFieldAttribute('language', 'type', 'hidden');
110 $form->setFieldAttribute('language', 'default', '*');
111 }
112
113 return parent::preprocessForm($form, $data, $group);
114 }
115
116 public function getTable($name = 'Foo', $prefix = 'Administrator',
$options = [])
117 {
118 return parent::getTable($name, $prefix, $options);
119 }
120 }
31.1.1.5. components/com_foos/src/View/Form/HtmlView.php
components/com_foos/src/View/Form/HtmlView.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Site\View\Form;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\Factory;
9 use Joomla\CMS\Language\Multilanguage;
10 use Joomla\CMS\Language\Text;
55 if (count($errors = $this->get('Errors'))) {
56 $app->enqueueMessage(implode("\n", $errors), 'error');
57
58 return false;
59 }
60
61 // Create a shortcut to the parameters.
62 $this->params = $this->state->params;
63
64 // Escape strings for HTML output
65 $this->pageclass_sfx = htmlspecialchars($this->params->get('
pageclass_sfx'));
66
67 // Override global params with foo specific params
68 $this->params->merge($this->item->params);
69
70 // Propose current language as default when creating new foo
71 if (empty($this->item->id) && Multilanguage::isEnabled()) {
72 $lang = Factory::getLanguage()->getTag();
73 $this->form->setFieldAttribute('language', 'default', $lang
);
74 }
75
76 $this->_prepareDocument();
77
78 parent::display($tpl);
79 }
80
81 protected function _prepareDocument()
82 {
83 $app = Factory::getApplication();
84 $menus = $app->getMenu();
85 $title = null;
86
87 // Because the application sets a default page title,
88 // we need to get it from the menu item itself
89 $menu = $menus->getActive();
90
91 if ($menu) {
92 $this->params->def('page_heading', $this->params->get('
page_title', $menu->title));
93 } else {
94 $this->params->def('page_heading', Text::_('
COM_FOOS_FORM_EDIT_FOO'));
95 }
96
97 $title = $this->params->def('page_title', Text::_('
COM_FOOS_FORM_EDIT_FOO'));
98
99 if ($app->get('sitename_pagetitles', 0) == 1) {
100 $title = Text::sprintf('JPAGETITLE', $app->get('sitename'),
$title);
101 } else if ($app->get('sitename_pagetitles', 0) == 2) {
102 $title = Text::sprintf('JPAGETITLE', $title, $app->get('
sitename'));
103 }
104
105 $this->document->setTitle($title);
106
107 $pathway = $app->getPathWay();
108 $pathway->addItem($title, '');
109 }
110 }
In the code example above, I have used the code in Joomla as a guide when checking the permissions.
If someone is not authorised, a message is displayed. Depending on the environment in which the
extension is programmed, it is more user-friendly to offer a login option immediately. In this case: Place
in the file components/com_foos/src/View/Form/HtmlView.php the following code excerpt
instead of this
If the authorisation check fails, you are immediately redirected to the registration form.
components/com_foos/tmpl/form/edit.php
1
2 <?php
3
4 \defined('_JEXEC') or die;
5
6 use Joomla\CMS\Factory;
7 use Joomla\CMS\Language\Multilanguage;
8 use Joomla\CMS\HTML\HTMLHelper;
9 use Joomla\CMS\Language\Associations;
10 use Joomla\CMS\Router\Route;
11 use Joomla\CMS\Language\Text;
12 use Joomla\CMS\Layout\LayoutHelper;
13
14 HTMLHelper::_('behavior.keepalive');
15 HTMLHelper::_('behavior.formvalidator');
16 HTMLHelper::_('script', 'com_foos/admin-foos-letter.js', ['version' =>
'auto', 'relative' => true]);
17
18 $this->tab_name = 'com-foos-form';
19 $this->ignore_fieldsets = ['details', 'item_associations', 'language'];
20 $this->useCoreUI = true;
21 ?>
22 <form action="<?php echo Route::_('index.php?option=com_foos&id=' . (
int) $this->item->id); ?>" method="post" name="adminForm" id="
adminForm" class="form-validate form-vertical">
23 <fieldset>
24 <?php echo HTMLHelper::_('uitab.startTabSet', $this->tab_name,
['active' => 'details']); ?>
25 <?php echo HTMLHelper::_('uitab.addTab', $this->tab_name, '
details', empty($this->item->id) ? Text::_('COM_FOOS_NEW_FOO
') : Text::_('COM_FOOS_EDIT_FOO')); ?>
26 <?php echo $this->form->renderField('name'); ?>
27
28 <?php if (is_null($this->item->id)) : ?>
29 <?php echo $this->form->renderField('alias'); ?>
30 <?php endif; ?>
31 <?php echo $this->form->renderFieldset('details'); ?>
32 <?php echo HTMLHelper::_('uitab.endTab'); ?>
33
34 <?php if (Multilanguage::isEnabled()) : ?>
35 <?php echo HTMLHelper::_('uitab.addTab', $this->
tab_name, 'language', Text::_('JFIELD_LANGUAGE_LABEL
')); ?>
36 <?php echo $this->form->renderField('language'); ?>
37 <?php echo HTMLHelper::_('uitab.endTab'); ?>
38 <?php else : ?>
39 <?php echo $this->form->renderField('language'); ?>
40 <?php endif; ?>
41
42 <?php echo LayoutHelper::render('joomla.edit.params', $this);
?>
43 <?php echo HTMLHelper::_('uitab.endTabSet'); ?>
44
45 <input type="hidden" name="task" value=""/>
46 <input type="hidden" name="return" value="<?php echo $this->
return_page; ?>"/>
47 <?php echo HTMLHelper::_('form.token'); ?>
48 </fieldset>
49 <div class="mb-2">
50 <button type="button" class="btn btn-primary" onclick="Joomla.
submitbutton('foo.save')">
51 <span class="fas fa-check" aria-hidden="true"></span>
52 <?php echo Text::_('JSAVE'); ?>
53 </button>
54 <button type="button" class="btn btn-danger" onclick="Joomla.
submitbutton('foo.cancel')">
55 <span class="fas fa-times-cancel" aria-hidden="true"></span
>
56 <?php echo Text::_('JCANCEL'); ?>
57 </button>
58 </div>
59 </form>
Last but not least we need the file components/com_foos/tmpl/form/edit.xml to create the
menu item.
components/com_foos/tmpl/form/edit.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <metadata>
4 <layout title="COM_FOOS_FORM_VIEW_DEFAULT_TITLE">
5 <help
6 key="JHELP_MENUS_MENU_ITEM_FOO_CREATE"
7 />
8 <message>
9 <![CDATA[COM_FOOS_FORM_VIEW_DEFAULT_DESC]]>
10 </message>
11 </layout>
12 <fields name="params">
13
14 </fields>
15 </metadata>
administrator/components/com_foos/src/Extension/FoosComponent.php
1 defined('JPATH_PLATFORM') or die;
2
3 +use Joomla\CMS\Application\SiteApplication;
4 use Joomla\CMS\Association\AssociationServiceInterface;
5 use Joomla\CMS\Association\AssociationServiceTrait;
6 use Joomla\CMS\Categories\CategoryServiceInterface;
7
8 use Joomla\CMS\Extension\MVCComponent;
9 use Joomla\CMS\HTML\HTMLRegistryAwareTrait;
10 use FooNamespace\Component\Foos\Administrator\Service\HTML\
AdministratorService;
11 +use FooNamespace\Component\Foos\Administrator\Service\HTML\Icon;
12 use Psr\Container\ContainerInterface;
13 use Joomla\CMS\Helper\ContentHelper;
14
15
16 public function boot(ContainerInterface $container)
17 {
18 $this->getRegistry()->register('foosadministrator', new
AdministratorService);
19 + $this->getRegistry()->register('fooicon', new Icon($container->
get(SiteApplication::class)));
20 }
We extend the template for the view: If you are allowed to edit the element if ($canEdit), then you
see the icon to open the form.
components/com_foos/tmpl/foo/default.php
1 \defined('_JEXEC') or die;
2
3 +use Joomla\CMS\Factory;
4 +use Joomla\CMS\Helper\ContentHelper;
5 use Joomla\CMS\Language\Text;
6
7 -if ($this->item->params->get('show_name')) {
8 +$canDo = ContentHelper::getActions('com_foos', 'category', $this->
item->catid);
9 +$canEdit = $canDo->get('core.edit') || ($canDo->get('core.edit.own')
&& $this->item->created_by == Factory::getUser()->id);
10 +$tparams = $this->item->params;
11
12 +if ($tparams->get('show_name')) {
13 if ($this->params->get('show_foo_name_label')) {
14 echo Text::_('COM_FOOS_NAME');
15 }
16
17 echo $this->item->name;
18 }
19 +?>
20
21 +<?php if ($canEdit) : ?>
22 + <div class="icons">
23 + <div class="btn-group float-right">
24 + <button class="btn btn-secondary dropdown-toggle" type="
button" id="dropdownMenuButton-<?php echo $this->item->id; ?>"
25 + aria-label="<?php echo JText::_('JUSER_TOOLS'); ?>"
26 + data-toggle="dropdown" aria-haspopup="true" aria-
expanded="false">
27 + <span class="fa fa-cog" aria-hidden="true"></span>
28 + </button>
29 + <ul class="dropdown-menu" aria-labelledby="
dropdownMenuButton-<?php echo $this->item->id; ?>">
30 + <li class="edit-icon"> <?php echo JHtml::_('fooicon.
edit', $this->item, $tparams); ?> </li>
31 + </ul>
32 + </div>
33 + </div>
34 +<?php endif; ?>
35 +
36 +<?php
37 echo $this->item->event->afterDisplayTitle;
38 echo $this->item->event->beforeDisplayContent;
39 echo $this->item->event->afterDisplayContent;
Tip: Do you want a user to be redirected to the finished view of an item after it has been created?
This is only possible in a indirect way. Because you don’t know the ID when you create it, you
have to ask for it. Since we extend the model classes of Joomla-Core, we can access the ID via the
model in the postSaveHook() method of the controller. Concretely, in the file src/components
/com_foos/src/Controller/FooController.php the following code could be used to set
up the redirection:
1 ...
2 protected function postSaveHook(\Joomla\CMS\MVC\Model\BaseDatabaseModel
$model, $validData = [])
3 {
4 $id = $model->getState($model->getName() . '.id');
5 $this->setRedirect(Route::_('index.php?option=com_agosms&view=foo&
id=' . $id, false));
6 return $id;
7 }
8 ...
Copy the files in the administrator folder into the administrator folder of your Joomla 4 installa-
tion.
Copy the files in the components folder into the components folder of your Joomla 4 installation.
Install your component as described in part one, after you have copied all the files. Joomla will update
the namespaces for you during the installation. Since a new file has been added, this is necessary.
2. Create a menu item to change a Foo element and one that displays a Foo element.
Figure 31.1.: Joomla Frontend Editing Menu Item to Create a Foo Element
3. Open the menu item to create a Foo element in the frontend. Make sure you have the necessary
rights. If you have left the default rights, you must log in with a user who is at least an author.
Make sure that you can create an element.
4. Make sure that you see the edit icon in the detail view of an element and that an element is
editable.
31.3. Links
1
github.com/joomla/joomla-cms/pull/24311
Why use categories? Categories are often used when there are many posts on a site. With the help
of categories, they can be grouped and managed more easily. Example: In the component content,
articles can be filtered by category. If there are 200 articles on the site, it is easier to find a post if you
know its category.
For the frontend, there are built-in menu item types in Joomla that use categories: Category Blog and
Category List. The menu item types or layouts simplify the display of posts in a category. When a new
post is assigned to the category, it automatically appears on the page. This display is configurable. For
example, imagine a blog layout of the “Events” category that displays the latest articles first on the site.
When a new article is added to this category, it will automatically appear at the top of the Events blog.
All you have to do is add the post to the category. The category structure, for example “Events | Online
Events | Sports | Yoga”, is completely independent of the site’s menu structure. The site can have one
or six menu levels and Yoga can be placed as a menu item in the first level.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t25...t26
Categories1 are a way to organize content in Joomla. A category contains items and other categories.
An item can belong to only one category. If a category is contained in another, it is a subcategory of
that category. Does it happen in your structure that single elements belong to several subsets? Then
categories are not the right choice. In this case use tags.
1
docs.joomla.org/category
32.1.1.1. components/com_foos/src/Model/CategoryModel.php
The class we use to prepare the data for displaying the category view extends the ListModel class
in the /libraries/src/MVC/Model/ListModel.php file, as does the FeaturedModel class in
components/com_foos/src/Model/FeaturedModel.php. ListModel provides, among other
things, the ability to handle the display of multiple items simultaneously on a web page, including
support for pagination. Below I include my full code, which is derived from com_contact.
components/com_foos/src/Model/CategoryModel.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Site\Model;
5
6 defined('_JEXEC') or die;
7
8 use Joomla\CMS\Categories\Categories;
9 use Joomla\CMS\Categories\CategoryNode;
10 use Joomla\CMS\Component\ComponentHelper;
11 use Joomla\CMS\Factory;
12 use Joomla\CMS\Helper\TagsHelper;
13 use Joomla\CMS\Language\Multilanguage;
14 use Joomla\CMS\MVC\Model\ListModel;
15 use Joomla\CMS\Table\Table;
16 use Joomla\Database\ParameterType;
17 use Joomla\Registry\Registry;
18
19 class CategoryModel extends ListModel
20 {
21 protected $_item = null;
22
23 protected $_articles = null;
24
25 protected $_siblings = null;
26
27 protected $_children = null;
28
29 protected $_parent = null;
30
31 protected $_category = null;
32
33 protected $_categories = null;
34
35 public function __construct($config = [])
36 {
37 if (empty($config['filter_fields'])) {
38 $config['filter_fields'] = [
39 'id', 'a.id',
40 'name', 'a.name',
41 'state', 'a.state',
42 'ordering', 'a.ordering',
43 'featuredordering', 'a.featured'
44 ];
45 }
46
47 parent::__construct($config);
48 }
49
50 public function getItems()
51 {
52 // Invoke the parent getItems method to get the main list
53 $items = parent::getItems();
54
55 if ($items === false) {
56 return false;
57 }
58
59 // Convert the params field into an object, saving original in
_params
60 for ($i = 0, $n = count($items); $i < $n; $i++) {
61 $item = &$items[$i];
62
63 if (!isset($this->_params)) {
64 $item->params = new Registry($item->params);
65 }
66
67 // Some contexts may not use tags data at all, so we allow
callers to disable loading tag data
68 if ($this->getState('load_tags', true)) {
69 $this->tags = new TagsHelper;
70 $this->tags->getItemTags('com_foos.foo', $item->id);
71 }
72 }
73
74 return $items;
75 }
76
77 protected function getListQuery()
78 {
79 $user = Factory::getUser();
80 $groups = $user->getAuthorisedViewLevels();
81
82 // Create a new query object.
83 $db = $this->getDbo();
84 $query = $db->getQuery(true);
85
86 $query->select($this->getState('list.select', 'a.*'))
317 $table->load($pk);
318 $table->hit($pk);
319 }
320
321 return true;
322 }
323 }
32.1.1.2. components/com_foos/src/Service/Category.php
In the Category service for the frontend part we set the specific options for our component.
components/com_foos/src/Service/Category.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Site\Service;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\Categories\Categories;
9
10 class Category extends Categories
11 {
12 public function __construct($options = [])
13 {
14 $options['table'] = '#__foos_details';
15 $options['extension'] = 'com_foos';
16 $options['statefield'] = 'published';
17
18 parent::__construct($options);
19 }
20 }
32.1.1.3. components/com_foos/src/View/Category/HtmlView.php
We handle the category view in the frontend via the file components/com_foos/src/View/
Category/HtmlView.php.
components/com_foos/src/View/Category/HtmlView.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Site\View\Category;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\MVC\View\CategoryView;
9 use FooNamespace\Component\Foos\Site\Helper\RouteHelper;
10
11 class HtmlView extends CategoryView
12 {
13 protected $extension = 'com_foos';
14
15 protected $defaultPageTitle = 'COM_FOO_DEFAULT_PAGE_TITLE';
16
17 protected $viewName = 'foo';
18
19 protected $runPlugins = true;
20
21 public function display($tpl = null)
22 {
23 parent::commonCategoryDisplay();
24
25 $this->pagination->hideEmptyLimitstart = true;
26
27 foreach ($this->items as $item) {
28 $item->slug = $item->id;
29 $temp = $item->params;
30 $item->params = clone $this->params;
31 $item->params->merge($temp);
32 }
33
34 return parent::display($tpl);
35 }
36
37 protected function prepareDocument()
38 {
39 parent::prepareDocument();
40
41 $menu = $this->menu;
42 $id = (int) @$menu->query['id'];
43
44 if ($menu && (!isset($menu->query['option']) || $menu->query['
option'] != $this->extension || $menu->query['view'] ==
$this->viewName
45 || $id != $this->category->id)) {
46 $path = [['title' => $this->category->title, 'link' => ''
]];
47 $category = $this->category->getParent();
48
49 while ((!isset($menu->query['option']) || $menu->query['
option'] !== 'com_foos' || $menu->query['view'] === 'foo
'
50 || $id != $category->id) && $category->id > 1) {
51 $path[] = ['title' => $category->title, 'link' =>
RouteHelper::getCategoryRoute($category->id,
$category->language)];
52 $category = $category->getParent();
53 }
54
55 $path = array_reverse($path);
56
57 foreach ($path as $item) {
58 $this->pathway->addItem($item['title'], $item['link']);
59 }
60 }
61
62 parent::addFeed();
63 }
64 }
32.1.1.4. components/com_foos/tmpl/category/default.php
That we also create a template for the category view is not new. As usual we create the file
default.php in the directory components/com_foos/tmpl/category. We use joomla.
content.category_default here. You can find this layout file in the folder layouts/joomla/
content/category_default.php.
components/com_foos/tmpl/category/default.php
1
2 <?php
3
4 \defined('_JEXEC') or die;
5
6 use Joomla\CMS\Layout\LayoutHelper;
7 ?>
8
9 <div class="com-foo-category">
10 <?php
11 $this->subtemplatename = 'items';
12 echo LayoutHelper::render('joomla.content.category_default',
$this);
13 ?>
14 </div>
32.1.1.5. components/com_foos/tmpl/category/default.xml
Um im Backend auf benutzerfreundliche Art und Weise einen Menüpunkt für die Navigation im Frontend
anlegen zu können, erstellen wir die Datei components/com_foos/tmpl/category/default.xml.
Das haben wir hier im Text vorher schon öfter erledigt. Beispielsweise für ein Element oder für die
Ansicht der Haupteinträge (featured).
components/com_foos/tmpl/category/default.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <metadata>
4 <layout title="COM_FOOS_CATEGORY_VIEW_DEFAULT_TITLE">
5 <help
6 key = "JHELP_MENUS_MENU_ITEM_FOO_CATEGORY"
7 />
8 <message>
9 <![CDATA[COM_FOOS_CATEGORY_VIEW_DEFAULT_DESC]]>
10 </message>
11 </layout>
12
13 <!-- Add fields to the request variables for the layout. -->
14 <fields name="request">
15 <fieldset
16 name="request"
17 addfieldprefix="Joomla\Component\Categories\Administrator\
Field"
18 >
19 <field
20 name="id"
21 type="modal_category"
22 label="JGLOBAL_CHOOSE_CATEGORY_LABEL"
23 extension="com_foos"
24 required="true"
25 select="true"
26 new="true"
27 edit="true"
28 clear="true"
29 />
30 </fieldset>
31 </fields>
32 <fields name="params">
33 <fieldset name="basic" label="JGLOBAL_FIELDSET_DISPLAY_OPTIONS"
>
34 <field
35 name="show_pagination"
36 type="list"
37 label="JGLOBAL_PAGINATION_LABEL"
38 useglobal="true"
39 >
40 <option value="0">JHIDE</option>
41 <option value="1">JSHOW</option>
42 <option value="2">JGLOBAL_AUTO</option>
43 </field>
44
45 <field
46 name="show_pagination_results"
47 type="list"
48 label="JGLOBAL_PAGINATION_RESULTS_LABEL"
49 useglobal="true"
50 class="custom-select-color-state"
51 >
52 <option value="0">JHIDE</option>
53 <option value="1">JSHOW</option>
54 </field>
55 </fieldset>
56 </fields>
57 </metadata>
The category views in Joomla usually have a lot of other parameters. For example, I have ignored
the subcategories and filters. This keeps the example clear. Look up in the core extensions what is
important to you .
If your element is not displayed, it may be because you have set the parameter show_name to no
for the element.
32.1.1.6. components/com_foos/tmpl/category/default_items.php
To make the category view code clear, we work with layouts. In the template components/com_foos
/tmpl/category/default.php we use the layout joomla.content.category_default. This
in turn requires the items layout, which we implement in the file components/com_foos/tmpl/
category/default_items.php. At first glance, this seems cumbersome. In practice, however, it
has proven its worth.
components/com_foos/tmpl/category/default_items.php
1
2 <?php
3
4 \defined('_JEXEC') or die;
5
6 use Joomla\CMS\HTML\HTMLHelper;
7 use Joomla\CMS\Language\Text;
8 use Joomla\CMS\Router\Route;
9 use Joomla\CMS\Uri\Uri;
10 use FooNamespace\Component\Foos\Site\Helper\RouteHelper;
11
12 HTMLHelper::_('behavior.core');
13 ?>
14 <div class="com-foo-category__items">
The view in this chapter is not styled. Since this is a matter of taste - and in my opinion a task
of the template - anyway, I leave the styling to you. I am of the opinion that the layouts of the
categories do not respect the separation of model, view and controller. That’s why discussions
like the one in Issue 32012a keep coming up. Again and again it has to be decided whether the
insertion of a CSS class in the output of a component brings too much dependency and belongs
only in the template - or whether only in this way a user-friendly offer is possible - where the
number of intro articles can be determined in the backend via a user interface.
a
github.com/joomla/joomla-cms/issues/32012
1. install your component in Joomla version 4 to test it: Copy the files in the administrator
folder into the administrator folder of your Joomla 4 installation. Install your component as
described in part one, after copying all files. Joomla will update the namespaces for you during
the installation. Since new files have been added, this is necessary.
2. Create a menu item that displays the elements of a category of our extension.
3. switch to the frontend and make sure that the elements are displayed correctly.
Search engine friendly URLs do not work yet. The URL to the items of the component will appear
in the form JOOMLA/category?view=category&id=8. We use a service to repair this fault. At the
same time, this is a good example to work out what is necessary to integrate a service in a Joomla
extension.
Search Engine Friendly (SEF), human readable1 are URLs that make sense to both humans and search
engines because they explain the path to the specific page. Joomla is able to create URLs in any format.
This does not depend on URL rewriting performed by the web server, so it will work even if Joomla
uses a server other than Apache with the mod_rewrite module. The SEF URLs follow a certain fixed
pattern, but the user can define a short descriptive text alias2 for each segment of the URL.
Internally, the local part of a SEF URL (the part after the domain name) is called the route. The
creation and processing of SEF URLs is therefore called routing, and the corresponding code is
called router.
An example of routing is the URL to the article “Welcome to Joomla” in the sample data. Without
SEF URLs switched on, the URL is /index.php?option=com_content&view=article&id=1:
welcome-to-joomla&catid=1:latest-news&Itemid=50. With SEF-URLs switched on and
mod_rewrite switched off, it is /index.php/the-news/1-latest-news/1-welcome-to-joomla.
With SEF URLs and mod_rewrite turned on, it is /the-news/1-latest-news/1-welcome-to-
joomla.
Search Engine Friendly URLs can be enabled by turning on the Search Engine Friendly URLs option
in the Global configuration. This option is activated by default in Joomla 4. For more information,
see Enabling Search Engine Friendly (SEF) URLs in the documentationa .
a
docs.joomla.org/Enabling_Search_Engine_Friendly_(SEF)_URLs
For impatient people: View the changed program code in the Diff Viewa and copy these changes
1
en.wikipedia.org/wiki/Clean_URL
2
docs.joomla.org/Alias
33.1.1.1. components/com_foos/src/Service/Router.php
components/com_foos/src/Service/Router.php
1
2 <?php
3
4 namespace FooNamespace\Component\Foos\Site\Service;
5
6 \defined('_JEXEC') or die;
7
8 use Joomla\CMS\Application\SiteApplication;
9 use Joomla\CMS\Categories\CategoryFactoryInterface;
10 use Joomla\CMS\Categories\CategoryInterface;
11 use Joomla\CMS\Component\ComponentHelper;
12 use Joomla\CMS\Component\Router\RouterView;
13 use Joomla\CMS\Component\Router\RouterViewConfiguration;
14 use Joomla\CMS\Component\Router\Rules\MenuRules;
15 use Joomla\CMS\Component\Router\Rules\NomenuRules;
16 use Joomla\CMS\Component\Router\Rules\StandardRules;
17 use Joomla\CMS\Menu\AbstractMenu;
18 use Joomla\Database\DatabaseInterface;
19 use Joomla\Database\ParameterType;
20
21 class Router extends RouterView
22 {
23 protected $noIDs = false;
24
25 private $categoryFactory;
26
27 private $categoryCache = [];
28
29 private $db;
30
31 public function __construct(SiteApplication $app, AbstractMenu
$menu, CategoryFactoryInterface $categoryFactory,
DatabaseInterface $db)
32 {
33 $this->categoryFactory = $categoryFactory;
34 $this->db = $db;
35
36 $params = ComponentHelper::getParams('com_foos');
37 $this->noIDs = (bool) $params->get('sef_ids');
38 $categories = new RouterViewConfiguration('categories');
39 $categories->setKey('id');
40 $this->registerView($categories);
41 $category = new RouterViewConfiguration('category');
42 $category->setKey('id')->setParent($categories, 'catid')->
setNestable();
43 $this->registerView($category);
44 $foo = new RouterViewConfiguration('foo');
45 $foo->setKey('id')->setParent($category, 'catid');
46 $this->registerView($foo);
47 $this->registerView(new RouterViewConfiguration('featured'));
48 $form = new RouterViewConfiguration('form');
49 $form->setKey('id');
50 $this->registerView($form);
51
52 parent::__construct($app, $menu);
53
54 $this->attachRule(new MenuRules($this));
55 $this->attachRule(new StandardRules($this));
56 $this->attachRule(new NomenuRules($this));
57 }
58
59 public function getCategorySegment($id, $query)
60 {
61 $category = $this->getCategories()->get($id);
62
63 if ($category) {
64 $path = array_reverse($category->getPath(), true);
65 $path[0] = '1:root';
66
67 if ($this->noIDs) {
68 foreach ($path as &$segment) {
69 list($id, $segment) = explode(':', $segment, 2);
70 }
71 }
72
73 return $path;
74 }
75
76 return [];
77 }
78
79 public function getCategoriesSegment($id, $query)
80 {
81 return $this->getCategorySegment($id, $query);
82 }
83
84 public function getFooSegment($id, $query)
85 {
86 if (!strpos($id, ':')) {
87 $id = (int) $id;
88 $dbquery = $this->db->getQuery(true);
89 $dbquery->select($this->db->quoteName('alias'))
90 ->from($this->db->quoteName('#__foos_details'))
91 ->where($this->db->quoteName('id') . ' = :id')
92 ->bind(':id', $id, ParameterType::INTEGER);
93 $this->db->setQuery($dbquery);
94
95 $id .= ':' . $this->db->loadResult();
96 }
97
98 if ($this->noIDs) {
99 list($void, $segment) = explode(':', $id, 2);
100
101 return [$void => $segment];
102 }
103
104 return [(int) $id => $id];
105 }
106
107 public function getFormSegment($id, $query)
108 {
109 return $this->getFooSegment($id, $query);
110 }
111
112 public function getCategoryId($segment, $query)
113 {
114 if (isset($query['id'])) {
115 $category = $this->getCategories(['access' => false])->get(
$query['id']);
116
117 if ($category) {
118 foreach ($category->getChildren() as $child) {
119 if ($this->noIDs) {
120 if ($child->alias == $segment) {
121 return $child->id;
122 }
123 } else {
124 if ($child->id == (int) $segment) {
125 return $child->id;
126 }
127 }
128 }
129 }
130 }
131
administrator/components/com_foos/services/provider.php
1 use FooNamespace\Component\Foos\Administrator\Extension\FoosComponent;
2 use FooNamespace\Component\Foos\Administrator\Helper\
AssociationsHelper;
3 use Joomla\CMS\Association\AssociationExtensionInterface;
4 +use Joomla\CMS\Component\Router\RouterFactoryInterface;
5 +use Joomla\CMS\Extension\Service\Provider\RouterFactory;
6
7 public function register(Container $container)
8 $container->registerServiceProvider(new CategoryFactory('\\
FooNamespace\\Component\\Foos'));
9 $container->registerServiceProvider(new MVCFactory('\\
FooNamespace\\Component\\Foos'));
10 $container->registerServiceProvider(new
ComponentDispatcherFactory('\\FooNamespace\\Component\\Foos'
));
11 + $container->registerServiceProvider(new RouterFactory('\\
FooNamespace\\Component\\Foos'));
12
13 $container->set(
14 ComponentInterface::class,
15 function (Container $container) {
16 $component->setMVCFactory($container->get(
MVCFactoryInterface::class));
17 $component->setCategoryFactory($container->get(
CategoryFactoryInterface::class));
18 $component->setAssociationExtension($container->get(
AssociationExtensionInterface::class));
19 + $component->setRouterFactory($container->get(
RouterFactoryInterface::class));
20
21 return $component;
22 }
1
2 use FooNamespace\Component\Foos\Administrator\Service\HTML\Icon;
3 use Psr\Container\ContainerInterface;
4 use Joomla\CMS\Helper\ContentHelper;
5 +use Joomla\CMS\Component\Router\RouterServiceInterface;
6 +use Joomla\CMS\Component\Router\RouterServiceTrait;
7
8 -class FoosComponent extends MVCComponent implements
BootableExtensionInterface, CategoryServiceInterface,
AssociationServiceInterface
9 +class FoosComponent extends MVCComponent implements
BootableExtensionInterface, CategoryServiceInterface,
AssociationServiceInterface, RouterServiceInterface
10 {
11 use CategoryServiceTrait;
12 use AssociationServiceTrait;
13 use HTMLRegistryAwareTrait;
14 + use RouterServiceTrait;
Copy the files in the administrator folder into the administrator folder of your Joomla 4 installa-
tion.
Copy the files in the components folder into the components folder of your Joomla 4 installation.
Install your component as described in part one, after you have copied all the files. Joomla will update
the namespaces for you during the installation. Since a new file has been added, this is necessary.
2. Activate the setting search engine friendly URLs in the global configuration.
Figure 33.1.: Search engine friendly URLs in the global configuration of Joomla
Figure 33.2.: Search Engine Friendly URLs in Joomla - Create Menu Item
4. check the URLs with which the menu item is called up in the frontend. Instead of http://
localhost/ single-foo-astrid?view=foo&id=2, http://localhost/ single-foo
-astrid should appear - depending on how you named your menu items. In the case of
categories, the improvement is even more obvious.
33.3. Links
Routing in com_contact3
3
github.com/joomla/joomla-cms/pull/27693
Dependency Injection (DI) sounds complicated and the synonym introducing dependencies does not
really sound positive. At first glance, program code should be as flexible as possible. In other words,
not dependent on anything else. Nobody likes to be dependent. The word has a negative touch.
Complicated and negative? On closer inspection, neither applies. With the help of a practical example,
the advantages will become clear.
The explanations in this chapter are an excursus. In other words, the code described here is not
included in the final version of the boilerplate.
The initial situation: Imagine you want to make the directions for each item in your component
individually describable.
For impatient people: View the changed program code in the Diff Viewa .
a
codeberg.org/astrid/j4examplecode/compare/t27..t27a1
34.1.1.1. administrator/components/com_foos/src/Extension/FoosComponent.php
So that every direction can be managed from one place, you start the call in the file administrator
/components/com_foos/src/Extension/FoosComponent.php. This file uses a container, or
rather the interface ContainerInterface.
administrator/components/com_foos/src/Extension/FoosComponent.php
1 use Joomla\CMS\HTML\HTMLRegistryAwareTrait;
2 use FooNamespace\Component\Foos\Administrator\Service\HTML\
AdministratorService;
3 use FooNamespace\Component\Foos\Administrator\Service\HTML\Icon;
4 +use FooNamespace\Component\Foos\Administrator\Service\HTML\Direction;
5 use Psr\Container\ContainerInterface;
6 use Joomla\CMS\Helper\ContentHelper;
7 use Joomla\CMS\Component\Router\RouterServiceInterface;
8 public function boot(ContainerInterface $container)
9 {
10 $this->getRegistry()->register('foosadministrator', new
AdministratorService);
11 $this->getRegistry()->register('fooicon', new Icon($container->
get(SiteApplication::class)));
12 + $this->getRegistry()->register('foodirection', new Direction())
;
13 }
34.1.1.2. administrator/components/com_foos/src/Service/HTML/Direction.php
We print the directions as text using the displayDirection method of the Direction class.
administrator/components/com_foos/src/Service/HTML/Direction.php
1 +<?php
2 +/**
3 + * @package Joomla.Site
4 + * @subpackage com_foos
5 + *
6 + * @copyright Copyright (C) 2005 - 2018 Open Source Matters, Inc.
All rights reserved.
7 + * @license GNU General Public License version 2 or later; see
LICENSE.txt
8 + */
9 +
10 +namespace FooNamespace\Component\Foos\Administrator\Service\HTML;
11 +
12 +\defined('_JEXEC') or die;
13 +
14 +/**
15 + * Directions Helper
16 + *
17 + * @since __DEPLOY_VERSION__
18 + */
19 +class Direction
20 +{
21 + /**
22 + * Service constructor
23 + *
24 + * @since __DEPLOY_VERSION__
25 + */
26 + public function __construct()
27 + {
28 + }
29 +
30 + /**
31 + * Method to generate a routing direction
32 + *
33 + * @return string The HTML markup for the create item link
34 + *
35 + * @since __DEPLOY_VERSION__
36 + */
37 + public function displayDirection()
38 + {
39 + return "The route description";
40 + }
41 +}
34.1.1.3. components/com_foos/tmpl/foo/default.php
components/com_foos/tmpl/ foo/default.php
1 </div>
2 <?php endif; ?>
3
4 +<hr>
5 +<?php echo HTMLHelper::_('foodirection.displayDirection', $this->item,
$tparams); ?>
6 +<hr>
7 +
8 <?php
9 echo $this->item->event->afterDisplayTitle;
10 echo $this->item->event->beforeDisplayContent;
When you call up an item in the frontend, the text you prepared to describe the directions appears.
Figure 34.1.: Joomla 4 - Output Step 1 of the Example on Services and Dependency Injection
Now the thing is that there are different ways to describe it:
For some items you have a descriptive graphic that shows the location. For another item, there is no
graphic. Instead, the address can be easily found via geocoding services and displayed on a digital
map. For other items, the position can only be described by text because insider knowledge is required.
We will work on this problem in step 2.
34.1.2.1. administrator/components/com_foos/src/Service/HTML/Direction.php
First, we prepare a class for each description type. Each class can prepare the text for the directions
separately and therefore well arranged. In this step, we next display the description for each type.
administrator/components/com_foos/src/Service/HTML/Direction.php
1
2 \defined('_JEXEC') or die;
3
4 +use FooNamespace\Component\Foos\Administrator\Service\HTML\Directions\
Image;
5 +use FooNamespace\Component\Foos\Administrator\Service\HTML\Directions\
Map;
6 +use FooNamespace\Component\Foos\Administrator\Service\HTML\Directions\
Text;
7 +
8 class Direction
9 {
10 + protected $directionTool1;
11 + protected $directionTool2;
12 + protected $directionTool3;
13 +
14 class Direction
15 public function __construct()
16 {
17 + $this->directionTool1 = new Image;
18 + $this->directionTool2 = new Map;
19 + $this->directionTool3 = new Text;
20 }
21
22 public function __construct()
23 public function displayDirection()
24 {
25 - return "The route description";
26 + return
27 + $this->directionTool1->findDirection() . "<br>" .
28 + $this->directionTool2->findDirection() . "<br>" .
29 + $this->directionTool3->findDirection();
30 }
31 }
34.1.2.2. administrator/components/com_foos/src/Service/HTML/Directions/Image.php
Below you see the class that is responsible for displaying the image.
administrator/components/com_foos/src/Service/HTML/Directions/Image.php
1 +<?php
2 +/**
3 + * @package Joomla.Site
4 + * @subpackage com_foos
5 + *
6 + * @copyright Copyright (C) 2005 - 2018 Open Source Matters, Inc.
All rights reserved.
34.1.2.3. administrator/components/com_foos/src/Service/HTML/Directions/Map.php
The most complex is probably the creation of the route via the digital map, which is the task of the
class “Map”.
administrator/components/com_foos/src/Service/HTML/Directions/Map.php
1 +<?php
2 +/**
3 + * @package Joomla.Site
4 + * @subpackage com_foos
5 + *
6 + * @copyright Copyright (C) 2005 - 2018 Open Source Matters, Inc.
All rights reserved.
7 + * @license GNU General Public License version 2 or later; see
LICENSE.txt
8 + */
9 +
10 +namespace FooNamespace\Component\Foos\Administrator\Service\HTML\
Directions;
11 +
12 +\defined('_JEXEC') or die;
13 +
14 +/**
15 + * Content Component HTML Helper
16 + *
17 + * @since __DEPLOY_VERSION__
18 + */
19 +class Map
20 +{
21 +
22 + /**
23 + * Service constructor
24 + *
25 + * @param CMSApplication $application The application
26 + *
27 + * @since __DEPLOY_VERSION__
28 + */
29 + public function __construct()
30 + {
31 + }
32 +
33 + /**
34 + * Method to generate a link to the create item page for the given
category
35 + *
36 + * @param object $category The category information
37 + * @param Registry $params The item parameters
38 + * @param array $attribs Optional attributes for the link
39 + *
40 + * @return string The HTML markup for the create item link
41 + *
42 + * @since __DEPLOY_VERSION__
43 + */
44 + public static function findDirection()
45 + {
46 + return "Find direction with a Map.";
47 + }
48 +}
34.1.2.4. administrator/components/com_foos/src/Service/HTML/Directions/Text.php
The class named “Text” prepares the textual description of the route.
administrator/components/com_foos/src/Service/HTML/Directions/Text.php
1 +<?php
2 +/**
3 + * @package Joomla.Site
4 + * @subpackage com_foos
5 + *
6 + * @copyright Copyright (C) 2005 - 2018 Open Source Matters, Inc.
All rights reserved.
7 + * @license GNU General Public License version 2 or later; see
LICENSE.txt
8 + */
9 +
10 +namespace FooNamespace\Component\Foos\Administrator\Service\HTML\
Directions;
11 +
12 +\defined('_JEXEC') or die;
13 +
14 +/**
15 + * Content Component HTML Helper
16 + *
17 + * @since __DEPLOY_VERSION__
18 + */
19 +class Text
20 +{
21 +
22 + /**
23 + * Service constructor
24 + *
25 + * @param CMSApplication $application The application
26 + *
27 + * @since __DEPLOY_VERSION__
28 + */
29 + public function __construct()
30 + {
31 + }
32 +
33 + /**
34 + * Method to generate a link to the create item page for the given
category
35 + *
36 + * @param object $category The category information
37 + * @param Registry $params The item parameters
38 + * @param array $attribs Optional attributes for the link
39 + *
40 + * @return string The HTML markup for the create item link
41 + *
42 + * @since __DEPLOY_VERSION__
43 + */
44 + public static function findDirection()
45 + {
46 + return "Find direction via Text Explanation.";
47 + }
48 +}
When you call up an item in the frontend, the text you prepared to describe the directions appears.
Figure 34.2.: Joomla 4 - Output Step 2 of the Example on Services and Dependency Injection
The problem: At the moment, all types of directions are displayed and it is not ensured that this
description exists. Often not every type is available. Sometimes it is also the case that you want to
specify which type is displayed. Instead of making all types available, it would be better if the optimal
directions could be defined. This way it would also be possible to add or remove new types without
having to make changes to the existing code. We will look at how this is possible in step 3.
For each item, the description of the direction is possible by text, by image or by digital map. It would
be nice if the three types were equally applicable next to each other and if it was ensured that there is
at least one description. Let us look at “interfaces” and “traits” in this context.
An interface is a contract between the implementing class and the calling class. The contract ensures
that each class meets some criteria that the interface implements. We have three options. For each,
we create an interface. An interface is something like a contract that ensures minimum requirements
are met. We then implement these interfaces in the classes. By using a “trait”, we ensure that we don’t
have to rewrite the contract each time. We use standards. This way, our service works as agreed!
For impatient people: View the changed program code in the Diff Viewa .
a
codeberg.org/astrid/j4examplecode/compare/t27a2..t27a3
34.1.3.1. administrator/components/com_foos/src/Extension/FoosComponent.php
In the component class we add everything necessary for the service Direction.
administrator/components/com_foos/src/Extension/FoosComponent.php
1 use Joomla\CMS\Helper\ContentHelper;
2 use Joomla\CMS\Component\Router\RouterServiceInterface;
3 use Joomla\CMS\Component\Router\RouterServiceTrait;
4 +use FooNamespace\Component\Foos\Administrator\Service\Direction\
DirectionServiceInterface;
5 +use FooNamespace\Component\Foos\Administrator\Service\Direction\
DirectionServiceTrait;
6
7 -class FoosComponent extends MVCComponent implements
BootableExtensionInterface, CategoryServiceInterface,
AssociationServiceInterface, RouterServiceInterface
8 +class FoosComponent extends MVCComponent implements
BootableExtensionInterface, CategoryServiceInterface,
AssociationServiceInterface, RouterServiceInterface,
DirectionServiceInterface
9 {
10 use CategoryServiceTrait;
11 use AssociationServiceTrait;
12 use HTMLRegistryAwareTrait;
13 use RouterServiceTrait;
14 + use DirectionServiceTrait;
15
16 public function boot(ContainerInterface $container)
17 {
18 $this->getRegistry()->register('foosadministrator', new
AdministratorService);
19 $this->getRegistry()->register('fooicon', new Icon($container->
get(SiteApplication::class)));
20 - $this->getRegistry()->register('foodirection', new Direction())
;
21 }
34.1.3.2. administrator/components/com_foos/src/Service/Direction/DirectionExtensionInterface.php
administrator/components/com_foos/src/Service/Direction/DirectionExtensionInterface.php
1 +<?php
2 +/**
3 + * @package Joomla.Site
4 + * @subpackage com_foos
5 + *
6 + * @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.
org>
7 + * @license GNU General Public License version 2 or later; see
LICENSE.txt
8 + */
9 +
10 +namespace FooNamespace\Component\Foos\Administrator\Service\Direction;
11 +
12 +\defined('JPATH_PLATFORM') or die;
13 +
14 +/**
15 + * Direction Extension Interface for the helper classes
16 + *
17 + * @since __DEPLOY_VERSION__
18 + */
19 +interface DirectionExtensionInterface
20 +{
21 + /**
22 + * Method to get the direction for a given item.
23 + *
24 + * @return string Direction
25 + *
26 + * @since __DEPLOY_VERSION__
27 + */
28 + public static function findDirection();
29 +}
34.1.3.3. administrator/components/com_foos/src/Service/Direction/DirectionServiceInterface.php
DirectionServiceInterface’ is another interface. It defines which interface the service supports and
how it can be accessed. Specifically, we use DirectionExtensionInterface, which we discussed
in the previous section. We can retrieve this via getDirectionExtension.We will do the latter in a
concrete example below.
administrator/components/com_foos/src/Service/Direction/DirectionServiceInterface.php
1 +<?php
2 +/**
3 + * @package Joomla.Site
4 + * @subpackage com_foos
5 + *
6 + * @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.
org>
7 + * @license GNU General Public License version 2 or later; see
LICENSE.txt
8 + */
9 +
10 +namespace FooNamespace\Component\Foos\Administrator\Service\Direction;
11 +
12 +\defined('JPATH_PLATFORM') or die;
13 +
14 +/**
15 + * The Direction service.
16 + *
17 + * @since __DEPLOY_VERSION__
18 + */
19 +interface DirectionServiceInterface
20 +{
21 + /**
22 + * Returns the Directions extension helper class.
23 + *
24 + * @return DirectionExtensionInterface
25 + *
26 + * @since __DEPLOY_VERSION__
27 + */
28 + public function getDirectionExtension():
DirectionExtensionInterface;
29 +}
34.1.3.4. administrator/components/com_foos/src/Service/Direction/DirectionServiceTrait.php
administrator/components/com_foos/src/Service/Direction/DirectionServiceTrait.php
1 +<?php
2 +/**
3 + * @package Joomla.Site
4 + * @subpackage com_foos
5 + *
6 + * @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.
org>
7 + * @license GNU General Public License version 2 or later; see
LICENSE.txt
8 + */
9 +
10 +namespace FooNamespace\Component\Foos\Administrator\Service\Direction;
11 +
12 +\defined('JPATH_PLATFORM') or die;
13 +
14 +/**
15 + * Trait to implement DirectionServiceInterface
16 + *
17 + * @since __DEPLOY_VERSION__
18 + */
19 +trait DirectionServiceTrait
20 +{
21 + /**
22 + * The direction extension.
23 + *
24 + * @var DirectionExtensionInterface
25 + *
26 + * @since __DEPLOY_VERSION__
27 + */
28 + private $directionExtension = null;
29 +
30 + /**
31 + * Returns the directions extension helper class.
32 + *
33 + * @return DirectionExtensionInterface
34 + *
35 + * @since __DEPLOY_VERSION__
36 + */
37 + public function getDirectionExtension():
DirectionExtensionInterface
38 + {
39 + return $this->directionExtension;
40 + }
41 +
42 + /**
43 + * The direction extension.
44 + *
45 + * @param DirectionExtensionInterface $directionExtension The
extension
46 + *
47 + * @return void
48 + *
49 + * @since __DEPLOY_VERSION__
50 + */
51 + public function setDirectionExtension(DirectionExtensionInterface
$directionExtension)
52 + {
53 + $this->directionExtension = $directionExtension;
54 + }
55 +}
34.1.3.5. administrator/components/com_foos/src/Service/HTML/Directions/Image.php
The class Image should in any case provide the function findDirection. We achieve this by imple-
menting the interface DirectionExtensionInterface.
administrator/components/com_foos/src/Service/HTML/Directions/Image.php
1
2 -namespace FooNamespace\Component\Foos\Administrator\Service\HTML\
Directions;
3 +namespace FooNamespace\Component\Foos\Administrator\Service\Direction;
4
5 \defined('_JEXEC') or die;
6
7 -class Image
8 +class Image implements DirectionExtensionInterface
9 {
34.1.3.6. administrator/components/com_foos/src/Service/HTML/Directions/Map.php
The class Map should also offer the function findDirection. We achieve this by also implementing
the interface DirectionExtensionInterface.
administrator/components/com_foos/src/Service/HTML/Directions/Map.php
1
2 -namespace FooNamespace\Component\Foos\Administrator\Service\HTML\
Directions;
3 +namespace FooNamespace\Component\Foos\Administrator\Service\Direction;
4
5 \defined('_JEXEC') or die;
6
7 -class Map
8 +class Map implements DirectionExtensionInterface
9 {
34.1.3.7. administrator/components/com_foos/src/Service/HTML/Directions/Text.php
Last but not least, Map shall provide the function findDirection. Therefore, this also implements
the interface DirectionExtensionInterface.
administrator/components/com_foos/src/Service/HTML/Directions/Text.php
1
2 -namespace FooNamespace\Component\Foos\Administrator\Service\HTML\
Directions;
3 +namespace FooNamespace\Component\Foos\Administrator\Service\Direction;
4
5 \defined('_JEXEC') or die;
6
7 -class Text
8 +class Text implements DirectionExtensionInterface
9 {
34.1.3.8. administrator/components/com_foos/src/Service/HTML/Direction.php
administrator/components/com_foos/src/Service/HTML/Direction.php
1 -<?php
2 -/**
3 - * @package Joomla.Site
4 - * @subpackage com_foos
5 - *
6 - * @copyright Copyright (C) 2005 - 2018 Open Source Matters, Inc.
All rights reserved.
7 - * @license GNU General Public License version 2 or later; see
LICENSE.txt
8 - */
9 -
10 -namespace FooNamespace\Component\Foos\Administrator\Service\HTML;
11 -
12 -\defined('_JEXEC') or die;
13 -
14 -use FooNamespace\Component\Foos\Administrator\Service\HTML\Directions\
Image;
15 -use FooNamespace\Component\Foos\Administrator\Service\HTML\Directions\
Map;
16 -use FooNamespace\Component\Foos\Administrator\Service\HTML\Directions\
Text;
17 -
18 -/**
19 - * Directions Helper
20 - *
21 - * @since __DEPLOY_VERSION__
22 - */
23 -class Direction
24 -{
25 - protected $directionTool1;
26 - protected $directionTool2;
27 - protected $directionTool3;
28 -
29 - /**
30 - * Service constructor
31 - *
32 - * @since __DEPLOY_VERSION__
33 - */
34 - public function __construct()
35 - {
36 - $this->directionTool1 = new Image;
37 - $this->directionTool2 = new Map;
38 - $this->directionTool3 = new Text;
39 - }
40 -
41 - /**
42 - * Method to generate a routing direction
43 - *
44 - * @return string The HTML markup for the direction
45 - *
46 - * @since __DEPLOY_VERSION__
47 - */
48 - public function displayDirection()
49 - {
50 - return
51 - $this->directionTool1->findDirection() . "<br>" .
52 - $this->directionTool2->findDirection() . "<br>" .
53 - $this->directionTool3->findDirection();
54 - }
55 -}
34.1.3.9. components/com_foos/tmpl/foo/default.php
When displaying in the frontend, we can load the component class via $fooComponent =
Factory::getApplication()->bootComponent('com_foos') and dynamically re-set the
interface $fooComponent->setDirectionExtension(new DirectionMap) during runtime.
This way it is possible to use different implementations for the output findDirection(). To
ensure that the method findDirection() is always available, we have implemented the interface
DirectionExtensionInterface in the possible DirectionExtensions DirectionExtension.
components/com_foos/tmpl/foo/default.php
1 use Joomla\CMS\Helper\ContentHelper;
2 use Joomla\CMS\HTML\HTMLHelper;
3 use Joomla\CMS\Language\Text;
4 +use FooNamespace\Component\Foos\Administrator\Service\Direction\Map as
DirectionMap;
5 +use FooNamespace\Component\Foos\Administrator\Service\Direction\Text
as DirectionText;
6 +use FooNamespace\Component\Foos\Administrator\Service\Direction\Image
as DirectionImage;
7
8 $canDo = ContentHelper::getActions('com_foos', 'category', $this->
item->catid);
9 $canEdit = $canDo->get('core.edit') || ($canDo->get('core.edit.own')
&& $this->item->created_by == Factory::getUser()->id);
10
11 </div>
12 <?php endif; ?>
13
14 +<?php
15 + $fooComponent = Factory::getApplication()->bootComponent('com_foos'
);
16 +?>
17 +<hr>
18 +<?php
19 + $fooComponent->setDirectionExtension(new DirectionMap);
20 + echo $fooComponent->getDirectionExtension()->findDirection();
21 +?>
22 <hr>
23 -<?php echo HTMLHelper::_('foodirection.displayDirection', $this->item,
$tparams); ?>
24 +<?php
25 + $fooComponent->setDirectionExtension(new DirectionText);
26 + echo $fooComponent->getDirectionExtension()->findDirection();
27 +?>
28 +<hr>
29 +<?php
30 + $fooComponent->setDirectionExtension(new DirectionImage);
31 + echo $fooComponent->getDirectionExtension()->findDirection();
32 +?>
33 <hr>
34
35 <?php
When you call up an item in the frontend, the text you prepared to describe the directions appears. In
the example, for demonstration purposes, I still display all possible directions. In my opinion, however,
it becomes clear how uncomplicated it is to change the output at runtime or how easy it is to manipulate
it with the help of parameters. Parameters are also the subject of a chapter in this tutorial.
Figure 34.3.: Joomla 4 - Output Step 3 of the Example on Services and Dependency Injection
In this section you see a simple example. It contains the basics of DI. Passing the requirements for a
class to the class via a set method, where the set method typically corresponds to the name of the
property. In our case, this is DirectionExtension: we want to set the extension that outputs the
Direction.
An Inversion of Control (IoC) container can help manage all parts of the application. Instead of
creating a new DirectionExtension each time, it is much easier to remember how to prepare
a DirectionExtension. Since the DirectionExtension in our example does not have many de-
pendencies, the advantages of a container are hard to see. But imagine that every time you create a
DirectionExtension you have to remember to pass the dependencies like impement the interface
DirectionExtensionInterface and provide the method findDirection. With a container, it is
possible to set up everything as if it were a template and leave the creation to the application. This is
even more handy when the dependencies we inject have dependencies within their dependencies.
This can all become very complex. Examples you can find in the ../services/provider.php files,
for example in /administrator/components/com_foos/services/provider.php.
34.4. Links
35. Dashboard
Extensive Joomla Core extensions have a dashboard in which related functions are displayed. This is
user-friendly because it provides an overview. This way, a user can orientate himself in the extension
without many clicks. In this part, we create such a dashboard for our sample component.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t27...t28
administrator/components/com_foos/presets/foos.xml
1
2 <?xml version="1.0"?>
3 <menu
4 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5 xmlns="urn:joomla.org"
6 xsi:schemaLocation="urn:joomla.org menu.xsd"
7 >
8 <menuitem
9 title="COM_FOOS"
10 type="heading"
11 icon="comment"
12 class="class:comment"
13 >
14 <menuitem
15 title="COM_FOOS"
16 type="component"
17 element="com_foos"
18 link="index.php?option=com_foos"
19 quicktask="index.php?option=com_foos&view=foo&
layout=edit"
20 quicktask-title="COM_FOOS"
21 />
22
23 <menuitem
24 title="JCATEGORY"
25 type="component"
26 element="com_foos"
27 link="index.php?option=com_categories&extension=
com_foos"
28 quicktask="index.php?option=com_categories&view=
category&layout=edit&extension=com_foos"
29 quicktask-title="JCATEGORY"
30 />
31 </menuitem>
32 </menu>
35.1.2.1. administrator/components/com_foos/foos.xml
We modify the XML manifest so that the sidebar in the Joomla administration template knows how to
link to the dashboard.
administrator/components/com_foos/foos.xml
1 </media>
2 <!-- Back-end files -->
3 <administration>
4 - <!-- Menu entries -->
5 - <menu view="foos">COM_FOOS</menu>
6 + <menu img="class:comment">
7 + COM_FOOS
8 + <params>
9 + <dashboard>foos</dashboard>
10 + </params>
11 + </menu>
12 <submenu>
13 + <menu link="option=com_foos">
14 + COM_FOOS
15 + <params>
16 + <menu-quicktask-title>COM_FOOS</menu-quicktask-
title>
17 + <menu-quicktask>index.php?option=com_foos&view=
foo&layout=edit</menu-quicktask>
18 + </params>
19 + </menu>
20 <menu link="option=com_foos">COM_FOOS</menu>
21 - <menu link="option=com_categories&extension=com_foos">
JCATEGORY</menu>
22 + <menu link="option=com_categories&extension=com_foos">
23 + JCATEGORY
24 + <params>
25 + <menu-quicktask-title>JCATEGORY</menu-quicktask-
title>
26 + <menu-quicktask>index.php?option=com_categories&
;view=category&layout=edit&extension=com_foos</menu-
quicktask>
27 + </params>
28 + </menu>
29 <menu link="option=com_fields&context=com_foos.foo">
JGLOBAL_FIELDS</menu>
30 <menu link="option=com_fields&view=groups&context=
com_foos.foo">JGLOBAL_FIELD_GROUPS</menu>
31 </submenu>
32
33 <filename>foos.xml</filename>
34 <filename>config.xml</filename>
35 <folder>forms</folder>
36 + <folder>presets</folder>
37 <folder>language</folder>
38 <folder>services</folder>
39 <folder>sql</folder>
35.1.2.2. administrator/components/com_foos/script.php
In the installation script we add the call. With this, we call a Joomla-specific function that makes our
dashboard known in the CMS.
administrator/components/com_foos/script.php
1 use Joomla\CMS\Language\Text;
2 use Joomla\CMS\Log\Log;
3 use Joomla\CMS\Table\Table;
4 +use Joomla\CMS\Installer\InstallerScript;
5
6 -class Com_FoosInstallerScript
7 +class Com_FoosInstallerScript extends InstallerScript
8 {
9 public function install($parent): bool
10 return false;
11 }
12
13 + $this->addDashboardMenu('foos', 'foos');
14 +
15 return true;
16 }
17
18 public function update($parent): bool
19 {
20 echo Text::_('COM_FOOS_INSTALLERSCRIPT_UPDATE');
21
22 + $this->addDashboardMenu('foo', 'foo');
23 +
24 return true;
25 }
Copy the files in the administrator folder into the administrator folder of your Joomla 4 installa-
tion.
Install your component as described in part one, after copying all files. Joomla will update the names-
paces for you during the installation. Since a new file has been added, this is necessary. We have also
added instructions to the installation script.
35.3. Links
Allow 3rd party components to create the dashboard[github.com/joomla/joomla-cms/pull/28027] Joomla Manual [manual.joomla.org/
core-functions/Dashboard]
36. Tags
Tags or Keywords are a flexible solution to organise content in Joomla A keyword can be assigned to
many different elements of different content types. Each element can have unlimited tags.
Joomla’s tagging system is used in all core extensions. It is designed to be easily integrated into other
extensions that use standard Joomla design patterns. Using tags in a third-party extension is quite
simple. Using it in your own extension requires the modifications explained in this section.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t28...t29
No new files.
The form through which the search tools are managed receives an entry for the keywords.
administrator/components/com_foos/forms/filter_foos.xml
1 <option value="*">JALL</option>
2 </field>
3
4 + <field
5 + name="tag"
6 + type="tag"
7 + label="JTAG"
8 + multiple="true"
9 + mode="nested"
10 + custom="false"
11 + hint="JOPTION_SELECT_TAG"
12 + onchange="this.form.submit();"
13 + />
14 </fields>
15
16 <fields name="list">
In the XML form, we add the form field that contains the information about the tag. Since we use
Joomla Standard, we can use many ready-made functions out-of-the-box.
administrator/components/com_foos/forms/foo.xml
1 label="JFIELD_ORDERING_LABEL"
2 content_type="com_foos.foo"
3 />
4 +
5 + <field
6 + name="tags"
7 + type="tag"
8 + label="JTAG"
9 + class="advancedSelect"
10 + multiple="true"
11 + />
12 </fieldset>
13 <fields name="params" label="JGLOBAL_FIELDSET_DISPLAY_OPTIONS">
14 <fieldset name="display" label="
JGLOBAL_FIELDSET_DISPLAY_OPTIONS">
36.1.2.3. administrator/components/com_foos/script.php
In the installation script, we make sure that our extension is recognised as a separate content type in
Joomla.
administrator/components/com_foos/script.php
1 {
2 echo Text::_('COM_FOOS_INSTALLERSCRIPT_POSTFLIGHT');
3
4 + $this->saveContentTypes();
5 +
6 return true;
7 }
8
9
10
11 return $id;
12 }
13 +
14 + private function saveContentTypes()
15 + {
16 + $table = Table::getInstance('Contenttype', 'JTable');
17 +
18 + $table->load(['type_alias' => 'com_foos.foo']);
19 +
20 + $tablestring = '{
21 + "special": {
22 + "dbtable": "#__foos",
23 + "key": "id",
24 + "type": "FooTable",
25 + "prefix": "Joomla\\\\Component\\\\Foos\\\\Administrator
\\\\Table\\\\",
26 + "config": "array()"
27 + },
28 + "common": {
29 + "dbtable": "#__ucm_content",
30 + "key": "ucm_id",
31 + "type": "Corecontent",
32 + "prefix": "JTable",
33 + "config": "array()"
34 + }
35 + }';
36 +
37 + $fieldmapping = '{
38 + "common": {
39 + "core_content_item_id": "id",
40 + "core_title": "name",
41 + "core_state": "published",
42 + "core_alias": "alias",
43 + "core_publish_up": "publish_up",
44 + "core_publish_down": "publish_down",
45 + "core_access": "access",
46 + "core_params": "params",
47 + "core_featured": "featured",
48 + "core_language": "language",
49 + "core_ordering": "ordering",
50 + "core_catid": "catid",
51 + "asset_id": "null"
52 + },
53 + "special": {
54 + }
55 + }';
56 +
57 + $contenttype = [];
58 + $contenttype['type_id'] = ($table->type_id) ? $table->type_id :
0;
59 + $contenttype['type_title'] = 'Foos';
60 + $contenttype['type_alias'] = 'com_foos.foo';
61 + $contenttype['table'] = $tablestring;
62 + $contenttype['rules'] = '';
63 + $contenttype['router'] = 'RouteHelper::getFooRoute';
64 + $contenttype['field_mappings'] = $fieldmapping;
65 + $contenttype['content_history_options'] = '';
66 +
67 + $table->save($contenttype);
68 +
69 + return;
70 + }
71 }
In the model of the element, we insert the tags into the batch processing batch and ensure that the
associated tags are loaded.
administrator/components/com_foos/src/Model/FooModel.php
1 use Joomla\CMS\Language\LanguageHelper;
2 use Joomla\Database\ParameterType;
3 use Joomla\Utilities\ArrayHelper;
4 +use Joomla\CMS\Helper\TagsHelper;
5
6 ... class FooModel extends AdminModel
7 protected $batch_commands = [
8 'assetgroup_id' => 'batchAccess',
9 'language_id' => 'batchLanguage',
10 + 'tag' => 'batchTag',
11 'user_id' => 'batchUser',
12 ];
13
14 ... public function getItem($pk = null)
15 }
16 }
17
18 + // Load item tags
19 + if (!empty($item->id)) {
20 + $item->tags = new TagsHelper;
21 + $item->tags->getTagIds($item->id, 'com_foos.foo');
22 + }
23 +
24 return $item;
25 }
We change the model of the overview list of our extension in the backend regarding the filters and the
database query.
administrator/components/com_foos/src/Model/FoosModel.php
1 use Joomla\CMS\Language\Associations;
2 use Joomla\CMS\Factory;
3 use Joomla\Utilities\ArrayHelper;
4 +use Joomla\Database\ParameterType;
5
6 ... protected function getListQuery()
7 $query->where($db->quoteName('a.language') . ' = ' . $db->
quote($language));
8 }
9
10 + // Filter by a single or group of tags.
11 + $tag = $this->getState('filter.tag');
12 +
13 + // Run simplified query when filtering by one tag.
14 + if (\is_array($tag) && \count($tag) === 1) {
15 + $tag = $tag[0];
16 + }
17 +
18 + if ($tag && \is_array($tag)) {
19 + $tag = ArrayHelper::toInteger($tag);
20 +
21 + $subQuery = $db->getQuery(true)
22 + ->select('DISTINCT ' . $db->quoteName('content_item_id'
))
23 + ->from($db->quoteName('#__contentitem_tag_map'))
24 + ->where(
25 + [
26 + $db->quoteName('tag_id') . ' IN (' . implode(',
', $query->bindArray($tag)) . ')',
27 + $db->quoteName('type_alias') . ' = ' . $db->
quote('com_foos.foo'),
28 + ]
29 + );
30 +
31 + $query->join(
32 + 'INNER',
33 + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'),
34 + $db->quoteName('tagmap.content_item_id') . ' = ' . $db
->quoteName('a.id')
35 + );
36 + } else if ($tag = (int) $tag) {
37 + $query->join(
38 + 'INNER',
39 + $db->quoteName('#__contentitem_tag_map', 'tagmap'),
In the view, we ensure that the keywords matching the language are loaded.
administrator/components/com_foos/src/View/Foo/HtmlView.php
1
2 // Only allow to select categories with All language or
with the forced language.
3 $this->form->setFieldAttribute('catid', 'language', '*,' .
$forcedLanguage);
4 +
5 + // Only allow to select tags with All language or with the
forced language.
6 + $this->form->setFieldAttribute('tags', 'language', '*,' .
$forcedLanguage);
7 }
8
9 $this->addToolbar();
So that the batch processing can also be used for the tags, we insert a form field. With the help of this
field it is possible to select a keyword that will be assigned to all selected items.
administrator/components/com_foos/tmpl/foos/default_batch_body.php
1 </div>
2 </div>
Copy the files in the administrator folder into the administrator folder of your Joomla 4 installa-
tion.
2. install your component as described in part one, after copying all files. As we have changed
things in the installation script, this is necessary.
6. create a menu item that shows all elements that are assigned to a certain keyword and see the
display in the frontend.
If you have tagged a Foo element and are now surprised that it is not displayed, first check whether the
Foo element is published. Only published elements are displayed in the frontend.
7. Create a new tag and assign it to several Foo items by batch processing.
Figure 36.6.: Assign a keyword in a custom Joomla 4 extension by batch processing - open batch
processing
Figure 36.7.: Assign a keyword in a custom Joomla 4 extension by batch processing - submit form
8. think about how and where you show the keywords in the frontend of your own extension.
com_contact’ provides a parameter that allows the website owner to set whether tags are
displayed. The display is done with the help of the layout joomla.content.tags.
itemTags)) : ?>
2 <div class="com-contact__tags">
3 <?php $this->item->tagLayout = new FileLayout('joomla.content.
tags'); ?>
4 <?php echo $this->item->tagLayout->render($this->item->tags->
itemTags); ?>
5 </div>
6 <?php endif; ?>
You want to tag Joomla elements in order to build a keyword directory. You make the assignment
by opening the element (article, category ...) and entering the keywords on the right. Do you have
any problems? Are not all keywords saved. Do you also notice that not all keywords are displayed
in the selection menu of other components? The reason is that the display of the keywords is
limited to 30. I have searched for documentation. I have not found anything. Maybe it’s because
I’m not searching properly. I sometimes find it easier to look directly in the code: The place that
answers the question is in the file libraries/src/Form/Field/TagField.phpa . The PR, where you can
also read out the reasons for the introduction, is PR 31481b . The workaround is to change the
display mode in the Tags component.
a
github.com/joomla/joomla-cms/blob/ba5fc69400c2fb2a27e56d0b8bec0db10c8705df/libraries/src/form/field/tagfield.php#l136
b
github.com/joomla/joomla-cms/pull/31481
36.3. Links
1
docs.joomla.org/j3.x:using_tags_in_an_extension
In this part we will take a look at the Joomla 4 API and how to access Joomla 4 content. A programming
interface - API for short (from English application programming interface) - is a program part that
is made available by a software system to other programs for connection to the system. Nowadays,
many online services provide APIs; these are then called web service. The existence of a documented
application programming interface (API) for a Joomla component makes it possible to work together
with others. Either via additional software that uses the API via extension or data becomes usable in
other applications via the API.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t29...t30
37.1.1.1. Component
1 <?php
2 namespace FooNamespace\Component\Foos\Api\Controller;
3
4 defined('_JEXEC') or die;
5
6 use Joomla\CMS\MVC\Controller\ApiController;
7 use Joomla\Component\Fields\Administrator\Helper\FieldsHelper;
8
9 class FooController extends ApiController
10 {
11 protected $contentType = 'foos';
12
13 protected $default_view = 'foos';
14
15 protected function save($recordKey = null)
16 {
17 $data = (array) json_decode($this->input->json->getRaw(), true)
;
18
19 foreach (FieldsHelper::getFields('com_foos.foo') as $field)
20 {
21 if (isset($data[$field->name]))
22 {
23 !isset($data['com_fields']) && $data['com_fields'] =
[];
24
25 $data['com_fields'][$field->name] = $data[$field->name
];
26 unset($data[$field->name]);
27 }
28 }
29
30 $this->input->set('data', $data);
31
32 return parent::save($recordKey);
33 }
34 }
1 <?php
2 namespace FooNamespace\Component\Foos\Api\View\Foos;
3
4 defined('_JEXEC') or die;
5
37.1.1.2. Plugin
1 <?php
2 defined('_JEXEC') or die;
3
4 use Joomla\CMS\Plugin\CMSPlugin;
5 use Joomla\CMS\Router\ApiRouter;
6
7 class PlgWebservicesFoos extends CMSPlugin
8 {
9 protected $autoloadLanguage = true;
10
11 public function onBeforeApiRoute(&$router)
12 {
13 $router->createCRUDRoutes(
14 'v1/foos',
15 'foo',
16 ['component' => 'com_foos']
17 );
18
19 $router->createCRUDRoutes(
20 'v1/foos/categories',
21 'categories',
22 ['component' => 'com_categories', 'extension' => 'com_foos'
]
23 );
24 }
25 }
37.1.1.3. plugins/webservices/foos/language/en-GB/plg_webservices_foos.ini
1
2 PLG_WEBSERVICES_FOOS="Web Services - Foos"
3 PLG_WEBSERVICES_FOOS_XML_DESCRIPTION="Used to add foos routes to the
API for your website."
37.1.1.4. plugins/webservices/foos/language/en-GB/plg_webservices_foos.sys.ini
I also include the language file, which is mainly responsible for the installation and the creation of the
menu in the dashboard.
1
2 PLG_WEBSERVICES_FOOS="Web Services - Foos"
3 PLG_WEBSERVICES_FOOS_XML_DESCRIPTION="Used to add foos routes to the
API for your website."
37.1.3. Component
37.1.3.1. administrator/components/com_foos/foos.xml
In the installation file it is important to include the folder api. Otherwise the files in the subfolder api
will not be copied to the correct directory during an installation.
1 <folder>tmpl</folder>
2 </files>
3 </administration>
4 + <api>
5 + <files folder="api/components/com_foos">
6 + <folder>src</folder>
7 + </files>
8 + </api>
9 <changelogurl>https://codeberg.org/astrid/j4examplecode/raw/branch/
tutorial/changelog.xml</changelogurl>
10 <updateservers>
11 <server type="extension" name="Foo Updates">https://codeberg.
org/astrid/j4examplecode/raw/branch/tutorial/foo_update.xml
</server>
37.1.3.2. Miscellaneous
37.1.3.2.1. Public Api It is possible to declare routes as public via setting a flag. However, this is
risky and can give away sensitive information. You can set public access when registering the route.
If you use Joomla\CMS\Router\ApiRouter::createCRUDRoutes(), pass the fourth argument
with true to enable public GETs.
1 ...
2 $router->createCRUDRoutes(
3 'v1/foos',
4 'foos',
5 ['component' => 'com_foos'],
6 true
7 );
8 ...
Or when you manually instantiate the route use the following code.
1 ...
2 $route = new Joomla\Router\Route(['GET'], 'v1/foos', 'foos.displayList'
, [], ['component' => 'com_foos', 'public' => true]);
3 $router->addRoute($route);
4 ...
1. create a new installation. To do this, uninstall your previous installation and copy all files again.
Copy the files in the administrator folder into the administrator folder of your Joomla 4
installation. Copy the files in the api folder into the api folder of your Joomla 4 installation.
Copy the files in the plugin folder into the plugin folder of your Joomla 4 installation. Install
your component and the plugin as described in part one, after copying all files. 2.
For the following examples, I assume that your installation is located at http://localhost/joomla
-cms4 and that your user and password are admin (Base64: YWRtaW46YWRtaW4=). Change this if
necessary.
37.2.1. curl.haxx.de
For Curl3 you need to change the password to Base64. A website that does this for you is
base64encode.org.
Do you use Curl? The following query will list all the elements:
1 {
2 "links": {
3 "self": "http://localhost/joomla-cms4/api/index.php/v1/foos"
4 },
5 "data": [
6 {
7 "type": "foos",
8 "id": "2",
9 "attributes": {
10 "id": 2,
11 "name": "Astrid",
12 "catid": 8
13 }
14 },
15 {
16 "type": "foos",
17 "id": "3",
18 "attributes": {
19 "id": 3,
20 "name": "Elmar",
21 "catid": 0
22 }
23 },
24 {
25 "type": "foos",
26 "id": "1",
27 "attributes": {
28 "id": 1,
29 "name": "Nina",
3
curl.haxx.se
30 "catid": 0
31 }
32 }
33 ],
34 "meta": {
35 "total-pages": 1
36 }
37 }
Providing the credentials is mandatory. All together the call in a console looks like this:
37.2.2. postman.com
Do you use postman.com? Then the collection4 might be helpful for you. It contains additional queries
for com_content.
37.2.3. Misc
37.3. Links
Integration in Weblinks7
4
github.com/astridx/boilerplate/blob/tutorial/tutorial/component/30/Content%20und%20Foos.postman_collection.json
5
addons.mozilla.org/en-us/firefox/addon/restclient/
6
docs.joomla.org/j4.x:joomla_core_apis
7
github.com/joomla-extensions/weblinks/pull/407
Part II.
Plugins
38. Plugins
You have created a plug-in in the previous section. You have probably already configured other plugins
in the plugin manager and know the different types. Plugins cover many different areas in Joomla.
This chapter provides an overview of what plugins are and how they work within Joomla.
In the Joomla Documentationa you will find a list of all plugin groups with all associated events.
Use this list as a quick reference.
a
docs.joomla.org/Plugin/Events
You already know that there are different types of extensions: Components, modules, templates,
languages and plugins. While components, modules, templates and languages usually cause a direct
output, a plugin typically works in the background. Plugins are versatile. Each plugin has its own
purpose. Let’s organise plugins a little. Even within Joomla, they are divided into plugin groups. It’s
much easier to understand the purpose if you look at each type separately. In this chapter, we will get
an overview of the different types and their special features.
The Joomla core comes with a lot of plugins. These are divided into 22 plugin types in Joomla 4.2 and
so is this part of the text. For example, there is a chapter about content plugins and another one about
system plugins.
For an overview of all plugins available in Joomla core and their associated events/events, see
the Joomla documentationa . Check out the code if you need some programming inspiration.
a
docs.joomla.org/Help4.x:Plugins:_Name_of_Plugin
In my opinion, it helps to understand Joomla plugins if you study each type on its own. That is why we
are doing this now. The types or groups are classified as follows:
Plugins of the Action Log type record user activities in the Joomla Core extensions of the page to review
them later if needed. If you want to log activities in a third-party extension, create a plugin of this type
for it.
API Authentication type plugins are used to provide authentication for web services in Joomla. Re-
member: You activated a Joomla core plugin of this type in the previous chapter on web services.
38.2.3. Authentication
When someone logs into Joomla, the Joomla application authenticates that user. On most websites,
authentication is performed against the Joomla database. This type of authentication is performed
by the authentication plugin. With an authentication plugin, it is possible to use external services to
authenticate users: Joomla provides an authentication plugin for LDAP, which is used in Windows
domains.
Joomla 3 had plugins for authentication via Gmail on board. Joomla 4 no longer offers thisa . The
technology used by the plugin is no longer state of the art and less secure. Nowadays, applications
should authorize themselves via the OAuth 2.0b protocol with Google.
a
developer.joomla.org/news/724-removal-of-the-gmail-authentication-plugin-as-of-joomla-4-0.html
b
en.wikipedia.org/wiki/oauth
38.2.4. Behaviour
Behavior type plugins are used to enable a specific behavior in the website. Examples in Joomla core
are tagging or versioning of elements.
38.2.5. CAPTCHA
Plugins of this group allow to check forms with a Captcha Check1 (engl. completely automated public
Turing test to tell computers and humans apart), a fully automated public Turing test that detects
whether a human or a machine submits the form. The Joomla core comes with a plugin for Google
reCaptcha2 . Custom captcha methods are easily added.
1
en.wikipedia.org/wiki/captcha
2
google.com/recaptcha/about/
Captchas are a nice way to add an individual touch to the website. If it is too much work to create
images that match the topic, you can work with questions. On the website of a fire department
association a possible question would be, about the color of the fire truck.
38.2.6. Content
A content plugin is mostly used to change the content of the article before it is displayed or before it is
saved in the database. Those who have special requirements can use a plugin of this type for custom
functions after the article is saved in the database. Whenever you want to customize the processing of
the content, choosing this type of plugin is perfect.
38.2.7. Editors
Editor plugins convert an HTML textarea element into a JavaScript-based editor. Well known plugins of
this group are TinyMCE and CodeMirror. If no WYSIWYG3 editor plugin is enabled, Joomla displays a
normal HTML textarea. Technically, this is also done via a plugin, namely via Editor | None.
A third party plugin from the Editor group, which is very popular in the Joomla community, is the
JCE-Editora .
a
www.joomlacontenteditor.net/
At the bottom of a Joomla editor, buttons appear in addition to the toolbar - for example, a button to
add a read more link or a button to add a page break. These buttons are generated by plugins of the
editors-xtd type.
38.2.9. Extensions
There are not many plugins in this group, nevertheless it is an interesting group. Whenever a Joomla
extension is installed or removed, it is possible to hook into the installation via a plugin of this group.An
extension plugin does a task during an installation! The Joomla plugin of extension type is used to
clean up update pages. Update pages are URLs that are stored in the extension manager for updating
extensions. Since Joomla 3.2 it is possible for commercial extensions to use this plugin to allow
private downloads with a security key. And last but not least: Extensions - Namespace Updater
automatically creates and updates the file administrator/cache/autoload_psr4.php.
3
en.wikipedia.org/wiki/wysiwyg
38.2.10. Fields
The Fields plugin type allows you to create fields in extensions that support custom fields. For example,
a calendar can be added when creating an item, through which a date is stored with the item, which is
output at a specific location in the content. This makes it easier to output content in the same layout
or to query content in other extensions. For example, a field that stores a geographic coordinate will
display a marker at that position on a digital map in a module.
38.2.11. FileSystem
Plugins of the Filesystem type are used to define one or more local directories for storing files. Do you
want to offer the flexible changing of a directory for your extension. Then check out the Joomla core
plugin Filesystem - Local with which you can set the directory where image files are stored.
38.2.12. Finder
The default search in Joomla 4 is the Search Index or Smart Search component: com_finder. In Joomla
3 this was com_search. The main difference between the two is that com_search searches the content
in real time and may open many different database tables to do so, while com_finder creates index
tables first and then searches only that index. The latter allows for a more efficient and therefore faster
full-text search. The new search index is more complex than the old classic search, which required
no configuration but offered few options. com_finder uses an active index based on stem reduction4 .
Specifically, the PHP library php-stemmer[github.com/wamania/php-stemmer] is applied. The idea
is to increase the performance and quality of the search result by covering multiple syntactic words
with a base form. For example, gardening and garden have related meanings. Each type of content
requires its own Finder plugin. Create a Finder plugin if you want content in your component to be
found,
com_search is still availablea as a decoupled component, and it also requires a separate plugin
for third-party extension content to be found.
a
github.com/joomla-extensions/search
38.2.13. Installer
Do you want to change the installation process of your extension? Then take a look at the installer type
plugins.
4
en.wikipedia.org/wiki/stemming
Cropping images, changing the size or rotating them is each possible with a core Joomla plugin from
the Media Action. Expand this plugin group if the media or image editing functions are not enough for
you.
38.2.15. Privacy
If your self-programmed extension processes personal data, then plugins of the type Privacy come into
play. Create a plugin of this type and make sure in the code that this data is correctly processed by
Joomla in the core privacy component. This is the only way Joomla can handle user requests for stored
data or deletion requests. For Joomla core extensions the required plugins are available in Joomla 4.
Use a plugin of the type quickicon to place a quickicon on the dashboard of the Joomla backend.
The Joomla Core Sample Data module provides a unified workflow for adding sample files. Want to
jump in here and make sample files installable for your extension with a click? Then, you probably
guessed it already, a plugin of the type sample files is required.
38.2.18. System
System Plugins performs a wide variety of tasks. This sounds vague, however. To make it a bit more
concrete, examples follow. System plugins can add HTML code, CSS or JavaScript to the Joomla page
after it is generated.Plugins of this type modify Joomla forms before they are generated. With the help
of system plugins alternative error handling is possible. This was only a small part of the possible. You
see, system plugins are very powerful. To be able to fulfill this powerful task, they are called frequently
and therefore need resources. Use them carefully!
Another current example is the keyboard shortcut plugin newly added in Joomla 4.2a
a
github.com/joomla/joomla-cms/pull/38092
38.2.19. Task
Do you have tasks that have to be done again and again? Or tasks for the future that you would like
to plan and definitely must not forget? Since Joomla 4.1, you can automate these with the new task
planner. And what is essential for developers: All Joomla extensions can take advantage of it and
schedule tasks and execute them regularly. Especially if the website host does not allow cron jobs.
It is possible to use the core scheduler to schedule tasks in your own extensions. Task Plugins are
integrated into Joomla via PR 351435 .
In addition to standard authentication, there is the possibility to achieve additional security by adding
a parallel second authentication.
38.2.21. User
Is there a connection between the data in a component and the users in the Joomla user management?
Technically, this is implemented with a plug-in of the user type. Are you wondering how this works?
Then take a look at the plugin for the contact component, which links a contact to a user.
A Web Services plug-in adds the routes of an extension to the website’s API. We practically used this
plugin in the previous part.
38.2.23. Workflow
In workflow management, there are different transitions that can be manipulated using a plugin.
38.3. Examples
I have created example plugins that together allow a simple realization of the IndieWeb. I have described
the setup on a website at blog.astrid-guenther.de/en/cassiopeia-joomla-indieweb. This is about the
programming.
5
github.com/joomla/joomla-cms/pull/35143
The IndieWeb allows a person to publish their thoughts and ideas in one place and then share them on
other social websites. It is important to always remain the owner of your own digital content.
What if a social network develops in such a way that you no longer feel comfortable there and therefore
no longer visit it? Or the owner of the website decides to shut it down? All your contributions are lost!
In my opinion, a digital profile and its content should not be an identity owned by an external company.
A person should be the sole owner of the content they share online. And that’s what *IndieWeb_
encourages people to do.
The IndieWeb is a people-centred alternative to the Corporate Web is a quote I took from the
website IndieWeb.orga . The website indiewebify.me/ supported me in the implementation. I first
read about this on the blog chringel.dev.
a
indieweb.org/
1. set up web sign-in To authenticate yourself as the owner of your website using your domain, you
need to set up a way to sign in using IndieAuth. That is, you use your domain to verify yourself as
the owner of your other social profiles. Simply add a rel=me microformat to all your links that
lead to your profiles on other platforms. We do this within the content plugin.
2. add author markup The next step is to provide some basic information about the author on
the website. Often there is already an about me page, but it is not machine readable. The
microformat h-card provides properties that can be parsed. I have added these invisibly to the
markup of the website in combination with the following element. This way the design of the
template is not affected.
3. add content tagging If you want to publish content on the IndieWeb, it needs to be machine-
readable. I added the h-entry microformat. The website IndieWebify.me was a great help in
this step. In this plugin I add the following h-entry properties:
4. add webmentions What are webmentions? Webmentions are a W3C Recommendation6 for
conversations and interactions on web pages. It is a simple way to notify a URL when it is
6
w3.org/TR/webmention/
mentioned on a web page. Basically, it’s a way to interact with other people’s content from your
own website.
Example: I read a post on another blog and want to respond to it. I can do that by writing a post on
my website and linking to the other post. Then I can send a webmention to the other blog to let them
know that I have reacted to the post from my website. That sounds complicated? Well, it’s just like
most social networks where you respond to a post by commenting or liking it.
There is a simple way to set up webmentions: Webmention.io. It’s a service that handles webmentions
by using web sign-in and adding some endpoints as links to your website. Here in the example we set
the endpoints, which I add to the head of the website via a system plugin.
What was missing was a way to display the webmentions. The procedure in the content plugin for
parsing webmentions is currently dynamic. This is not performant. A better solution is to retrieve the
webmentions from time to time and store them in the database.
5. syndication and backfeed A final piece of the puzzle are: POSSE and Backfeed.
POSSE means that you first publish your content on your own website and then post links on other
platforms (Publish on Site, Syndicate Elsewhere). For example, by sharing about your post on Mastodon
and then adding a link to your website.
Backfeed describes the process of pulling the interactions of your POSSE copy to the original post. So
when someone comments on a toot with the link to your post, it is actually redirected to your website
as a webmention.
Working through the 5 points makes a Joomla website a IndieWeb citizen. The plugins described
below are a simple implementation. Web Sign-In can be used via the system plugin, there is
content with microformats via the content plugin and webmentions are sent to and received from
other IndieWeb sites. Syndication is a problematic issue. The process is a bit convoluted and I’m
not sure I’m implementing it properly. You have to publish your own post first, then share the
link, and lastly add that shared link to your own post. This is where the editors-xtd plugin helps.
38.3.2. Fields
The Custom Field is intended to support inserting a Reply-toa element.
a
indieweb.org/in-reply-to
A custom form field, written for a custom field itself, is searched for by default in the /fields subdirec-
tory. You can find the code for this search in the onCustomFieldsGetTypes() function. This is im-
plugins/fields/indieweb/fields/indieweb.php
1
2 <?php
3
4 \defined('JPATH_PLATFORM') or die;
5
6
7 class JFormFieldIndieweb extends Joomla\CMS\Form\Field\UrlField
8 {
9 protected $type = 'indieweb';
10 }
plugins/fields/indieweb/indieweb.php
1
2 <?php
3
4 use Joomla\CMS\Form\Form;
5
6 \defined('_JEXEC') or die;
7
8 class PlgFieldsIndieweb extends \Joomla\Component\Fields\Administrator\
Plugin\FieldsPlugin
9 {
10 public function onCustomFieldsPrepareDom($field, DOMElement $parent
, Form $form)
11 {
12 $fieldNode = parent::onCustomFieldsPrepareDom($field, $parent,
$form);
13
14 if (!$fieldNode) {
15 return $fieldNode;
16 }
17
18 $fieldNode->setAttribute('validate', 'indieweb');
19
20 return $fieldNode;
21 }
22 }
Das XML-Manifest wird für die Installation verwendet. Die Parameter werden später noch einmal für
ein einzelnes Feld implementiert. Hier im Installationsmanifest stehen sie, damit man sie global im
Plugin-Manager setzen kann.
plugins/fields/indieweb/indieweb.xml
1
2 <?xml version="1.0" encoding="utf-8" ?>
3 <extension type="plugin" group="fields" method="upgrade">
4 <name>plg_fields_indieweb</name>
5 <creationDate>[DATE]</creationDate>
6 <author>[AUTHOR]</author>
7 <authorEmail>[AUTHOR_EMAIL]</authorEmail>
8 <authorUrl>[AUTHOR_URL]</authorUrl>
9 <copyright>[COPYRIGHT]</copyright>
10 <license>GNU General Public License version 2 or later;</license>
11 <version>__BUMP_VERSION__</version>
12 <description>PLG_FIELDS_INDIEWEB_XML_DESCRIPTION</description>
13 <files>
14 <filename plugin="indieweb">indieweb.php</filename>
15 <folder>params</folder>
16 <folder>language</folder>
17 <folder>fields</folder>
18 <folder>tmpl</folder>
19 <folder>fields</folder>
20 <folder>rules</folder>
21 </files>
22 <config>
23 <fields name="params">
24 <fieldset name="basic">
25 <field
26 name="schemes"
27 type="list"
28 label="PLG_FIELDS_INDIEWEB_PARAMS_SCHEMES_LABEL"
29 multiple="true"
30 layout="joomla.form.field.list-fancy-select"
31 validate="options"
32 >
33 <option value="http">HTTP</option>
34 <option value="https">HTTPS</option>
35 </field>
36
37 <field
38 name="relative"
39 type="radio"
40 label="PLG_FIELDS_INDIEWEB_PARAMS_RELATIVE_LABEL"
41 layout="joomla.form.field.radio.switcher"
42 default="1"
43 filter="integer"
44 >
45 <option value="0">JNO</option>
46 <option value="1">JYES</option>
47 </field>
48 </fieldset>
49 </fields>
50 </config>
51 </extension>
Als nächste sind die Sprachdateien für die Übersetzung der Vollständigkeit halber abgedruckt.
plugins/fields/indieweb/language/en-GB/plg_fields_indieweb.ini
1
2 PLG_FIELDS_INDIEWB="Fields - INDIEWEB"
3 PLG_FIELDS_INDIEWEB_LABEL="INDIEWEB (%s)"
4 PLG_FIELDS_INDIEWEB_PARAMS_RELATIVE_LABEL="Relative URLs"
5 PLG_FIELDS_INDIEWEB_PARAMS_SCHEMES_LABEL="Schemes"
6 PLG_FIELDS_INDIEWEB_PARAMS_SHOW_URL="Show URL"
7 PLG_FIELDS_INDIEWEB_XML_DESCRIPTION="This plugin lets you create new
fields of type 'URL' in any extensions where custom fields are
supported."
8 JVISIT_REPLY_TO_WEBSITE="In reply to website: "
9 JVISIT_REPLY_TO_LINK="In reply to internal link: "
plugins/fields/indieweb/language/en-GB/plg_fields_indieweb.sys.ini
1
2 PLG_FIELDS_INDIEWEB="Fields - INDIEWEB"
3 PLG_FIELDS_INDIEWEB_XML_DESCRIPTION="This plugin lets you create new
fields of type 'URL' in any extensions where custom fields are
supported."
plugins/fields/indieweb/params/indieweb.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <form>
4 <fields name="fieldparams">
5 <fieldset name="fieldparams">
6 <field
7 name="schemes"
8 type="list"
9 label="PLG_FIELDS_INDIEWEB_PARAMS_SCHEMES_LABEL"
10 multiple="true"
11 layout="joomla.form.field.list-fancy-select"
12 validate="options"
13 >
14 <option value="http">HTTP</option>
15 <option value="https">HTTPS</option>
16 </field>
17
18 <field
19 name="relative"
20 type="list"
21 label="PLG_FIELDS_INDIEWEB_PARAMS_RELATIVE_LABEL"
22 filter="integer"
23 validate="options"
24 >
25 <option value="">COM_FIELDS_FIELD_USE_GLOBAL</option>
26 <option value="1">JYES</option>
27 <option value="0">JNO</option>
28 </field>
29
30 <field
31 name="show_url"
32 type="radio"
33 label="PLG_FIELDS_INDIEWEB_PARAMS_SHOW_URL"
34 layout="joomla.form.field.radio.switcher"
35 default="1"
36 filter="integer"
37 >
38 <option value="0">JNO</option>
39 <option value="1">JYES</option>
40 </field>
41 </fieldset>
42 </fields>
43 </form>
The rules for validation belong in the rules directory. This is implemented in the file administrator
/components/com_fields/src/Plugin/FieldsPlugin.php#L96. Again, I made it simple and
copied from the validation of the url field. Primarily, I want to show where the files are inserted so that
they are found correctly by Joomla.
plugins/fields/indieweb/rules/indieweb.php
1
2 <?php
3
4 use Joomla\CMS\Form\Form;
5 use Joomla\CMS\Form\FormRule;
6 use Joomla\CMS\Language\Text;
7 use Joomla\Registry\Registry;
8 use Joomla\String\StringHelper;
9 use Joomla\Uri\UriHelper;
10
11 \defined('JPATH_PLATFORM') or die;
12
13 class JFormRuleIndieweb extends FormRule
14 {
15 public function test(\SimpleXMLElement $element, $value, $group =
null, Registry $input = null, Form $form = null)
16 {
17 // If the field is empty and not required, the field is valid.
18 $required = ((string) $element['required'] === 'true' || (
string) $element['required'] === 'required');
19
20 if (!$required && empty($value)) {
21 return true;
22 }
23
24 $urlParts = UriHelper::parse_url($value);
25
26 // See https://www.w3.org/Addressing/URL/url-spec.txt
27 // Use the full list or optionally specify a list of permitted
schemes.
28 if ($element['schemes'] == '') {
29 $scheme = ['http', 'https'];
30 } else {
31 $scheme = explode(',', $element['schemes']);
32 }
33
34 /*
35 if ($urlParts === false || !\array_key_exists('scheme',
$urlParts)) {
36 /*
37 if ($urlParts === false || !$element['relative']) {
38 $element->addAttribute('message', Text::sprintf('
JLIB_FORM_VALIDATE_FIELD_URL_SCHEMA_MISSING', $value
, implode(', ', $scheme)));
39
40 return false;
41 }
42
43 // The best we can do for the rest is make sure that the
path exists and is valid UTF-8.
44 if (!\array_key_exists('path', $urlParts) || !StringHelper
::valid((string) $urlParts['path'])) {
45 return false;
46 }
47
48 // The internal URL seems to be good.
49 return true;
50 }
51
52 // Scheme found, check all parts found.
53 $urlScheme = (string) $urlParts['scheme'];
54 $urlScheme = strtolower($urlScheme);
55
56 if (\in_array($urlScheme, $scheme) == false) {
57 return false;
58 }
59
60 // For some schemes here must be two slashes.
61 $scheme = ['http', 'https'];
62
63 if (\in_array($urlScheme, $scheme) && substr($value, \strlen(
$urlScheme), 3) !== '://') {
64 return false;
65 }
66
67 // The best we can do for the rest is make sure that the
strings are valid UTF-8
68 // and the port is an integer.
69 if (\array_key_exists('host', $urlParts) && !StringHelper::
valid((string) $urlParts['host'])) {
70 return false;
71 }
72
73 if (\array_key_exists('port', $urlParts) && !\is_int((int)
$urlParts['port'])) {
74 return false;
75 }
76
77 if (\array_key_exists('path', $urlParts) && !StringHelper::
valid((string) $urlParts['path'])) {
78 return false;
79 }
80
81 return true;
82 }
83 }
plugins/fields/indieweb/tmpl/indieweb.php
1
2 <?php
3
4 defined('_JEXEC') or die;
5
6 use Joomla\CMS\Language\Text;
7 use Joomla\CMS\Uri\Uri;
8
9 $value = $field->value;
10
11 if ($value == '') {
12 return;
13 }
14
15 $attributes = '';
16
17 $attributes = ' target="_self"';
18
19 if (!Uri::isInternal($value)) {
20 $text = Text::_('JVISIT_REPLY_TO_WEBSITE');
21 } else {
22 $text = Text::_('JVISIT_REPLY_TO_LINK');
23 }
24
25 if ($fieldParams->get('show_url', 0)) {
26 $text = $text . htmlspecialchars($value);
27 }
28
29 echo sprintf(
30 '<div class="u-in-reply-to h-cite"><a class="u-url" href="%s"%s>%s
</a></div>',
31 htmlspecialchars($value),
32 $attributes,
33 $text
34 );
38.3.3. Task
The task plugin is there to fetch Webmention from the website webmention.io at regular time
intervals.
We start with the manifest for the installation. Note that we use namespace here.
plugins/task/indieweb/indieweb.xml
1
2 <?xml version="1.0" encoding="utf-8" ?>
3 <extension type="plugin" group="task" method="upgrade">
4 <name>plg_task_indie_web</name>
5 <author>Astrid Günther</author>
6 <creationDate>[DATE]</creationDate>
7 <author>[AUTHOR]</author>
8 <authorEmail>[AUTHOR_EMAIL]</authorEmail>
9 <authorUrl>[AUTHOR_URL]</authorUrl>
10 <copyright>[COPYRIGHT]</copyright>
11 <license>GNU General Public License version 2 or later;</license>
12 <version>__BUMP_VERSION__</version>
13 <description>PLG_TASK_INDIE_WEB_XML_DESCRIPTION</description>
14 <namespace path="src">Joomla\Plugin\Task\IndieWeb</namespace>
15 <files>
16 <folder plugin="indieweb">services</folder>
17 <file>indieweb.xml</file>
18 <file>webmentions.json</file>
19 <folder>language</folder>
20 <folder>src</folder>
21 </files>
22 <config>
23 <fields name="params">
24 <fieldset name="basic">
25 </fieldset>
26 <fieldset name="WEBMENTION_IO">
27 <field
28 name="token"
29 type="text"
30 label="PLG_TASK_INDIEWEB_WEBMENTION_IO_TOKEN_LABEL"
31 description="
PLG_TASK_INDIEWEB_WEBMENTION_IO_TOKEN_DESC"
32 />
33 </fieldset>
34 </fields>
35 </config>
36 </extension>
plugins/task/indieweb/language/en-GB/plg_task_indieweb.ini
1
2 PLG_TASK_INDIE_WEB="Task - Indieweb"
3 PLG_TASK_INDIE_WEB_DESC="Fetches webmentions on each run."
4 PLG_TASK_INDIE_WEB_ERROR_WEBMENTIONS_PHP_NOTUNWRITABLE="Could not make
configuration.php un-writable."
5 PLG_TASK_INDIE_WEB_ERROR_WEBMENTIONS_PHP_NOTWRITABLE="Could not make
configuration.php writable."
6 PLG_TASK_INDIE_WEB_ERROR_WRITE_FAILED="Could not write to the
configuration file!"
7 PLG_TASK_INDIE_WEB_ROUTINE_END_LOG_MESSAGE="ToggleOffline return code
is: %1$d. Processing Time: %2$.2f seconds."
8 PLG_TASK_INDIE_WEB_TASK_LOG_INDIE_WEB="Webmentions in File %1$s."
9 PLG_TASK_INDIE_WEB_TITLE="Fetches webmentions"
10 PLG_TASK_INDIE_WEB_XML_DESCRIPTION="Offers task routines to fetch
webmentions."
plugins/task/indieweb/language/en-GB/plg_task_indieweb.sys.ini
1
2 PLG_TASK_INDIE_WEB="Task - Indieweb"
1
2 <?php
3
4 defined('_JEXEC') or die;
5
6 use Joomla\CMS\Extension\PluginInterface;
7 use Joomla\CMS\Factory;
8 use Joomla\CMS\Plugin\PluginHelper;
9 use Joomla\DI\Container;
10 use Joomla\DI\ServiceProviderInterface;
11 use Joomla\Event\DispatcherInterface;
12 use Joomla\Plugin\Task\IndieWeb\Extension\IndieWeb;
13 use Joomla\Utilities\ArrayHelper;
14
15 return new class implements ServiceProviderInterface
16 {
17 public function register(Container $container)
18 {
19 $container->set(
20 PluginInterface::class,
21 function (Container $container) {
22 $plugin = new IndieWeb(
23 $container->get(DispatcherInterface::class),
24 (array) PluginHelper::getPlugin('task', 'indieweb')
,
25 ArrayHelper::fromObject(new JConfig()),
26 JPATH_BASE . '/plugins/task/indieweb/webmentions.
json'
27 );
28 $plugin->setApplication(Factory::getApplication());
29
30 return $plugin;
31 }
32 );
33 }
34 };
plugins/task/indieweb/src/Extension/IndieWeb.php
1
2 <?php
3
4 namespace Joomla\Plugin\Task\IndieWeb\Extension;
5
6 use Exception;
7 use Joomla\CMS\Plugin\CMSPlugin;
8 use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent;
9 use Joomla\Component\Scheduler\Administrator\Task\Status;
10 use Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait;
11 use Joomla\Event\DispatcherInterface;
12 use Joomla\Event\SubscriberInterface;
13 use Joomla\Filesystem\File;
14 use Joomla\Filesystem\Path;
15 use Joomla\Registry\Registry;
16
17 \defined('_JEXEC') or die;
18
19 final class IndieWeb extends CMSPlugin implements SubscriberInterface
20 {
21 use TaskPluginTrait;
22
23 protected const TASKS_MAP = [
24 'plg_task_fetch_webmentions' => [
25 'langConstPrefix' => 'PLG_TASK_INDIE_WEB',
26 ],
27 ];
28
29 protected $autoloadLanguage = true;
30
31 public static function getSubscribedEvents(): array
32 {
33 return [
34 'onTaskOptionsList' => 'advertiseRoutines',
35 'onExecuteTask' => 'alterIndiewebStatus',
36 ];
37 }
38
39 private $webmentionFile;
40
41 public function __construct(DispatcherInterface $dispatcher, array
$config, array $jConfig, string $webmentionFile)
42 {
43 parent::__construct($dispatcher, $config);
44
45 $this->webmentionFile = $webmentionFile;
46 }
47
48 public function alterIndiewebStatus(ExecuteTaskEvent $event): void
49 {
50 if (!array_key_exists($event->getRoutineId(), self::TASKS_MAP))
{
51 return;
52 }
53
54 $this->startRoutine($event);
55
56 $exit= $this->writewebmentionFile($this->webmentionFile);
57 $this->logTask(sprintf($this->getApplication()->getLanguage()->
_('PLG_TASK_INDIE_WEB_TASK_LOG_INDIE_WEB'), $this->
webmentionFile));
58
59 $this->endRoutine($event, $exit);
60 }
61
62 private function writewebmentionFile(string $config): int
63 {
64 $file = $this->webmentionFile;
65
66 if (file_exists($file) && Path::isOwner($file) && !Path::
setPermissions($file)) {
67 $this->logTask($this->getApplication()->getLanguage()->_('
PLG_TASK_INDIE_WEB_ERROR_WEBMENTIONS_PHP_NOTWRITABLE'),
'notice');
68 }
69
70 try {
71 $curl = curl_init();
72 curl_setopt($curl, CURLOPT_URL, 'https://webmention.io/api/
mentions.jf2?token=' . $this->params->get('token'));
73 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
74
75 $response = curl_exec($curl);
76
77 if ($response === false) {
78 $curlError = curl_error($curl);
79 curl_close($curl);
80 throw new ApiException('cURL Error: ' . $curlError);
81 }
82
83 $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
84
85 if ($httpCode >= 400) {
86 curl_close($curl);
87 $responseParsed = json_decode($response);
plugins/task/indieweb/webmentions.json
1 /* https://codeberg.org/astrid/j4examplecode/raw/branch/t30a/src/
plugins/task/indieweb/webmentions.json */
2 {
3 "type": "feed",
4 "name": "Webmentions",
5 "children": [
6 {
7 "type": "entry",
8 "author": {
9 "type": "card",
10 "name": "Astrid",
11 "photo": "https://webmention.io/avatar/fimidi.com/19be.
jpg",
12 "url": "https://fimidi.com/@astrid"
13 },
14 "url": "https://fimidi.com/@astrid/109303891082037165",
15 "published": "2022-11-07T18:16:57+00:00",
16 "wm-received": "2022-11-13T10:32:24Z",
17 "wm-id": 1557987,
18 "wm-source": "https://fimidi.com/web/@astrid
/109303891082037165",
19 "wm-target": "https://astrid-guenther.de/en/
webprogrammierung/imagemap-and-or-advent-calender-for-
joomla",
20 "content": {
21 "html": "<p>I like advent calendars. I am currently in
the process of designing and ...</p>",
22 "text": "I like advent calendars. I am currently in the
process of designing and ..."
23 },
24 "mention-of": "https://astrid-guenther.de/en/
webprogrammierung/imagemap-and-or-advent-calender-for-
joomla",
25 "wm-property": "mention-of",
26 "wm-private": false
27 }
28 ]
29 }
38.3.4. System
For inserting elements in the <head> of the HTML markup we access a system plugin.
This onAfterDispatch event is fired after the framework is loaded and the application initialization
method is called. Here it is possible to insert elements into the document.
plugins/system/indieweb/indieweb.php
1
2 <?php
3
4 use Joomla\CMS\Plugin\CMSPlugin;
5
6 \defined('_JEXEC') or die;
7
8 class PlgSystemIndieweb extends CMSPlugin
9 {
10 protected $app;
11
12 public function onAfterDispatch()
13 {
14 $doc = $this->app->getDocument();
plugins/system/indieweb/indieweb.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <extension type="plugin" group="system" method="upgrade">
4 <name>plg_system_indieweb</name>
5 <creationDate>[DATE]</creationDate>
6 <author>[AUTHOR]</author>
7 <authorEmail>[AUTHOR_EMAIL]</authorEmail>
8 <authorUrl>[AUTHOR_URL]</authorUrl>
9 <copyright>[COPYRIGHT]</copyright>
10 <license>GNU General Public License version 2 or later;</license>
11 <version>__BUMP_VERSION__</version>
12 <description>PLG_SYSTEM_INDIEWEB_XML_DESCRIPTION</description>
13 <files>
14 <file>indieweb.xml</file>
15 <file plugin="indieweb">indieweb.php</file>
16 <folder>language</folder>
17 </files>
18 <config>
19 <fields name="params">
20 <fieldset name="basic">
21 <field
22 name="authorization_endpoint"
23 type="url"
24 label="
PLG_SYSTEM_INDIEWEB_AUTHORIZATION_ENDPOINT_LABEL
"
25 description="
PLG_SYSTEM_INDIEWEB_AUTHORIZATION_ENDPOINT_DESC"
26 hint="https://indieauth.com/auth"
27 filter="url"
28 validate="url"
29 />
30 <field
31 name="token_endpoint"
32 type="url"
33 label="PLG_SYSTEM_INDIEWEB_TOKEN_ENDPOINT_LABEL"
34 description="
PLG_SYSTEM_INDIEWEB_TOKEN_ENDPOINT_DESC"
35 hint="https://tokens.indieauth.com/token"
36 filter="url"
37 validate="url"
38 />
39 <field
40 name="webmention"
41 type="url"
42 label="PLG_SYSTEM_INDIEWEB_WEBMENTIOM_LABEL"
43 description="PLG_SYSTEM_INDIEWEB_WEBMENTIOM_DESC"
44 hint="https://webmention.io/example.org/webmention"
45 filter="url"
46 validate="url"
47 />
48 <field
49 name="pingback"
50 type="url"
51 label="PLG_SYSTEM_INDIEWEB_PINGBACK_LABEL"
52 description="PLG_SYSTEM_INDIEWEB_PINGBACK_DESC"
53 hint="https://webmention.io/example.org/xmlrpc"
54 filter="url"
55 validate="url"
56 />
57 </fieldset>
58 </fields>
59 </config>
60 </extension>
plugins/system/indieweb/language/en-GB/plg_system_indieweb.ini
1
2 PLG_SYSTEM_INDIEWEB="System - Indieweb"
3 PLG_SYSTEM_INDIEWEB_XML_DESCRIPTION="Inserts meta information in the
header of the website.<ol><li><link rel='authorization_endpoint'
href='https://eample.org' /><li><link rel='token_endpoint'
href='https://eample.org' /><li><link rel='webmention' href='
https://eample.org' /><li><link rel='pingback' href='https://
eample.org' />"
plugins/system/indieweb/language/en-GB/plg_system_indieweb.sys.ini
1
2 PLG_SYSTEM_INDIEWEB="System - Indieweb"
3 PLG_SYSTEM_INDIEWEB_XML_DESCRIPTION="Inserts meta information in the
38.3.5. Content
The content plugin adds elements to the HTML markup that meet the minimum syntactic rules of
the IndieWeb. The elements are partially assigned the CSS class hidden and therefore do not
appear in the default template Cassiopeia. I accept the disadvantage that the content appears
twice in the markup. The advantage is that I am not dependent on how a template renders the
content and that the plugin does not affect the appearance of the website.
plugins/content/indieweb/indieweb.php
1
2 <?php
3
4 use Joomla\CMS\Factory;
5 use Joomla\CMS\Language\Multilanguage;
6 use Joomla\CMS\Plugin\CMSPlugin;
7 use Joomla\CMS\Router\Route;
8 use Joomla\Component\Contact\Site\Helper\RouteHelper;
9 use Joomla\Database\ParameterType;
10 use Joomla\Registry\Registry;
11
12 \defined('_JEXEC') or die;
13
14 class PlgContentIndieweb extends CMSPlugin
15 {
16 protected $db;
17
18 public function onContentPrepare($context, &$row, $params, $page =
0)
19 {
20 if ($context === 'com_finder.indexer') {
21 return;
22 }
23
24 $allowed_contexts = ['com_content.article', 'com_agadvents.
agadvent'];
25
26 if (!in_array($context, $allowed_contexts)) {
27 return;
28 }
29
30 if (!($params instanceof Registry)) {
31 return;
32 }
33
34 if (!isset($row->id) || !(int) $row->id) {
35 return;
36 }
37
38 if ($context === 'com_content.article') {
39 $indieweb = $this->getIndiewebData($row->created_by);
40 $row->contactid = $indieweb->contactid;
41 $row->webpage = $indieweb->webpage;
42 $row->email = $indieweb->email_to;
43 $row->authorname = $indieweb->name;
44 }
45
46 // Todo Save created_by with agadvent
47 if ($context === 'com_agadvents.agadvent') {
48 $row->webpage = "";
49 $row->email = "";
50 $row->authorname = "Advent";
51 $row->title = $row->name;
52 $row->introtext = '';
53 $row->text = $row->fulltext;
54 }
55
56 $url = $this->params->get('url', 'url');
57
58 $row->indieweb_link = '';
59
60 // Web Sign In
61 $row->text = $row->text . '<div class="hidden"><ul>';
62 $row->text = $row->text . '<li><a rel="me" href="mailto:' .
$row->email . '">' . $row->email . '</a></li>';
63
64 foreach ($this->params->get('websignin') as $websigninitem) {
65 $row->text = $row->text . '<li><a rel="me" href="' .
$websigninitem->websignin_url . '">' . $websigninitem->
websignin_url . '</a></li>';
66 }
67 $row->text = $row->text . '</ul></div>';
68
69
70 // Content
71 $row->text = $row->text . '<article class="hidden h-entry">
72 <h1 class="p-name">' . $row->title . '</h1>
73 <p>Published by
74 <p class="p-author h-card"><a class="u-url u-uid" href="' .
$row->webpage . '">' . $row->authorname . '</a></p> on
75
76 <time class="dt-published" datetime="' . $row->publish_up . '">
' . $row->publish_up . '</time>
77 </p>
78 <p class="p-summary">' . $row->introtext . '</p>
79 <div class="e-content">' . str_replace($row->introtext, '',
$row->text) . '</div>
80 </article>';
81
82
83 $webmention_file = JPATH_BASE . '/plugins/task/indieweb/
webmentions.json';
84 $webmentions = file_get_contents($webmention_file);
85 $webmentions = json_decode($webmentions);
86
87 $webmentions_urls = "";
88 if ($webmentions !== null) {
89 foreach ($webmentions->children as $i => $webmention) {
90 if (str_contains($webmention->{'wm-target'}, $row->
alias)) {
137
138 $query->select($db->quoteName('contact.id', 'contactid'))
139 ->select(
140 $db->quoteName(
141 [
142 'contact.alias',
143 'contact.catid',
144 'contact.webpage',
145 'contact.email_to',
146 'contact.name',
147 ]
148 )
149 )
150 ->from($db->quoteName('#__contact_details', 'contact'))
151 ->where(
152 [
153 $db->quoteName('contact.published') . ' = 1',
154 $db->quoteName('contact.user_id') . ' = :createdby'
,
155 ]
156 )
157 ->bind(':createdby', $userId, ParameterType::INTEGER);
158
159 if (Multilanguage::isEnabled() === true) {
160 $query->where(
161 '(' . $db->quoteName('contact.language') . ' IN ('
162 . implode(',', $query->bindArray([Factory::getLanguage
()->getTag(), '*'], ParameterType::STRING))
163 . ') OR ' . $db->quoteName('contact.language') . ' IS
NULL)'
164 );
165 }
166
167 $query->order($db->quoteName('contact.id') . ' DESC')
168 ->setLimit(1);
169
170 $db->setQuery($query);
171
172 $indiewebs[$userId] = $db->loadObject();
173
174 return $indiewebs[$userId];
175 }
176 }
plugins/content/indieweb/indieweb.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <extension type="plugin" group="content" method="upgrade">
4 <name>plg_content_indieweb</name>
5 <author>Astrid Günther</author>
6 <creationDate>[DATE]</creationDate>
7 <author>[AUTHOR]</author>
8 <authorEmail>[AUTHOR_EMAIL]</authorEmail>
9 <authorUrl>[AUTHOR_URL]</authorUrl>
10 <copyright>[COPYRIGHT]</copyright>
11 <license>GNU General Public License version 2 or later;</license>
12 <version>__BUMP_VERSION__</version>
13 <description>PLG_CONTENT_INDIEWEB_XML_DESCRIPTION</description>
14 <files>
15 <file>indieweb.xml</file>
16 <file plugin="indieweb">indieweb.php</file>
17 <folder>language</folder>
18 </files>
19 <config>
20 <fields name="params">
21 <fieldset name="basic">
22 </fieldset>
23 <fieldset name="WebSignIn">
24 <field
25 name="websignin"
26 type="subform"
27 label="PLG_CONTENT_INDIEWEB_WEBSIGNIN_LABEL"
28 description="PLG_CONTENT_INDIEWEB_WEBSIGNIN_DESC"
29 layout="joomla.form.field.subform.repeatable-table"
30 icon="list"
31 multiple="true"
32 default=''
33 >
34 <form repeat="true">
35 <field
36 name="websignin_url"
37 type="url"
38 label="
PLG_CONTENT_INDIEWEB_WEBSIGNIN_URL_LABEL
"
39 hint="mailto:[email protected] or https://
fimidi.com/@username"
40 filter="url"
41 validate="url"
42 size="50"
43 />
44 </form>
45 </field>
46 </fieldset>
47 </fields>
48 </config>
49 </extension>
Below are the two language files necessary for correct translation.
plugins/content/indieweb/language/en-GB/plg_content_indieweb.ini
1
2 PLG_CONTENT_INDIEWEB="Content - Indieweb"
3 PLG_CONTENT_INDIEWEB_XML_DESCRIPTION="Adds visible and invisible
information about the content, the author of the content,
webmentions and syndication links for the indieweb. Requirement: The
user who wrote the post must be connected to a contact."
plugins/content/indieweb/language/en-GB/plg_content_indieweb.sys.ini
1
2 PLG_CONTENT_INDIEWEB="Content - Indieweb"
3 PLG_CONTENT_INDIEWEB_XML_DESCRIPTION="Adds visible and invisible
information about the content, the author of the content,
webmentions and syndication links for the indieweb. Requirement: The
user who wrote the post must be connected to a contact."
4
5 COM_PLUGINS_WEBSIGNIN_FIELDSET_LABEL="Web Sign In"
6 PLG_CONTENT_INDIEWEB_WEBSIGNIN_LABEL="Web Sign In URLs"
7 PLG_CONTENT_INDIEWEB_WEBSIGNIN_URL_LABEL="URL"
8 PLG_CONTENT_INDIEWEB_WEBSIGNIN_DESC="<p>In order to be able to sign in
using your domain name, connect it to your existing identities. You
probably already have many disconnected profiles on the web. </p><p>
Linking between them and your domain name with the rel=me
microformat ensures that i t s easy to see that you on Google/
Twitter/Github/Flickr/Facebook/email are all the same person as your
domain name (https://indieweb.org/How_to_set_up_web_sign-
in_on_your_own_domain).</p><p>The outer container contains the class
hidden, so that the information is inserted hidden on the website
in a template that styles the class with display:none.</p>"
media/plg_editors-xtd_indieweb/joomla.asset.json
1 /* https://codeberg.org/astrid/j4examplecode/raw/branch/t30a/src/media/
plg_editors-xtd_indieweb/joomla.asset.json */
2
3 {
4 "$schema": "https://developer.joomla.org/schemas/json-schema/
web_assets.json",
5 "name": "plg_editors-xtd_indieweb",
6 "version": "4.0.0",
7 "description": "Joomla CMS",
8 "license": "GPL-2.0-or-later",
9 "assets": [
10 {
11 "name": "plg_editors-xtd_indieweb.admin-article-indieweb",
12 "type": "script",
13 "uri": "plg_editors-xtd_indieweb/admin-article-indieweb.js",
14 "dependencies": [
15 "core"
16 ],
17 "attributes": {
18 "nomodule": true,
19 "defer": true
20 },
21 "version": "3caf2bd836dad54185a2fbb3c9a625b7576d677c"
22 }
23 ]
24 }
1 /* https://codeberg.org/astrid/j4examplecode/raw/branch/t30a/src/media/
plg_editors-xtd_indieweb/js/admin-article-indieweb.js */
2
3 (() => {
4
5 const options = window.Joomla.getOptions('xtd-indieweb');
6
7 window.insertIndieweb = editor => {
8 if (!options) {
9 // Something went wrong!
10 throw new Error('XTD Button \'indieweb\' not properly initialized
');
11 }
12
13 const content = window.Joomla.editors.instances[editor].getValue();
14
15 if (!content) {
16 Joomla.editors.instances[editor].replaceSelection('{
loadsyndication testurl,testurl2,testurl3}');
17 } else if (content && !content.match(/{loadsyndication\s/i)) {
18 Joomla.editors.instances[editor].replaceSelection('{
loadsyndication testurl,testurl2,testurl3}');
19 } else {
20 // @todo replace with joomla-alert
21 alert(options.exists);
22 return false;
23 }
24
25 return true;
26 };
27 })();
The file plugins/editors-xtd/indieweb/indieweb.php inserts the button in the editor for the
event onDisplay.
plugins/editors-xtd/indieweb/indieweb.php
1
2 <?php
3
4 use Joomla\CMS\Language\Text;
5 use Joomla\CMS\Object\CMSObject;
6 use Joomla\CMS\Plugin\CMSPlugin;
7
8 \defined('_JEXEC') or die;
9
10 class PlgButtonIndieweb extends CMSPlugin
11 {
12 protected $autoloadLanguage = true;
13
14 protected $app;
15
16 public function onDisplay($name)
17 {
18 $doc = $this->app->getDocument();
19 $doc->getWebAssetManager()
20 ->registerAndUseScript('plg_editors-xtd_indieweb.admin-
article-indieweb', 'plg_editors-xtd_indieweb/admin-
article-indieweb.min.js', [], ['defer' => true], ['core'
]);
21
22 // Pass some data to javascript
23 $doc->addScriptOptions(
24 'xtd-indieweb',
25 [
26 'exists' => Text::_('PLG_EDITORS-
XTD_INDIEWEB_ALREADY_EXISTS', true),
27 ]
28 );
29
30 $button = new CMSObject();
31 $button->modal = false;
32 $button->onclick = 'insertIndieweb(\'' . $name . '\');return
false;';
33 $button->text = Text::_('PLG_EDITORS-
XTD_INDIEWEB_BUTTON_INDIEWEB');
The installation manifest lists the information and files necessary for installation.
plugins/editors-xtd/indieweb/indieweb.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <extension type="plugin" group="editors-xtd" method="upgrade">
4 <name>plg_editors-xtd_indieweb</name>
5 <author>Astrid Günther</author>
6 <creationDate>[DATE]</creationDate>
7 <author>[AUTHOR]</author>
8 <authorEmail>[AUTHOR_EMAIL]</authorEmail>
9 <authorUrl>[AUTHOR_URL]</authorUrl>
10 <copyright>[COPYRIGHT]</copyright>
11 <license>GNU General Public License version 2 or later;</license>
12 <version>__BUMP_VERSION__</version>
13 <description>PLG_EDITORS-XTD_INDIEWEB_XML_DESCRIPTION</description>
14 <files>
15 <file>indieweb.xml</file>
16 <file plugin="indieweb">indieweb.php</file>
17 <folder>language</folder>
18 </files>
19 </extension>
plugins/editors-xtd/indieweb/language/en-GB/plg_editors-xtd_indieweb.ini
1
2 PLG_EDITORS-XTD_INDIEWEB="Button - IndieWeb Syndication"
3 PLG_EDITORS-XTD_INDIEWEB_XML_DESCRIPTION="Enables a button which allows
you to insert the <em>IndieWeb Syndication …</em> link into
an Article. See Content Plugin Indieweb"
4 PLG_EDITORS-XTD_INDIEWEB_ALREADY_EXISTS="There is already a IndieWeb
Syndication link that has been inserted. Only one link is permitted.
"
5 PLG_EDITORS-XTD_INDIEWEB_BUTTON_INDIEWEB="IndieWeb Syndications"
plugins/editors-xtd/indieweb/language/en-GB/plg_editors-xtd_indieweb.sys.ini
1
2 PLG_EDITORS-XTD_INDIEWEB="Button - IndieWeb Syndication"
3 PLG_EDITORS-XTD_INDIEWEB_XML_DESCRIPTION="Enables a button which allows
you to insert the <em>IndieWeb Syndication …</em> link into
an Article. See Content Plugin Indieweb"
Do you want your plugin to be activated automatically during an installation? In that case, add the
following code7 to an installation script.
1 defined('_JEXEC') || die;
2
3 use Joomla\CMS\Factory;
4 use Joomla\CMS\Installer\Adapter\PluginAdapter;
5 use Joomla\CMS\Installer\InstallerScript;
6
7 class plgYourplugintypYourpluginnameInstallerScript extends
InstallerScript
8 {
9 public function postflight($type, PluginAdapter $parent)
10 {
11 // Enable the plugin
12 if ($type === 'install' || $type === 'discover_install') {
13 $db = Factory::getDbo();
14 $query = $db->getQuery(true)
15 ->update('#__extensions')
16 ->set($db->qn('enabled') . ' = 1')
17 ->where($db->qn('type') . ' = ' . $db->q('plugin'))
18 ->where($db->qn('element') . ' = ' . $db->q('yourpluginname'))
19 ->where($db->qn('folder') . ' = ' . $db->q('yourplugintyp'));
20 $db->setQuery($query);
21 try {
22 $db->execute();
23 } catch (\Exception $e) {
24 // var_dump($e);
25 }
26 }
27 }
28 }
7
github.com/dgrammatiko/jailed-fs/blob/main/src/plugins/system/restrictedfs/script.php
Part III.
Module
We create a module.H This is an add-on that extends the display of the actual content. It is used when
a content is not the main content and is displayed in different positions. Besides, it is possible to select
the menu items under which the module is visible.
In Joomla, there are a variety of modules that I use as a guide. For example:
• Menus (mod_menu)
• Login form (mod_login)
• and many more.
This section explains how you create the basic framework for a simple module. In the first step it only
outputs a text. We will build on this in the further course.
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t30...t31
In this section we will add a module. There are some basic files that are used in the standard module
development pattern. We create these in this part.
39.1.1.1. Module
1
2 MOD_FOO="[PROJECT_NAME]"
3 MOD_FOO_XML_DESCRIPTION="Foo Module"
1
2 MOD_FOO="[PROJECT_NAME]"
3 MOD_FOO_XML_DESCRIPTION="Foo Module"
39.1.1.1.3. modules/mod_foo/ mod_foo.php mod_foo.php is the main entry point into the mod-
ule. The file executes the initialization routines, calls helper routines to collect all the required data,
and calls the template where the module output is displayed.
1
2 <?php
3
4 \defined('_JEXEC') or die;
5
6 use Joomla\CMS\Helper\ModuleHelper;
7
8 require ModuleHelper::getLayoutPath('mod_foo', $params->get('layout', '
default'));
39.1.1.1.4. modules/mod_foo/ mod_foo.xml mod_foo.xml defines the files that are copied by the
installation routine and specifies configuration parameters for the module. You already know this from
the previously created extensions.
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <extension type="module" client="site" method="upgrade">
4 <name>MOD_FOO</name>
5 <creationDate>[DATE]</creationDate>
6 <author>[AUTHOR]</author>
7 <authorEmail>[AUTHOR_EMAIL]</authorEmail>
8 <authorUrl>[AUTHOR_URL]</authorUrl>
9 <copyright>[COPYRIGHT]</copyright>
10 <license>GNU General Public License version 2 or later; see LICENSE
.txt</license>
11 <version>__BUMP_VERSION__</version>
12 <description>MOD_FOO_XML_DESCRIPTION</description>
13
14 <files>
15 <filename module="mod_foo">mod_foo.php</filename>
16 <folder>tmpl</folder>
17 <folder>language</folder>
18 <filename>mod_foo.xml</filename>
19 </files>
20 </extension>
1
2 <?php
3
4 \defined('_JEXEC') or die;
5
6 echo '[PROJECT_NAME]';
Note: In the template file it is possible to use all variables defined in mod_foo.php.
1. install your module in Joomla version 4 to test it. In the beginning, the easiest thing to do is to
copy the files manually in place:
Copy the files in the modules folder to the modules folder of your Joomla 4 installation.
2. install your module as described in part one, after you have copied all files. Open the menu
System | Install | Discover. Here you will see an entry for the module you just copied.
Select it and click on the button Install. 3.
Next, test if your module works properly. Open the menu “Content | Site Modules” and click in the
toolbar New.
4. enter a title in the appropriate field and choose a position. In the tab “Menu Assignment” make
sure that the module is displayed on all pages. At the end click the button Save in the toolbar.
5. That’s it. Switch to the frontend view and make sure that everything is displayed correctly.
We have a solid basis for the further steps in the development of the module.
39.3. Links
Joomla Dokumentation1
1
docs.joomla.org/j4.x:creating_a_simple_module
For impatient people: View the changed program code in the Diff Viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t31...t32
40.1.1.1. Modules
The logic in the module may be complex. Therefore it is good to structure the code clearly. This is done
by jnnhelper files. We create these in the directory Helper.
I named the file FooHelper in general. Good style is to give it a speaking name. Each helper
file has a specific task and it should be named after it. For example, the file that loads the latest
articles is called ArticlesLatestHelper. This way you can see at first sight what is in the file.
modules/mod_foo/ Helper/FooHelper.php
1
2 <?php
3
4 namespace FooNamespace\Module\Foo\Site\Helper;
5
6 \defined('_JEXEC') or die;
7
8 class FooHelper
9 {
10 public static function getText()
11 {
12 return 'FooHelpertest';
13 }
14 }
1 \defined('_JEXEC') or die;
2
3 use Joomla\CMS\Helper\ModuleHelper;
4 +use FooNamespace\Module\Foo\Site\Helper\FooHelper;
5 +
6 +$test = FooHelper::getText();
7
8 require ModuleHelper::getLayoutPath('mod_foo', $params->get('layout',
'default'));
40.1.2.0.2. modules/mod_foo/ mod_foo.xml We enter the namespace in the manifest. This way
it will be registered in Joomla during the installation. We also add the new directory so that it is copied
to the right place during installation.
text $test here. If we want to know more about what is behind $test, we look in the helper.
1 \defined('_JEXEC') or die;
2
3 -echo '[PROJECT_NAME]';
4 +echo '[PROJECT_NAME]' . $test;
Copy the files in the modules folder into the modules folder of your Joomla 4 installation.
Install your module as described in part one, after copying all files. Joomla will update the namespaces
for you during the installation. Since a file and namespaces have been added, this is necessary.
2. Check whether the text calculated via the function FooHelper::getText() is displayed in the
frontend.
40.3. Links
Joomla Dokumentation1
1
docs.joomla.org/j4.x:creating_a_simple_module
41. Parameter
Via Parameter, the Joomla module can be flexibly adapted for end users. Parameters are variables
through which Joomla is set to process certain values. In other words, parameters are influencing
factors set externally to the programme. They are used to tell the module externally which data should
be processed and how.
For impatient people: Look at the changed programme code in the Diff Viewa and copy these
changes into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t32...t33
In this part, only files have been changed. There are no new files.
41.1.3.1. Module
modules/mod_foo/ language/en-GB/en-GB.mod_foo.ini
1 MOD_FOO="[PROJECT_NAME]"
2 MOD_FOO_XML_DESCRIPTION="Foo Module"
3 +MOD_FOO_FIELD_URL_LABEL="URL"
4 +COM_MODULES_FOOPARAMS_FIELDSET_LABEL="Foo Parameter"
modules/mod_foo/ mod_foo.php
1 $test = FooHelper::getText();
2
3 +$url = $params->get('domain');
4 +
5 require ModuleHelper::getLayoutPath('mod_foo', $params->get('layout',
'default'));
41.1.3.1.3. modules/mod_foo/ mod_foo.xml In the manifest we add the new parameter so that it
is editable in the Joomla backend.
modules/mod_foo/ mod_foo.xml
1 <folder>language</folder>
2 <filename>mod_foo.xml</filename>
3 </files>
4 + <config>
5 + <fields name="params">
6 + <fieldset name="fooparams">
7 + <field
8 + name="domain"
9 + type="url"
10 + label="MOD_FOO_FIELD_URL_LABEL"
11 + filter="url"
12 + />
13 + </fieldset>
14 + </fields>
15 + </config>
16 </extension>
Use <fieldset name="basic"> to display the parameters in the first tab that opens immedi-
ately.
In addition to the parameters that a developer inserts into his module, there are standard parameters
An example of the more complex use of a parameter is a digital map where parameters are used
to enable controls such as locate me or a choice of map type.
modules/mod_foo/tmpl/default.php
1 \defined('_JEXEC') or die;
2
3 -echo '[PROJECT_NAME]' . $test;
4 +echo '[PROJECT_NAME]' . $test . '<br />' . $url;
Copy the files in the modules folder into the modules folder of your Joomla 4 installation.
A new installation is not necessary. Continue using the files from the previous part.
3. make sure that the value of the parameter is taken into account in the frontend display.
41.3. Links
Joomla Dokumentation1
1
docs.joomla.org/J4.x:Creating_a_Simple_Module
In this chapter we add an installation script. In the explanations of the component, I described what
you use it for.
For impatient people: Look at the changed program code in the diff viewa and copy these changes
into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t33...t34
In this section we will create a script that will be executed on specific events during the installation.
42.1.1.1. Module
42.1.1.1.1. modules/mod_foo/ script.php Using the example of the script file, I show that many
things are applied in the same way in the case of a module as in the case of a component.
You can use many things in the module in the same way as in the component. For example, the
update server, the changelog, help pages.
The point is to clarify the procedure. That’s why this script file only takes care of setting minimum
requirements and outputting texts. There are no limits to your imagination to extend this file.
modules/mod_foo/script.php
1
2 <?php
3
4 \defined('_JEXEC') or die;
5
6 use Joomla\CMS\Language\Text;
7 use Joomla\CMS\Log\Log;
8
9 class mod_fooInstallerScript
10 {
11
12 public function __construct()
13 {
14 $this->minimumJoomla = '4.0';
15 $this->minimumPhp = JOOMLA_MINIMUM_PHP;
16 }
17
18 function install($parent)
19 {
20 echo Text::_('MOD_FOO_INSTALLERSCRIPT_INSTALL');
21
22 return true;
23 }
24
25 function uninstall($parent)
26 {
27 echo Text::_('MOD_FOO_INSTALLERSCRIPT_UNINSTALL');
28
29 return true;
30 }
31
32 function update($parent)
33 {
34 echo Text::_('MOD_FOO_INSTALLERSCRIPT_UPDATE');
35
36 return true;
37 }
38
39 function preflight($type, $parent)
40 {
41 // Check for the minimum PHP version before continuing
42 if (!empty($this->minimumPhp) && version_compare(PHP_VERSION,
$this->minimumPhp, '<')) {
43 Log::add(Text::sprintf('JLIB_INSTALLER_MINIMUM_PHP', $this
->minimumPhp), Log::WARNING, 'jerror');
44
45 return false;
46 }
47
48 // Check for the minimum Joomla version before continuing
49 if (!empty($this->minimumJoomla) && version_compare(JVERSION,
$this->minimumJoomla, '<')) {
50 Log::add(Text::sprintf('JLIB_INSTALLER_MINIMUM_JOOMLA',
$this->minimumJoomla), Log::WARNING, 'jerror');
51
52 return false;
53 }
54
55 echo Text::_('MOD_FOO_INSTALLERSCRIPT_PREFLIGHT');
56
57 return true;
58 }
59
60 function postflight($type, $parent)
61 {
62 echo Text::_('MOD_FOO_INSTALLERSCRIPT_POSTFLIGHT');
63
64 return true;
65 }
66 }
42.1.2.1. Module
src/modules/mod_foo/language/en-GB/en-GB.mod_foo.sys.ini
1 MOD_FOO="[PROJECT_NAME]"
2 MOD_FOO_XML_DESCRIPTION="Foo Module"
3 + MOD_FOO_INSTALLERSCRIPT_PREFLIGHT="<p>Anything here happens before
the + installation/update/uninstallation of the module</p>"
4 + MOD_FOO_INSTALLERSCRIPT_UPDATE="<p>The module has been updated</p>"
5 + MOD_FOO_INSTALLERSCRIPT_UNINSTALL="<p>The module has been uninstalled
</p>"
6 + MOD_FOO_INSTALLERSCRIPT_INSTALL="<p>The module has been installed</p>
"
7 + MOD_FOO_INSTALLERSCRIPT_POSTFLIGHT="<p>Anything here happens after
the installation/update/uninstallation of the module</p>"
42.1.2.1.2. modules/mod_foo/ mod_foo.xml Finally, we enter the name of the script file in the
manifest so that the installation routine will copy it to the right place and call it.
modules/mod_foo/ mod_foo.xml
5 <namespace>FooNamespace\Module\Foo</namespace>
6 <files>
7 <filename module="mod_foo">mod_foo.php</filename
1. Create a new Installation. Uninstall your previous installation and copy all files again.
Copy the files in the modules folder into the modules folder of your Joomla 4 installation.
Install your module as described in part one, after you have copied all the files. Joomla will execute
the script file for you during the installation. Convince yourself of this by checking the output of the
language strings.
42.3. Links
Joomla Dokumentation1
1
docs.joomla.org/j4.x:creating_a_simple_module
Part IV.
Template
Why should you create your own Joomla template? There are a few good reasons why we should make
this happen!
Especially for extension developers, I think it is essential to know how a Joomla template works.
This makes it possible to integrate the separation of logic and design into the extension.
• Creating our own Joomla template means that we have complete control over every last detail
of the look and feel of the website. We only create code that we like. It is much easier to change
a custom template than a complex Joomla template, where often the different elements are
attached to each other.
• Creating our own template means that we don’t overload the website with functions that we
don’t even use.
• If we want a custom Joomla template that is not used by thousands of other websites, creating
one is an option.
• If you have never created a Joomla template before, you will learn a lot about Joomla while
developing it. You will end up knowing a lot about the interaction of the different elements and
feel more confident.
This is not about learning HTML and CSS. That’s why I will use a ready-made HTML5 template
in this article. Follow my example and you will be able to create a complete Joomla template
yourself in the end. You develop HTML and CSS yourself or use a template like I did here.
A template is responsible for the design of the website. There are two types of templates in Joomla:
• Front-End-Templates and
• back-end templates.
We create a front-end template. This controls the way the website is presented to the user.
The principle for creating a template for the administration area is exactly the same. You create it
in the subdirectory /administrator/templates. You create the front-end template in the folder
/templates.
For impatient people: Take a look at the changed programme code in the Diff-Ansichta and copy
these changes into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t34...t35
With the template it is also so that you do not reinvent the wheel. You can use many things that Joomla
provides by default. This has advantages. The disadvantage is that individual wishes are more difficult
to implement, or rather Joomla knowledge is a necessary prerequisite. Therefore we start rudimentary.
The main thing is to look behind the functions and understand them.
43.1.1.1. Template
This part will guide you through the necessary steps to create a Joomla template - from scratch.
To further explain: As already mentioned, a component is responsible for the display of the main
content. The entire layout, for example the modules in a sidebar and the navigation are accessories.
The file component.php sets the focus on the main content.
Would you like to see the output of the file component.php? This view is displayed in the
browser if you append tmpl=component to the URL - for example like this: /index.php?tmpl
=component.
We create the file component.php here for the sake of completeness and add the text Component.
This way it is possible to test it later.
templates/facile/component.php
1
2 Component
In the Joomla 4 standard template Cassiopeia the file component.php is also implemented. You
can use it as a guide. It loads all essential content to display the area marked in the image below
independently.
43.1.1.1.2. templates/facile/error.php When website visitors call a page that does not exist, they
receive an error message. Joomla’s error message is generic. It is much better to create your own
individual error page.
In my opinion, a good error page includes:
• Minimalist design: express yourself with simple texts and clear images. Write only what is
necessary. Less is more!
• Link to the homepage: Describe clearly how to reach the homepage and put a link to it. An
additional link, for example in the logo, is helpful. But it should not be the only way to get back
to the homepage.
• A search: Offer the visitor a search field. He will know what he wants to see. A search field is used
because it offers an option to find it. Besides, it keeps him on your website.
• No technical terms: 404 Error is completely meaningless to many people.
In my opinion, the error page should not blame visitors. After all, it’s not their fault if a page
doesn’t exist or an internal server error occurs.
To let you know how and where to implement your error page, I created the file templates/facile/
error.php. This contains nothing more than the word Error. So it is possible to test the page. Let
your imagination run free with the content and design of your own individual error page.
templates/facile/error.php
1
2 Error
In the Joomla 4 standard template Cassiopeia the file error.php is also implemented. You can use it
as a guide. It loads all essential content to display the information in the following screen. In case of an
invalid URL a user will see the following view.
43.1.1.1.3. templates/facile/index.php The file index.php is the heart. It ensures that everything
works together. The following code snippet shows you a minimal structure.
templates/facile/index.php
1
2 <?php
3
4 \defined('_JEXEC') or die;
5 ?>
6
7 <!DOCTYPE html>
8 <html lang="de">
9 <head>
10 <meta charset="utf-8">
11 <meta name="viewport" content="width=device-width, initial-scale
=1.0">
12 <title>Titel</title>
13 </head>
14 <body>
15 Hallo Joomla!
16 </body>
17 </html>
The first line \defined('_JEXEC')or die; is written in PHP. The good thing about PHP and HTML
is that it can be written together in one file. We can put PHP statements into an HMTL file, and vice
versa. <?php opens a PHP statement - anywhere - and ?> closes it. With the line \defined('_JEXEC
')or die; we forbid direct access to this file. This is done through the Joomla API with the _JEXEC
command. This statement ensures that the file is accessed from within a Joomla session. If not, the
processing aborts ... or die;. This is how Joomla makes it harder for a hacker to inject malicious
code.
Then we declare the document type1 with <!doctype html>. This ensures that the document is
parsed the same way by different browsers. HTML5 is the simplest and most reliable doctype declara-
tion. This is what we use.
Note that the doctype is simply !DOCTYPE html and not !DOCTYPE html5.
What follows is the smallest possible structure of an HTML page. This page opens with <html> and
ends with </html>. The header starts with <head> and ends with </head>. The body starts with
<body> and ends with </body>.
Enough explanation. This is how the website looks minimally. It does not yet load any content from
Joomla My main point here was to show that the index.php of the active template is responsible for
everything. In our case, this is the file templates/facile/index.php. So far, the responsibility is
limited. Only the (German) greeting Hallo Joomla is displayed on the screen.
templates/facile/language/en-GB/en-GB.tpl_facile.ini
1
2 TPL_FACILE_XML_DESCRIPTION="Facile is a Joomla 4 template."
templates/facile/language/en-GB/en-GB.tpl_facile.sys.ini
1
2 FACILE="Facile - Site template"
To keep the website technically up to date or to integrate new features, it will be revised from
time to time. Mostly these are updates. During the update, display problems may occur. So that
visitors are not irritated by an error message, Joomla has a maintenance mode. If this is active a
special maintenance mode page is shown to the visitor, the offline.php.
The following minimalist code will display a registration form. You could display a short text instead.
The login form allows an administrator to authenticate and then test the site via frontend.
templates/facile/offline.php
1
2 <?php
3
4 defined('_JEXEC') or die;
5
6 use Joomla\CMS\Helper\AuthenticationHelper;
7 use Joomla\CMS\HTML\HTMLHelper;
8 use Joomla\CMS\Language\Text;
9 use Joomla\CMS\Router\Route;
10 use Joomla\CMS\Uri\Uri;
11
12 $twofactormethods = AuthenticationHelper::getTwoFactorMethods();
13 ?>
14
15 <!DOCTYPE html>
16 <html lang="<?php echo $this->language; ?>">
17 <head>
18 <meta name="viewport" content="width=device-width, initial-scale
=1.0">
19 <jdoc:include type="head" />
20 </head>
21 <body>
22 <jdoc:include type="message" />
23 <form action="<?php echo Route::_('index.php', true); ?>" method="
post" id="form-login">
24 <fieldset>
25 <label for="username"><?php echo Text::_('JGLOBAL_USERNAME'
); ?></label>
26 <input name="username" id="username" type="text">
27
28 <label for="password"><?php echo Text::_('JGLOBAL_PASSWORD'
); ?></label>
29 <input name="password" id="password" type="password">
30
31 <?php if (count($twofactormethods) > 1) : ?>
32 <label for="secretkey"><?php echo Text::_('
JGLOBAL_SECRETKEY'); ?></label>
33 <input name="secretkey" autocomplete="one-time-code" id="
secretkey" type="text">
34 <?php endif; ?>
35
36 <input type="submit" name="Submit" value="<?php echo Text::
_('JLOGIN'); ?>">
37
38 <input type="hidden" name="option" value="com_users">
39 <input type="hidden" name="task" value="user.login">
40 <input type="hidden" name="return" value="<?php echo
base64_encode(Uri::base()); ?>">
41 <?php echo HTMLHelper::_('form.token'); ?>
42 </fieldset>
43 </form>
44 </body>
45 </html>
With the login option you have now integrated the necessary function. You surely want to make your
maintenance page more attractive. There is a lot of inspiration available in the Internet. The easiest
way is to orientate yourself on the following example from the standard template Cassiopeia:
In the file templateDetails.xml the module positions are usually created and included into the
website via the command jdoc:include in the index.php. We will do this in a later part. Optionally
we can create parameters to make the template customizable via backend. In the further course of
this text I have included logoFile, siteTitle and siteDescription as parameters. But first, let’s
look at a minimal version of templateDetails.xml in the following code snippet.
src/templates/facile/templateDetails.xml
1
2 <?xml version="1.0" encoding="utf-8"?>
3 <extension type="template" client="site" method="upgrade">
4 <name>facile</name>
5 <creationDate>[DATE]</creationDate>
6 <author>[AUTHOR]</author>
7 <authorEmail>[AUTHOR_EMAIL]</authorEmail>
8 <authorUrl>[AUTHOR_URL]</authorUrl>
9 <copyright>[COPYRIGHT]</copyright>
10 <license>GNU General Public License version 2 or later;</license>
11 <version>__BUMP_VERSION__</version>
12 <description>TPL_FACILE_XML_DESCRIPTION</description>
13
14 <files>
15 <filename>component.php</filename>
16 <filename>error.php</filename>
17 <filename>index.php</filename>
18 <filename>offline.php</filename>
19 <filename>templateDetails.xml</filename>
20 <filename>template_preview.png</filename>
21 <filename>template_thumbnail.png</filename>
22 <folder>language</folder>
23 </files>
24 </extension>
What does this code mean exactly? XML documents should start with an XML declaration2 , but they
don’t have to. We create the declaration and specify XML version and charset (utf-8) here <?xml
version="1.0"encoding="utf-8"?>.
The other part of templateDetails.xml contains information for the installation. The type is
template in case of a template. The method="upgrade" allows to install the template at a later
time over a previous version.
What is important about method="upgrade": It installs newer versions of the files. Old files
that are no longer needed, however, remain. So they are not deleted. If you want to specifically
ensure that your extension does not contain unnecessary files for users, this have to be explicitly
implemented in an installation script.
• Template name,
• creation date,
• author, copyright,
• e-mail address, website,
• version and
• Description)
These will be displayed later in the template manager of the Joomla backend.
After that the installation routine is listed. Folders (<folder>) and files (<filename>) belonging to
the template. The HTML tag <positions> comes afterwards. We will add this later. Each position is
written in a separate line and is now ready to be included in the index.php and is thus selectable via
the module manager in the Joomla backend.
2
en.wikipedia.org/wiki/xhtml#xml_declaration
For more information on the templateDetails.xml file, see the Joomla documentation
docs.joomls.orga .
a
en.wikipedia.org/wiki/xhtml#xml_declaration
1. install your template in Joomla version 4 to test it. In the beginning, the easiest thing to do is to
copy the files manually in place:
Copy the files from the templates folder into the templates folder of your Joomla 4 installation.
2. install your template as described in part one, after you have copied all files. Open the menu
System | Install | Discover. Here you will see an entry for the template you just copied.
Select it and click on the button “Install”.
Next, test whether the template works without errors. Activate the Template Style Facile.
6. test the simple error page. To do this, enter a URL in the address field of the browser that does
not exist. For example call the URL /indexabcxyz.php. You should see the text Error.
tmpl=component in the address bar of the browser. You should see the text Component.
43.3. Links
3
github.com/c-lodder/lightning
4
github.com/dgrammatiko/sloth-pkg
5
html5up.net
The template should dynamically display the Joomla content from components, modules and plugins
at different positions. How this goal is achieved in Joomla is the topic of this chapter. So: How are
module positions integrated in the Joomla template.
For impatient people: Take a look at the changed programme code in the Diff-Ansichta and copy
these changes into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t35...t36
We will proceed step by step. In this part we add the module positions so that Joomla displays content
dynamically. We will take care of the design in the next part.
In this chapter only files have been changed and no new ones have been added.
44.1.2.1. Template
So far we have more or less a static website. In this part, we add content dynamically using module
positions.
templates/facile/component.php
1 -Component
2 +<!DOCTYPE html>
3 +<html lang="de">
4 +<head>
5 + <meta name="viewport" content="width=device-width, initial-scale=1"
>
6 + <jdoc:include type="head" />
7 +</head>
8 +<body>
9 + <jdoc:include type="message" />
10 + <jdoc:include type="component" />
11 +</body>
12 +</html>
13 +
The main new entry is <jdoc:include type="component"/>. The command inserts the main
content of the current page.
Are you wondering what system and error messages are exactly? In Joomla they are generated
by $this->setMessage(Text::sprintf('MESSAGE_TEXT', $id), 'error'); in an ob-
ject of type BaseControllera . In the file administrator/components/com_content/src/
Controller/DisplayController.php you will find an exampleb . This outputs the text You
are not permitted to use that link to directly access that page. in the fron-
tend. This is exactly where <jdoc:include type="message"/> is inserted in the template.
a
libraries/src/MVC/Controller/BaseController.php#L1066
b
administrator/components/com_content/src/controller/displaycontroller.php#l55
<jdoc:include type="head"/> loads content that requires extensions and includes them via
special commands. These are mainly scripts and styles.
44.1.2.1.2. templates/facile/index.php You already know it: The file index.php is the heart of the
template. It makes sure that everything works together. In the previous chapter we had not integrated
Joomla’s own content. I will make up for this here. A minimal structure, which inserts the Joomla
content, looks like this.
templates/facile/index.php
Inside the header area Joomla templates load header information with <jdoc: include type
="head"/> via Joomla API. We already use this above in the component.php file. The jdoc:
include command inserts the necessary header information. This way you are on the safe side.
I don‘t use this command in the index.php at the moment, because I want to show, that you can
also choose yourself, what you need.
We can find the jdoc:include command in other places in index.php. For example, we see <jdoc
:include type="message"/>, so the system messages work. Whenever Joomla has something
to tell the website visitor, this line will display it on the screen. For example, when sending an email
through a contact form, you will see the message “Your message was sent successfully”.
The last element worth mentioning is <jdoc:include type="modules"/>. As the name suggests,
this is used to include modules.
So, enough explained. All contents are integrated via module Positions. They are not displayed nicely
so far. Don’t be scared if you open this version in your browser later. You will see all content in unstyled
form at the moment.
It may be important to you that a module position is only inserted if a module is published under
it, because this makes it easier to prevent the unnecessary setting of HTML elements for a wrapper.
How to achieve this is a topic of the next chapter. Or you want to make it optional in the template
which module position is used. For example, it is important to you that a sidebar can be completely
deactivated.You can achieve this with the help of parameters, which are the subject of the next
but one chapter.
templates/facile/language/en-GB/en-GB.tpl_facile.sys.ini
1
2 FACILE="Facile - Site template"
3 +TPL_FACILE_POSITION_MENU="Menu"
4 +TPL_FACILE_POSITION_SEARCH="Search"
5 +TPL_FACILE_POSITION_BANNER="Banner"
6 +TPL_FACILE_POSITION_TOP-A="Area under banner"
7 +TPL_FACILE_POSITION_TOP-B="Area above the content"
8 +TPL_FACILE_POSITION_MAIN-TOP="Main-top"
9 +TPL_FACILE_POSITION_BREADCRUMBS="Breadcrumbs"
10 +TPL_FACILE_POSITION_MAIN-BOTTOM="Main-bottom"
11 +TPL_FACILE_POSITION_SIDEBAR-LEFT="Sidebar-left"
12 +TPL_FACILE_POSITION_SIDEBAR-RIGHT="Sidebar-right"
13 +TPL_FACILE_POSITION_BOTTOM-A="Bottom-a"
14 +TPL_FACILE_POSITION_BOTTOM-B="Bottom-b"
15 +TPL_FACILE_POSITION_FOOTER="Footer"
16 +TPL_FACILE_POSITION_DEBUG="Debug"
17 +TPL_FACILE_POSITION_TOPBAR="Top Bar"
18 +TPL_FACILE_POSITION_BELOW-TOP="Below Top"
src/templates/facile/templateDetails.xml
1
2 <filename>template_thumbnail.png</filename>
3 <folder>language</folder>
4 </files>
5 +
6 + <positions>
7 + <position>topbar</position>
8 + <position>below-top</position>
9 + <position>menu</position>
10 + <position>search</position>
11 + <position>banner</position>
12 + <position>top-a</position>
13 + <position>top-b</position>
14 + <position>main-top</position>
15 + <position>main-bottom</position>
16 + <position>breadcrumbs</position>
17 + <position>sidebar-left</position>
18 + <position>sidebar-right</position>
19 + <position>bottom-a</position>
20 + <position>bottom-b</position>
21 + <position>footer</position>
22 + <position>debug</position>
23 + </positions>
24 </extension>
Copy the files in the templates folder to the templates folder of your Joomla 4 installation.
A new installation is not necessary. Continue using the ones from the previous part.
3. install the sample data, so that you have the same prerequisites as I have.
4. test now, if the sample files are displayed correctly. Activate the template style Cassiopei and
call the URL joomla-cms4/index.php. How you change a template style, I had shown in the
previous chapter with a picture. Your view should be like in the following picture.
5. next test if our template Facile works without errors. Activate the Template Style Facile and call
the URL joomla-cms4/index.php again. Your view should be like in the following picture.
You can view the module positions in the frontend. Activate the view in the global configuration in the
backend and call the URL joomla-cms4/index.php?tp=1. The appendage ?tp=1 is crucial.
This does not look inviting. I agree with you there. So next we pep up the template with CSS and
JavaScipt and adjust the default views of Joomla.
45. Overrides
In this chapter we will change the output of the extensions in the frontend. In Joomla this is done
using
• overrides,
• alternative overrides,
• layouts and
• module chromes.
The standard output of each Joomla extension can be manipulated via files in the template’s html
folder. Joomla offers different options for this purpose. Overrides, alternative overrides, layouts and
module chromes. Each variant has its purpose.
Overrides are the first choice. If there is already an override for an extension, you create an alternative
override. Layouts override a small area of a view and can be reused in different views. Last but not
least, module chromes offer a variant to use an override in different places slightly modified.
For impatient people: Take a look at the changed programme code in the Diff-Ansichta and copy
these changes into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t36...t37
My Goal: In the end, we will have discussed all override variations so that you can finish the template
or edit your own template according to your needs. Finished will be the home page view of the Joomla
4 blog sample files.
Home page view of the Joomla 4 blog sample in our new template Facile:
Overrides can be created comfortably with the help of the template manager. This offers a view that
highlights the differences to Joomla’s own code.
Tip: If you want to change a view only slightly, it is a good idea to take the original view as
a template. Then you can change it. To do this, create a copy of the existing view in the
html directory of the template and edit it. The copy is placed in the template directory,
exactly as the file templates/TEMPLATE_NAME/html/EXTENSION_NAME/VIEW_NAME/
FILE_NAME.php. For example, if you want to change the feature view of com_content,
then copy the file components/com_content/views/feature/tmpl/default.php
to templates/TEMPLATE_NAME/html/com_content/feature/default.php. Simi-
larly, if you want to change the appearance of the mod_article_latest module. Copy
modules/tmpl/mod_articles_news/default.php to templates/TEMPLATE_NAME/
html/mod_articles_news/default.php. Joomla 4 includes the standard frontend template
Cassiopeia. Cassipeia uses template overrides to create the dropdown menu. You can use this as
an example. Open the directory \template\cassiopeia. In the template folder, you will find a
subdirectory called html.
I took the design from the HTML5 UP template TXT1 . This tutorial is about Joomla. Explanations
about HTML, SCSS and CSS would go beyond the scope of this post. Therefore I leave them out and
concentrate on Joomla.
templates/facile/html/com_content/featured/default.php
1
2 <?php
3 defined('_JEXEC') or die;
1
html5up.net/txt
4 ?>
5
6 <div>
7 <h1>
8 <?php echo $this->escape($this->params->get('page_heading'));
?>
9 </h1>
10
11 <?php if (!empty($this->items)) : ?>
12 <?php foreach ($this->items as $key => &$item) : ?>
13 <div>
14 <?php
15 $this->item = & $item;
16 echo $this->loadTemplate('item');
17 ?>
18 </div>
19 <?php endforeach; ?>
20 <?php endif; ?>
21 </div>
templates/facile/html/com_content/featured/default_item.php
1
2 <?php
3
4 defined('_JEXEC') or die;
5
6 use Joomla\CMS\Router\Route;
7 use Joomla\Component\Content\Site\Helper\RouteHelper;
8 use Joomla\CMS\Layout\LayoutHelper;
9 ?>
10
11 <?php echo LayoutHelper::render('joomla.content.intro_image', $this->
item); ?>
12
13 <div>
14 <h2>
15 <a href="<?php echo Route::_(RouteHelper::getArticleRoute($this
->item->slug, $this->item->catid, $this->item->language));
?>">
16 <?php echo $this->escape($this->item->title); ?>
17 </a>
18 </h2>
19
20 <?php echo $this->item->introtext; ?>
21 </div>
Joomla first searches for files in the template directory. Therefore we create our own layout later.
We will save it under templates/facile/html/layouts/joomla/content/intro_image.php.
Our own layout shows the image in the correct size. Since the file layouts/joomla/content/
intro_image.php exists directly in the Joomla root directory, it would otherwise be used for the
display. If we had no special requirements, we could make it easy and use this Joomla own layout
layouts/joomla/content/intro_image.php.
templates/facile/html/layouts/joomla/content/intro_image.php
In addition to the override of entire views, Joomla supports the override of smaller code segments,
so-called layouts. Layouts are used by Joomla in various places. For example, to generate the
code that creates the search and sort filters in list views or when displaying post information (such
as author, creation date. . . ) above or below a post.
Because our template is built differently and expects different CSS elements, the display of the image via
joomla.content.intro_image is not optimal. Therefore we overwrite the layout in our template.
Because we want to reuse this, we do it in a way that we can also access our layout in other places via
echo LayoutHelper::render('joomla.content.intro_image', $this->item);. For this
we create the file templates/facile/html/layouts/joomla/content/intro_image.php.
1
2 <?php
3 defined('_JEXEC') or die;
4
5 use Joomla\CMS\HTML\HTMLHelper;
6 use Joomla\Component\Content\Site\Helper\RouteHelper;
7 use Joomla\CMS\Router\Route;
8
9 $images = json_decode($displayData->images);
10 $img = HTMLHelper::cleanImageURL($images->image_intro);
11 $alt = empty($images->image_intro_alt) && empty($images->
image_intro_alt_empty) ? '' : 'alt="'. htmlspecialchars($images->
image_intro_alt, ENT_COMPAT, 'UTF-8') .'"';
12 ?>
13
14 <a href="<?php echo Route::_(RouteHelper::getArticleRoute($displayData
->slug, $displayData->catid, $displayData->language)); ?>" class="
image featured">
15 <img src="<?php echo htmlspecialchars($img->url, ENT_COMPAT, 'UTF-8');
?>" alt="<?php echo $alt; ?>" />
16 </a>
Again for comparison: The original Joomla-own file of the layout joomla.content.
intro_image is located in the directory layouts/ joomla/content/intro_image.php.
The special file for our template is saved under templates/facile/html/ + layouts/joomla
/content/intro_image.php.
45.1.1.1.2. Override via Module Chrome mod_articles_news At the top of the home page,
the Joomla Blog sample data displays the module mod_articles_news. We create a standard
override analogous to the view of the main articles in com_content/featured/, in which we in-
clude the items in a subtemplate. The code of the two files mod_articles_news/_item.php and
mod_articles_news/default.php can be found below. These only support the necessary func-
tions and are therefore clearly compact for learning.
templates/facile/html/mod_articles_news/_item.php
1
2 <?php
3
4 defined('_JEXEC') or die;
5
6 use Joomla\CMS\Layout\LayoutHelper;
7 ?>
8
9 <div class="col-4 col-12-medium col-12-small">
10 <section class="box feature">
11 <a href="<?php echo $item->link; ?>" class="image featured"><
img src="<?php echo $item->imageSrc; ?>" alt="<?php echo
$item->imageAlt; ?>"/></a>
12
13 <h3><a href="<?php echo $item->link; ?>"><?php echo $item->
title; ?></a></h3>
14
15 <p>
templates/facile/html/mod_articles_news/default.php
1
2 <?php
3
4 defined('_JEXEC') or die;
5
6 use Joomla\CMS\Helper\ModuleHelper;
7
8 if (empty($list)) {
9 return;
10 }
11
12 ?>
13 <div>
14 <div class="row">
15 <!-- Feature -->
16 <?php foreach ($list as $item) : ?>
17 <?php require ModuleHelper::getLayoutPath('
mod_articles_news', '_item'); ?>
18 <?php endforeach; ?>
19 </div>
20 </div>
The override to the module “mod_articles_news” should be displayed in the upper area with a large
headline. On a subpage, it should appear with small headline in the sidebar. We could create a solution
with an alternative override. This variant is the subject of the next section. However, a lot of program
code would be written via alternativen Override redundantly. Actually, only the first line with the
heading is different. And here Joomlas module Chromes comes into play. We create a file in the
directory templates/facile/html/layouts/chromes/ which only contains the different code
and otherwise embeds the module exactly as it is. The latter is taken care of by echo $module->
content;. We can name the modules chrome file anything we want. I have chosen hr.php as name.
In the index.php at the end of this section you can see how to make sure that the hr.php file is
integrated in the header of the page but not in the sidebar.
templates/facile/html/layouts/chromes/hr.php
1
2 <?php
3 defined('_JEXEC') or die;
4 $module = $displayData['module'];
5 ?>
6
7 <section class="box features">
8 <h2 class="major"><span>News</span></h2>
9 <?php echo $module->content; ?>
10 </section>
There are requirements where the design of a module varies greatly in different places. In this case it
is necessary to create two different files. The file default.php is actually the override. If we create
another file in the directory next to default.php, this is an alternative override. A use case is a menu.
In the header, the main menu often looks quite different from the one in the footer. In our template the
main menu is implemented in the file default.php and the footer menu in the file bottom.php.
Note: The two files differ slightly. In the bottom.php file, the <ul> element must be given the
class menu so that no list item symbols are displayed in the frontend view. This could also be
handled via a Chrome module.
templates/facile/html/mod_menu/default.php
1
2 <?php
3 defined('_JEXEC') or die;
4
5 use Joomla\CMS\Helper\ModuleHelper;
6 ?>
7
8 <ul>
9 <?php foreach ($list as $i => &$item) {
10 $itemParams = $item->getParams();
11 $class = '';
12
13 if ($item->id == $active_id) {
14 $class .= ' current';
15 }
16
17 echo '<li class="' . $class . '">';
18
19 require ModuleHelper::getLayoutPath('mod_menu', 'default_url');
20
21 // The next item is deeper.
22 if ($item->deeper) {
23 echo '<ul>';
24 }
25 // The next item is shallower.
26 else if ($item->shallower) {
27 echo '</li>';
28 echo str_repeat('</ul></li>', $item->level_diff);
29 }
30 // The next item is on the same level.
31 else {
32 echo '</li>';
33 }
34 }
35 ?></ul>
templates/facile/html/mod_menu/bottom.php
1
2 <?php
3 defined('_JEXEC') or die;
4
5 use Joomla\CMS\Helper\ModuleHelper;
6 ?>
7
8 <ul class="menu">
9 <?php foreach ($list as $i => &$item) {
10 $itemParams = $item->getParams();
11 $class = '';
12
13 if ($item->id == $active_id) {
14 $class .= ' current';
15 }
16
17 echo '<li class="' . $class . '">';
18
19 require ModuleHelper::getLayoutPath('mod_menu', 'default_url');
20
21 // The next item is deeper.
22 if ($item->deeper) {
23 echo '<ul>';
24 }
25 // The next item is shallower.
26 else if ($item->shallower) {
27 echo '</li>';
28 echo str_repeat('</ul></li>', $item->level_diff);
29 }
30 // The next item is on the same level.
31 else {
32 echo '</li>';
33 }
34 }
35 ?></ul>
templates/facile/index.php
1 <!DOCTYPE html>
2 <html lang="de">
3
4 <head>
5 - <meta charset="utf-8">
6 - <meta name="viewport" content="width=device-width, initial-scale
=1.0">
7 - <title>Titel</title>
8 + <meta charset="utf-8">
9 + <meta name="viewport" content="width=device-width, initial-scale
=1.0">
10 + <link rel="stylesheet" href="<?php echo $templatePath; ?>/assets/
css/main.css" />
11 + <title>Titel</title>
12 </head>
13
14 -<body>
15 - <header>
16 - <div>
17 - <nav>
18 - <div>
19 - <jdoc:include type="modules" name="menu" />
20 - </div>
21 - </nav>
22 - <div>
23 - <jdoc:include type="modules" name="search" />
24 - </div>
25 - </div>
26 - </header>
27 -
28 - <div>
29 - <jdoc:include type="modules" name="banner" />
30 - </div>
31 -
32 - <div>
33 - <jdoc:include type="modules" name="top-a" />
34 - </div>
35 -
36 - <div>
37 - <jdoc:include type="modules" name="top-b" />
38 - </div>
39 -
40 - <div>
41 - <jdoc:include type="modules" name="sidebar-left" />
42 - </div>
43 -
44 - <div>
45 - <jdoc:include type="modules" name="breadcrumbs" />
46 - <jdoc:include type="modules" name="main-top" />
47 - <jdoc:include type="message" />
48 - <main>
49 - <jdoc:include type="component" />
50 - </main>
51 - <jdoc:include type="modules" name="main-bottom" />
52 - </div>
53 -
54 - <div>
55 - <jdoc:include type="modules" name="sidebar-right" />
56 - </div>
57 -
58 - <div>
59 - <jdoc:include type="modules" name="bottom-a" />
60 - </div>
61 -
62 - <div>
63 - <jdoc:include type="modules" name="bottom-b" />
64 - </div>
65 -
66 - <footer>
67 - <jdoc:include type="modules" name="footer" />
68 - </footer>
69 -
70 - <jdoc:include type="modules" name="debug" />
71 -
72 +<body class="homepage is-preload">
73 + <div id="page-wrapper">
74 +
75 + <?php if ($this->countModules('menu', true)) : ?>
76 + <nav id="nav">
77 + <jdoc:include type="modules" name="menu" />
78 + </nav>
79 + <?php endif; ?>
80 +
81 + <section id="main">
82 + <div class="container">
83 + <div class="row gtr-200">
84 + <div class="row">
85 +
86 + <?php if ($this->countModules('top-a', true)) :
?>
87 + <jdoc:include type="modules" name="top-a" style
="hr" />
88 + <?php endif; ?>
89 +
90 + <?php if ($this->countModules('sidebar-left',
true)) : ?>
91 + <div class="col-3 col-12-medium">
92 + <div class="sidebar">
93 + <jdoc:include type="modules" name="
sidebar-left" style="none" />
94 + </div>
95 + </div>
96 + <?php endif; ?>
97 +
98 + <div class="col-6 col-12-medium imp-medium">
99 + <div class="content">
100 +
101 + <?php if ($this->countModules('search',
true)) : ?>
102 + <section id="search">
103 + <jdoc:include type="modules" name="
breadcrumbs" style="none" />
104 + </section>
105 + <?php endif; ?>
106 +
107 + <?php if ($this->countModules('search',
true)) : ?>
108 + <section id="search">
109 + <jdoc:include type="modules" name="
search" style="none" />
110 + </section>
111 + <?php endif; ?>
112 +
113 + <jdoc:include type="modules" name="main
-top" style="none" />
114 + <jdoc:include type="message" />
115 + <main>
116 + <jdoc:include type="component" />
117 + </main>
118 +
119 + <jdoc:include type="modules" name="main
-bottom" style="none" />
120 +
121 + </div>
122 + </div>
123 +
124 + <?php if ($this->countModules('sidebar-right',
true)) : ?>
125 + <div class="col-3 col-12-medium">
126 + <div class="sidebar">
127 + <jdoc:include type="modules" name="
sidebar-right" style="none" />
128 + </div>
129 + </div>
130 + <?php endif; ?>
131 +
132 + <?php if ($this->countModules('bottom-a', true)
) : ?>
133 + <jdoc:include type="modules" name="bottom-a"
style="none" />
134 + <?php endif; ?>
135 + </div>
136 + </div>
137 + </div>
138 + </section>
139 +
140 + <footer id="footer">
141 + <?php if ($this->countModules('footer', true)) : ?>
142 + <div id="copyright">
143 + <jdoc:include type="modules" name="footer" />
144 + </div>
145 + <?php endif; ?>
146 + </footer>
147 +
148 + <jdoc:include type="modules" name="debug" />
149 +
150 + <script src="<?php echo $templatePath; ?>/assets/js/jquery.min.
js"></script>
151 + <script src="<?php echo $templatePath; ?>/assets/js/jquery.
dropotron.min.js"></script>
152 + <script src="<?php echo $templatePath; ?>/assets/js/jquery.
scrolly.min.js"></script>
153 + <script src="<?php echo $templatePath; ?>/assets/js/browser.min
.js"></script>
154 + <script src="<?php echo $templatePath; ?>/assets/js/breakpoints
.min.js"></script>
155 + <script src="<?php echo $templatePath; ?>/assets/js/util.js"></
script>
156 + <script src="<?php echo $templatePath; ?>/assets/js/main.js"></
script>
157 +
158 + </div>
159 </body>
160
161 </html>
Tip: To avoid adding elements unnecessarily it is good practice to check if a module position is used
in the Joomla installation. This is done with $this->countModules('NAME_DER_POSITIONS
', true).
I deleted the banner module, because I want to add a banner later using parameters.
Copy the files in the templates folder to the templates folder of your Joomla 4 installation.
A new installation is not necessary. Continue using the ones from the previous part.
We have installed the sample data in the previous chapter. If you have not done so, please do it now so
that the modules shown in the next image are available on the homepage of the Joomla installation.
2. open the module Bottom Menu and choose as layout bottom. For the module Main Menu
Blog replace the layout Dropdown with the default layout Default from Module.
3. open the module mod_articles_news (Articles - Newsflash) with the name Latest
Posts, which is shown in the header of the frontend. In the explanations of index.php you have
learned that a module Chrome is activated via the parameter style="hr" in <jdoc:include
type="modules"name="top-a"style="hr"/>. But you can also set this in the backend. The
next picture shows you how to do this in the Advanced tab via the Module Style parameter.
4. Play with the different possibilities. Create different types of overrides and test the output in the
frontend.
Figure 45.8.: Different options in Joomla Template Overrides creation | When creating an article
When creating a menu item, you only have the overrides to choose from, for which you have created
an XML file.
Figure 45.9.: Different possibilities with the Joomla Template Overrides creation | When creating a
menu item
Attention: You will not get an error message if you create an XML file but the related PHP file is miss-
ing due to a typo. There is also no hint if you create two XML files with the same title. Joomla
pretends in this case that there is only one of them. In the next screen, the file names are all cor-
rect. However, the title “COM_CONTENT_ARTICLE_VIEW_DEFAULT_TITLE” already exists. When I
created an article, the override was only offered for selection after I changed the language string to
COM_CONTENT_ARTICLE_VIEW_DEFAULT_MEINSPRECHENDERNAME_TITLE.
Parameters make the template flexibly configurable in the backend. Perhaps a colour selection should
be possible? The standard template Cassiopeia offers, among others, logoFile, siteTitle and
siteDescription as parameters. We add a banner and social media icons.
For impatient people: Take a look at the changed programme code in the Diff Viewa and copy
these changes into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t37...t38
In this section we will look at parameters and see that they add content relatively statically. This is a
drawback. The benefit is that it is not complicated to use.
a security risk. To prevent this, we use the function htmlspecialchars()b . This ensures that the
characters that have a special meaning in HTML are converted to plain text.
a
en.wikipedia.org/wiki/list_of_xml_and_html_character_entity_references
b
www.php.net/manual/en/function.htmlspecialchars.php
Take a look at the code snippet below. I think the HTML code is self-explanatory. We add HTML
markup, which is only displayed if a certain parameter is set. For example, for the footer via the query
$this->params->get('showFooter'). What is displayed then also depends on the values for the
parameters filled in by the user in the backend.
templates/facile/index.php
1 </nav>
2 <?php endif; ?>
3
4 + <?php if ($this->params->get('showBanner')) : ?>
5 + <section id="banner">
6 + <div class="content">
7 + <h2><?php echo htmlspecialchars($this->params->get('
bannerTitle')); ?></h2>
8 + <p><?php echo htmlspecialchars($this->params->get('
bannerDescription')); ?></p>
9 + <a href="#main"
10 + class="button scrolly"><?php echo htmlspecialchars
($this->params->get('bannerButton')); ?></a>
11 + </div>
12 + </section>
13 + <?php endif; ?>
14 +
15 <section id="main">
16 <div class="container">
17 <div class="row gtr-200">
18
19 </section>
20
21 <footer id="footer">
22 + <?php if ($this->params->get('showFooter')) : ?>
23 + <div class="col-12">
24 + <section>
25 + <?php
26 + $fieldValues = $this->params->get('
showFooterTouchFields');
27 +
28 + if (empty($fieldValues))
29 + {
30 + return;
31 + }
32 +
33 + $html = '<ul class="contact">';
34 +
35 + foreach ($fieldValues as $value)
36 + {
37 + $html .= '<li><a class="icon brands ' .
$value->touchsubicon . '" href="' . $value->touchsuburl . '"><span
class="label">' . $value->touchsubname . '</span></a></li>';
38 +
39 + }
40 +
41 + $html .= '</ul>';
42 +
43 + echo $html;
44 +
45 + ?>
46 + </section>
47 + </div>
48 + <?php endif; ?>
49 +
50 +
51 <?php if ($this->countModules('footer', true)) : ?>
52 <div id="copyright">
53 <jdoc:include type="modules" name="footer" />
Tip: You would like to optionally define in the template which module position is used. For exam-
ple, is it important for you that a sidebar can be completely deactivated? Then create the parame-
ter showSidebarLeft and extend the line <?php if ($this->countModules('sidebar-
left', true)): ?>. In the end, this should become <?php if ($this->countModules('
sidebar-left', true)&& $this->params->get('showSidebarLeft')): ?>.
templates/facile/language/en-GB/tpl_facile.ini
13 +TPL_FACILE_GET_IN_TOUCH_SUBNAME="Name"
14 +TPL_FACILE_GET_IN_TOUCH_SUBICON="Icon"
15 +TPL_FACILE_GET_IN_TOUCH_SUBURL="URL"
In order to display one form field in dependency to another, we use showon. showon="showBanner
:1" ensures that the current field is only shown if the showBanner field has the value 1.
The field of type type="subform" provides the possibility to flexibly define the number of values in
the backend form. Thus, with one form field it is possible to insert either only one link to Facebook or
to display many social media channels.
templates/facile/templateDetails.xml
1 <position>footer</position>
2 <position>debug</position>
3 </positions>
4 + <config>
5 + <fields name="params">
6 + <fieldset name="banner" label="
TPL_FACILE_BANNER_FIELDSET_LABEL" description="
TPL_FACILE_BANNER_FIELDSET_DESC">
7 + <field
8 + name="showBanner"
9 + type="radio"
10 + label="TPL_FACILE_BANNER_LABEL"
11 + layout="joomla.form.field.radio.switcher"
12 + default="0"
13 + filter="integer"
14 + >
15 + <option value="0">JNO</option>
16 + <option value="1">JYES</option>
17 + </field>
18 +
19 + <field
20 + name="bannerTitle"
21 + type="text"
22 + default="Welcome to the Joomla version of TXT by
HTML5 UP"
23 + label="TPL_FACILE_BANNER_TITLE"
24 + filter="string"
25 + showon="showBanner:1"
26 + />
27 +
28 + <field
29 + name="bannerDescription"
30 + type="text"
79 + name="touchsuburl"
80 + type="url"
81 + label="TPL_FACILE_GET_IN_TOUCH_SUBURL"
82 + size="30"
83 + filter="url"
84 + validate="url"
85 + />
86 + </form>
87 + </field>
88 + </fieldset>
89 + </fields>
90 + </config>
91 </extension>
Copy the files in the templates folder to the templates folder of your Joomla 4 installation.
A new installation is not necessary. Continue using the ones from the previous part. In any case you
should make sure that the template style Facile is active. In my examples the Blog sample files are
installed.
2. activate the banner in the template style of Facile and see the result in the frontend.
Figure 46.1.: Create Joomla Template - Banner via parameters in the frontend
Figure 46.2.: Create Joomla Template - Banner via parameters in the backend
3. activate the social media display in the template style of Facile and see the result in the frontend.
I use the icons fa-facebook-f for Facebook and fa-twitter for Twitter. I can do this because
the template integrates Facile Font Awesomea . See /templates/facile/assets/webfonts.
a
fontawesome.com/v5/search?m=free
There is a lot to consider when loading styles and stylesheets in the frontend. Performance plays a role
and possibly the order in which files are loaded. In Joomla, there were often conflicts and cumbersome
workarounds. Joomla 4 changes this with the concept of web assets.
I think it is important to understand that the Joomla Web Assets Manager manages all assets in a
Joomla installation. It does not apply assets specifically for a template. If an extension is loaded
and it needs assets, it can also use the Web Assets Manager. But: It does not have to. Assets can still
be included via Joomla\CMS\HTML\HTMLHelper - for example via HTMLHelper::_('jquery
.framework');. The advantage of the Webassets Manager is that it ensures that assets are not
loaded twice if two extension use the same asset file. And the assets are loaded in the defined
order. This prevents conflicts. What I think is especially noteworthy for template developers is that
when other Joomla extensions include their assets via HTMLHelper, these assets are appended
after the Web Asset Manager assets. This results in overriding styles that are set in the template.
See in this context (Issue 35706)https://github.com/joomla/joomla-cms/issues/35706.
For impatient people: Look at the changed programme code in the Diff Viewa and transfer these
changes into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t38...t39
In this section, we are not adding a new function. We are merely rebuilding. We change the way we
integrate the JavaScript and CSS files. From now on, a Joomla-specific function will be used for this,
which offers many advantages.
In the chapter explaining how to use the database in the frontend, I had already written that you
can integrate web assets via the joomla.asset.json file. Here I show how to use the Web Asset
Manager without joomla.asset.json.
In the file templates/facile/index.php we change the way JavaScript and CSS is included. We
replaced the <script> tags in the footer and the <link rel="stylesheet".. /> in the header.
Instead of them we use the Joomla Web Asset Manager. This makes it necessary to use the <jdoc:
include type="styles"/> and <jdoc:include type="styles"/> tags. We give control here.
Joomla does work for us in return. If we configure the assets correctly Joomla loads everything
optimized and conflict free.
47.1.2.0.1. templates/facile/index.php The following code snippet shows you the changes in the
file templates/facile/index.php.
templates/facile/index.php
1 \defined('_JEXEC') or die;
2 +
3 +use Joomla\CMS\HTML\HTMLHelper;
4 +
5 $templatePath = 'templates/' . $this->template;
6 +$wa = $this->getWebAssetManager();
7 +$wa->registerAndUseStyle('main', $templatePath . '/assets/css/main.css
');
8 +HTMLHelper::_('jquery.framework');
9 +$wa->registerAndUseScript('dropotron', $templatePath . '/assets/js/
jquery.dropotron.min.js', [], ['defer' => true], []);
10 +$wa->registerAndUseScript('scrolly', $templatePath . '/assets/js/
jquery.scrolly.min.js', [], ['defer' => true], []);
11 +$wa->registerAndUseScript('browser', $templatePath . '/assets/js/
browser.min.js', [], ['defer' => true], []);
12 +$wa->registerAndUseScript('breakpoints', $templatePath . '/assets/js/
breakpoints.min.js', [], ['defer' => true], []);
13 +$wa->registerAndUseScript('util', $templatePath . '/assets/js/util.js'
, [], ['defer' => true], []);
14 +$wa->registerAndUseScript('main', $templatePath . '/assets/js/main.js'
, [], ['defer' => true], []);
15 ?>
16
17 <!DOCTYPE html>
18 <html lang="de">
19
20 <head>
21 - <meta charset="utf-8">
22 + <jdoc:include type="metas" />
23 <meta name="viewport" content="width=device-width, initial-scale
=1.0">
24 - <link rel="stylesheet" href="<?php echo $templatePath; ?>/assets/
css/main.css" />
25 - <title>Titel</title>
26 + <jdoc:include type="styles" />
27 + <jdoc:include type="scripts" />
28 </head>
29
30 <body class="homepage is-preload">
31 @@ -137,15 +149,6 @@ class="button scrolly"><?php echo htmlspecialchars
($this->params->get('bannerBut
32 </footer>
33
34 <jdoc:include type="modules" name="debug" />
35 -
36 - <script src="<?php echo $templatePath; ?>/assets/js/jquery.min
.js"></script>
37 - <script src="<?php echo $templatePath; ?>/assets/js/jquery.
dropotron.min.js"></script>
38 - <script src="<?php echo $templatePath; ?>/assets/js/jquery.
scrolly.min.js"></script>
39 - <script src="<?php echo $templatePath; ?>/assets/js/browser.
min.js"></script>
40 - <script src="<?php echo $templatePath; ?>/assets/js/
breakpoints.min.js"></script>
41 - <script src="<?php echo $templatePath; ?>/assets/js/util.js
"></script>
42 - <script src="<?php echo $templatePath; ?>/assets/js/main.js
"></script>
43 -
44 </div>
45 </body>
Asynchronous loading of web assets leads to an improvement in noticed loading time. External
resources such as JavaScript can be assigned the defer and async attributes when tagged in
the HTML document. If a resource is given the defer attribute, the script will not execute until
the Document Object Model (DOM) has been loaded. By specifying the async attribute, the
JavaScript is loaded and executed asynchronously in the background. This avoids blocking the
rendering to the browser and multiple scripts are loaded and executed in parallel.
Copy the files in the templates folder to the templates folder of your Joomla 4 installation.
A new installation is not necessary. Continue using the files from the previous part, unless you use the
variant with the file joomla.asset.json. The joomla.asset.json has to be registered and this is
done during the installation.
2. no visible new function has to be added. Make sure that the drop down menu works and the
display fine. If it is, then all files are loaded correctly.
47.3. Links
Web Assets1
1
docs.joomla.org/j4.x:web_assets
Dark Mode is a hot topic right now. Apple, for example, has integrated dark mode into its operating
systems. Windows and Google have done the same. Dark Mode is in fashion. And not only that. It offers
advantages. Whether darker displays are good for the eyes is debatable. What is clear, however, is that
less light saves energy.
For impatient people: Look at the changed programme code in the Diff Viewa and transfer these
changes to your development version.
a
codeberg.org/astrid/j4examplecode/compare/t39...t40
In this section we will integrate the possibility of switching to a dark mode into the Tempalte Facile. We
do this with the help of a specially created CSS file. Which mode is active, we query via the property
prefers-color-scheme. This recognizes which variant the user has set as desired in the operating
system.
I use the following snippet to have the information displayed in the browser console beforehand. This
way I am sure that the property “prefers-color-scheme” is supported and how it is set.
1 <script>
2 if (window.matchMedia('(prefers-color-scheme)').media !== 'not all'
) {
3 console.log('Dark mode is supported');
4 }
5 if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
6 console.log('Dark mode');
7 } else {
8 console.log('Light mode');
9 }
10 </script>
Are you wondering what window.matchMedia means? window.PLACEHOLDER means that the vari-
able PLACEHOLDER is declared in the global scope. This means that any JavaScript code has access to
this variable. The use of window1 is not mandatory. However, window is often used as a convention
to indicate that a variable is global. Global variables should be avoided. Using them is not a good
programming style. It is safer to define your own variables, if possible.
If you want to implement a dark mode, there is an uncomplicated solution: simply display ev-
erything in black and white. The text @media (prefers-color-scheme: dark){ body {
background: #333!important; color: white !important; }} in the CSS file would
do this. A matching color scheme is better in terms of quality.
Added the CSS file templates/assets/css/main.dark.css. This new stylesheet contains the rules
for the dark mode. It differs from templates/assets/css/main.css only in some color codes.
The system messages appear to bright in dark mode. So far we have used these function un-
changed. In Dark Mode I adjust these now. This is the web component joomla-alert. The
appearance is changeable via joomla-alert { ..} in the CSS file.
templates/facile/index.php
48.2. Side Note: Dark Mode depending on the position of the sun
An interesting idea is to switch the dark mode depending on the position of the sun at the viewer: As
soon as the sun sets on the viewer’s side, the dark mode should kick in. Not only time and date play a
role, but also the geo position. I found a possible implementation on Codepen.
1 html {
2 --text-color: #2f2f2f;
3 --bg-color: #fff;
4 }
5
6 html[data-theme='dark'] {
7 --text-color: #fff;
8 --bg-color: #2f2f2f;
9 }
10
11 body {
12 colour: var(--text-color);
13 background: var(--bg-color);
14 }
These CSS variables are switched on and off via JavaScript, which queries the time zone.
32 }
33 }
34
35 navigator.geolocation.getCurrentPosition(success, error, options)
Copy the files in the templates folder to the templates folder of your Joomla 4 installation.
A new installation is not necessary. Continue using the ones from the previous part. Your website
should now support dark mode. In the upper left area there should be a switch to toggle the mode.
48.4. Links
prefers-color-scheme2
dark-mode-toggle-Element3
2
web.dev/prefers-color-scheme
3
github.com/googlechromelabs/dark-mode-toggle
49. Favicon
A favicon is a small icon used to identify a website in a recognizable way. It appears in different ways
on different devices. Most often, you see it as an icon in your favorites when you save the website here
to visit it again more quickly. Almost always the tabs in the browser are marked with the icon.
The size and type of the favicon is expected to be different on different devices. I use the website
realfavicongenerator.net to create the optimal format of my image for the individual devices. I
consider this tool to be tried and tested and the easiest to use. However, there is an alternative
newer approach that is used by the Joomla standard template Cassiopeia. If you prefer to use
the modern SVG format with an ICO file as a fallback layer, you will find a solution that suits you
better under Favicon in Joomla template.
For impatient people: Look at the changed programme code in the diff viewa and transfer these
changes into your development version.
a
codeberg.org/astrid/j4examplecode/compare/t40...t41
In this section we create a recognisable image. In the first step, we choose an image. For the example,
I chose a yellow PNG file. In the next step, we convert it into different formats using the website
realfavicongenerator.net.
Tip: Clear the browser cache if changes to the favicon are not visible during development.
The favicon generator creates 9 files which we copy into our template directory. I put all of them in the
directory templates/facile/favicon_package. These are exactly the files
1. android-chrome-192x192.png
2. android-chrome-512x512.png
3. apple-touch-icon.png
4. browserconfig.xml
5. favicon-16x16.png
6. favicon-32x32.png
7. favicon.ico
8. mstile-150x150.png
9. site.webmanifest
49.1.2.0.1. templates/facile/index.php In order for the files to be found, new lines in the file
templates/facile/index.php are required. The variable $templatePath helps me to create the
relativ path.
We introduced the variable $templatePath in the last chapter. It is assigned $templatePath =
'templates/'. $this->template; and thus points to the directory of the current template
in the Joomla directory tree. The entry <link rel="icon"type="image/png"sizes="16
x16"href="<?php echo $templatePath . '/favicon_package'; ?>/favicon-16
x16.png"> thus becomes <link rel="icon"type="image/png"sizes="16x16"href="
/PathToJoomla/templates/facile/favicon_package/favicon-16x16.png"> in the
HTML source code.
templates/facile/index.php
16 </head>
1. install your template in Joomla version 4 to test it: Copy the files in the templates folder to the
templates folder of your Joomla 4 installation. A new installation is not necessary. Continue
using the ones from the previous part. Make sure that the favicons are displayed correctly on the
devices. Below you can see a representation in the browser Firefox.
49.3. Links
Favicon Generator1
1
realfavicongenerator.net
50. Lighthouse
The template is ready. Now you want to make sure that it is technically good and contains no errors.
Then take a look at Lighthouse1 . This is a browser plug-in and an audit tool developed in Google
Chrome with which the loading time of a website can be examined and optimised. In addition to the
structure of HTML, CSS and JavaScript files, it also takes into account the integration of images and the
cache settings of the website.
1
developers.google.com/web/tools/lighthouse
2
en.wikipedia.org/wiki/mobile_app
Figure 50.1.: Create Joomla Template - Page Speed Analysis with Lighthouse
• 0 - 49 (red) = poor
• 50 to 89 (yellow) = medium
• 90 to 100 (green) = good
With Joomla, 100% is achievable in all areas. You can find a concrete example at die-beste-
website.de.
Lighthouse is included in the Google Chrome web browser by default. Open the website you want to
test and activate Lighthouse:
You can use the results and tips from Lighthouse to improve your website. Some of the tips3 come
directly from the Joomla community.
Figure 50.2.: Create Joomla Template - Page Speed Analysis with Lighthouse
The results of the Lighthouse analysis vary at different times and under different conditions. Reasons
for this are, for example
50.2. Links
Lighthouse4
3
github.com/googlechrome/lighthouse-stack-packs/pull/44/files
4
developers.google.com/web/tools/lighthouse
Part V.
51. Package
We have created a lot of different extensions. It is annoying to do a separate installation for each one.
This is not reasonable for a user. Moreover, some of these extensions build on each other and it is
important to make sure that everything is installed and nothing has been forgotten. Therefore, in
this concluding chapter I show how different extensions are packed together into one installation
package.
For impatient people: Look at the changed programme code in the Diff Viewa and apply these
changes to your development version.
a
codeberg.org/astrid/j4examplecode/compare/t41...t42
51.1.1.1. Package
administrator/manifests/ packages/foos/script.php
1
2 <?php
3
4 \defined('_JEXEC') or die;
5
6 class Pkg_FoosInstallerScript
7 {
8 public function __construct()
9 {
10 $this->minimumJoomla = '4.0';
11 $this->minimumPhp = JOOMLA_MINIMUM_PHP;
12 }
13 }
administrator/manifests/ packages/pkg_foos.xml
3. create a ZIP that contains all ZIP files and the files of this chapter.
5. make sure that all the extensions specified in the files section have been installed.
You will continue to develop your component. How do you make sure that users always use the latest
version? How do they know about an update? Now that the basic framework of the extension is ready,
it’s important that your users know about enhancements.
In this chapter I will explain how to create and run an update server for your component. If you want to
continue working on the features first, I fully understand. Then just skip this section and come back
when you publish your extension.
Update Server sounds complicated, it’s basically just a URL to an XML file. This URL is inserted in the
extension’s installation manifest. The XML file contains a number of details, including the new version
number and the download URL to the installation file. When Joomla finds an update for an installed
extension, this is displayed in the administration area.
For impatient people: Look at the changed program code in the diff viewa and include these
changes in your development version.
a
codeberg.org/astrid/j4examplecode/compare/t1...t1b
In the current section, two files are added that are stored outside the website. The addresses or URLs
under which these are stored were entered in the previous chapters in the file src/administrator/
components/com_foos/foos.xml.
1<changelogurl>https://codeberg.org/astrid/j4examplecode/raw/branch/
tutorial/changelog.xml</changelogurl>
2 <updateservers>
3 <server type="extension" name="Foo Updates">https://codeberg.org/
astrid/j4examplecode/raw/branch/tutorial/foo_update.xml</server>
4 </updateservers>
Wondering where to save those files? Perhaps an example is appropriate. Go to the repo
https://github.com/astridx/pkg_agadvents. Here you can see the files agadvents-update4.xml
and changelog.xml. If you click on one of the files, you can call the raw version via a button
You have told your component in the file administrator/components/com_foos/foos.xml where to find
out about updates. That is in the file foo_update.xml.
Create the file foo_update.xml. The file can be named anything as long as it matches the name you
specified in the installation XML administrator/components/com_foos/foos.xml.
The tag updates surrounds all update elements. Create another update section each time you release
a new version.
If your extension supports other Joomla versions, create separate <update> definitions for each
version.
The value of name will be displayed in the Extension Manager Update view. If you use the same name
as the extension, you avoid confusion:
The value of the description tag is displayed when you hover over the name in the Update view.
The value of the element tag is the installed name of the extension. This should match the value in
the element column in the #__extensions table in your database.
The value of the type tag describes what extension it is, e.g. whether it is a component, a module or a
plugin.
The value of the tag version is the version number for this version. This version number must be
higher than the currently installed version of the extension in order for the available update to be
displayed.
The tag changelogurl is optional and allows to display a link informing about the changes in this
version. This file is also the subject of this chapter.
The tag infourl is optional and allows you to display a link that informs about the update or a version
note.
The tag downloads shows all available download locations for an update. The value of the tag
downloadurl is the URL to download the extension. This file can be located anywhere. The attribute
type describes whether it is a full package or an update, and the format. And the attribute format
defines the package type like zip or tar.
The tag targetplatform describes the Joomla version for which this update is intended. The value of
the attribute name should always be set to “joomla”: <targetplatform name="joomla"version
="4.*"/>.
If you create your update for a specific Joomla version you can use min_dev_level and
max_dev_level.
Sometimes you want your update to be available for a minimum PHP version. Do this with the tag
php_minimum.
For plugins, add a tag called folder and a tag called client. These tags are only needed for
plugins.
The tag folder describes the type of plugin. Depending on the plugin type, this can be system
, content or search, for example. The value of the client tag describes the client_id in the
database table #__extensions. The value for plugins is always 0, components are always 1. Modules
and Templates, however, may vary depending on whether it is a frontend 0 or a backend 1 module.
foo_update.xml
1
2 <updates>
3 <update>
4 <name>com_foos</name>
5 <description>This is com_foo</description>
6 <element>com_foos</element>
7 <type>component</type>
8 <version>1.0.1</version>
9 <changelogurl>https://codeberg.org/astrid/j4examplecode/raw/
branch/tutorial/changelog.xml</changelogurl>
10 <infourl title="agosms">https://codeberg.org/astrid/
j4examplecode/src/branch/v1.0.1/README.md</infourl>
11 <downloads>
Do you like to use a checksum? See the test description in this PR if you don’t know how to do this.
Under Ubuntu Linux it is possible to calculate the checksum via the console with sha256sum -b
myfile.zip or sha284sum -b myfile.zip.
changelog.xml
1
2 <changelogs>
3 <changelog>
4 <element>com_foos</element>
5 <type>component</type>
6 <version>1.0.0</version>
7 <note>
8 <item>Initial Version</item>
9 </note>
10 </changelog>
11 <changelog>
12 <element>com_foos</element>
13 <type>component</type>
14 <version>1.0.1</version>
15 <security>
16 <item><![CDATA[<p>No security issues.</p>]]></item>
17 </security>
18 <fix>
19 <item>No fix</item>
20 </fix>
21 <language>
22 <item>English</item>
23 </language>
24 <addition>
1
docs.joomla.org/adding_changelog_to_your_manifest_file/en
You don’t know what <![CDATA[ ... ]]> means? The term CDATAa is used in the XML markup
language for various purposes. It indicates that a given part of the document is general characters
rather than program code with a more specific, limited structure. The CDATA section may contain
markup characters (<, > and &). These are not interpreted further by the parser. The use of entities
such as < and & is not necessary.
a
en.wikipedia.org/wiki/cdata
52.1.2.1. administrator/components/com_foos/foos.xml
Only the version number has been adjusted. This change is necessary in every new chapter, because a
new function is always added. I do not mention this explicitly in the following.
administrator/components/com_foos/foos.xml
1 <authorUrl>[AUTHOR_URL]</authorUrl>
2 <copyright>[COPYRIGHT]</copyright>
3 <license>GNU General Public License version 2 or later;</license>
4 - <version>1.0.0</version>
5 + <version>1.0.1</version>
6 <description>COM_FOOS_XML_DESCRIPTION</description>
7 <namespace path="src">FooNamespace\Component\Foos</namespace>
8 <scriptfile>script.php</scriptfile>
Copy the files in the administrator folder into the administrator folder of your Joomla 4 installa-
tion.
Copy the files in the components folder into the components folder of your Joomla 4 installation.
A new installation is not necessary. Continue using the files from part 1.
2. Next, create another version of the example extension. To do this, change the version number in
the manifest. Before that, it is not possible to test the update server. Because, there is no update
yet. I mention this here anyway, what exactly happens after the creation of the next versions. 3.
3. if everything works, you will see these displays in front of you after the installation, if you click on
the menu System on the left and then select Extension in the section Updates on the right.
The image shows the status after version 23.0.0 was released.
4. so open System | Update | Extension. Here you will be offered the update for your com-
ponent. If this is not the case, click on the button Find Updates.
5. When you open it for the first time you will see the message The Download Key is missing
because you have entered the element dlid in the manifest.
6. Add a download key via System | Update Sites. Click on the name of your component.
Then you will see the text field in which you can enter any value. At the moment, this value is not
checked when the update is retrieved. Save the value.
7. if you navigate back to System | Update | Extension, you will be able to initiate an update
or view the changelog.
The update was not possible before because the Download Key was not configured.
Click the Find Updates button in the toolbar if the update is no longer displayed.
52.3. Links
2
docs.joomla.org/deploying_an_update_server/en
Numerous types of form fields are built into Joomla! The following describes the common standard
types and their parameters.
The form field type subform provides a method for using XML forms within another or for reusing
forms within an existing form. When the multiple attribute is set to true, the contained form is
repeatable.
The field has two predefined layouts for displaying the subform as either a table or a div container,
as well as support for custom layouts.
I show an example of an XML field definition for repeatable mode below. Fundamental is the line
multiple="true".
1 <field
2 name="domains"
3 type="subform"
4 label="COM_USERS_CONFIG_FIELD_DOMAINS_LABEL"
5 hiddenLabel="true"
6 multiple="true"
7 layout="joomla.form.field.subform.repeatable-table"
8 formsource="administrator/components/com_users/forms/config_domain.
xml"
9 />
I have not found an example for multiple="false" in Joomla. A use case might be if the number of
subforms depends on a condition. Then multiple="false" can be useful.
Link[docs.joomla.org/Subform_form_field_type]
54. Tests
Automated tests are not a special tool for software developers in large projects. Especially for smaller
extensions, automated tests are a help to quickly identify problems. They help to ensure that extensions
work smoothly in newer Joomla versions. The Joomla Core developers want third-party software
developers to test their extensions. This way, bugs are noticed before a user finds them. This requires a
lot of work and is therefore often not done. Especially not if it has to be done manually by humans for
each release. Automatic testing makes it possible to repeat the manual steps for each release without
a human performing the steps themselves. This way, bugs are found before a user encounters them
when accessing the live system.
54.1. Links
https://magazine.joomla.org/all-issues/october-2022/off-to-cyprus-ehm-cypress-how-joomla-does-
its-end-to-end-testing
https://github.com/joomla/joomla-cms/pull/38422
https://www.youtube.com/watch?v=26jL9EVI-98
Part VI.
Outro
I hope you enjoyed reading it and at the same time learned the basics of programming with Joomla.
If you enjoyed the book, I’d be happy if you shared it with your friends — especially those who are
interested in Joomla. A constructive review helps me provide better content in the future based on
your feedback.
From here, I recommend you turn the sample extension into a real one.
To create your own extension with your own name based on the Boilerplate-Extension, I use the
file duplicate.sha .
a
github.com/astridx/boilerplate/blob/t43/duplicate.sh
Create your own Joomla project. Try out what you have learned and share it on Github. After you
have mastered the basics, I recommend the following to expand your extension and knowledge in a
meaningful way:
1
developer.joomla.org/coding-standards/basic-guidelines.html
2
docs.joomla.org/Testing_Joomla_Extensions_with_Codeception
Index
G ET, 58 database, 63
P OST, 58 prefix, 13
update, 66
access control list, 119 using, 71
actions, 219 dates, 15
alias, 63
Debug Console, 45
alternvative overrides, 475
DEPLOY VERSION, 35
API, 379
design pattern, 34
autoload
factory method, 34
autoload psr4.php, 9
observer, 172