Developing an MVC Component/Upgrading to Joomla4
From Joomla! Documentation
< J3.x:Developing an MVC Component
Articles in This Series
- Introduction
- Developing a Basic Component
- Adding a View to the Site Part
- Adding a Menu Type to the Site Part
- Adding a Model to the Site Part
- Adding a Variable Request in the Menu Type
- Using the Database
- Basic Backend
- Adding Language Management
- Adding Backend Actions
- Adding Decorations to the Backend
- Adding Verifications
- Adding Categories
- Adding Configuration
- Adding ACL
- Adding an Install/Uninstall/Update Script File
- Adding a Frontend Form
- Adding an Image
- Adding a Map
- Adding AJAX
- Adding an Alias
- Using the Language Filter Facility
- Adding a Modal
- Adding Associations
- Adding Checkout
- Adding Ordering
- Adding Levels
- Adding Versioning
- Adding Tags
- Adding Access
- Adding a Batch Process
- Adding Cache
- Adding a Feed
- Adding an Update Server
- Adding Custom Fields
- Upgrading to Joomla4
This is a multiple-article series of tutorials on how to develop a Model-View-Controller Component for Joomla! Version.
Begin with the Introduction, and navigate the articles in this series by using the navigation button at the bottom or the box to the right (the Articles in this series).
This step covers what's involved in upgrading the tutorial code to align with Joomla version 4.
Introduction[edit]
This page describes the main aspects related to component development which have been introduced in Joomla 4, and details what has been changed in the tutorial code to align with v4.
A series of videos accompanying this step in the tutorial is available at this youtube playlist.
It's not practical to detail all the code changes on the webpage as has been done for previous tutorial steps. Instead you can find the code as a github repository at Joomla 4 Tutorial code, and can compare this with the code of the previous tutorial step (Adding Custom Fields) at Joomla 3 MVC code. The Joomla 4 code replicates all the functionality of the Joomla 3 code, with the exception of implementing the little penguin icon introduced in Adding decorations to the backend (which isn't so relevant given the change of UI in Joomla 4 Administrator) and the use of a CAPTCHA plugin (Adding a frontend form). You'll probably find it helpful to have this Joomla 4 code at hand to refer to as you read through this page.
The code for this step has been developed by going through each of the previous tutorial steps and trying to get each step working under Joomla 4.2.5. As Joomla 4 is still evolving quite fast it may be that certain aspects cited here get modified during the lifetime of Joomla 4. Please feel free to correct aspects on this page and create pull requests on the Joomla 4 github repository.
If you're installing the Joomla 4 tutorial code on a new Joomla instance then you will need to configure your instance to make it multilingual and add in the Ajax hidden menuitems as described in the step Using the language filter facility. Also ensure that every helloworld record has a valid category.
Further Reading[edit]
You can find here a tutorial for Joomla 4 component development, similar to this Joomla 3 one, and some excellent Joomla 4 documentation is also available here. Both of these references include descriptions of Joomla 4 impacts on development of other types of Joomla extensions (plugins, modules and templates), which aren't covered here.
This step doesn't cover everything required to update a component from Joomla 3 to Joomla 4, just the aspects covered by the tutorial code. If you're upgrading your own Joomla 3 component then you may find it helpful to look at Potential backward compatibility issues in Joomla 4.
Namespacing[edit]
Component Namespacing[edit]
Namespacing of classes using the PSR-4 recommendation was gradually introduced during Joomla 3 releases, and with Joomla 4 it is pretty much all namespaced.
It is now expected that Joomla extensions use namespacing also, with the base namespace for the extension defined in its manifest XML file. Joomla components use a namespace of the form Joomla\Component\<Component Name> eg Joomla\Component\Content, so following that scheme I've given the tutorial code a base namespace of Robbie\Component\Helloworld. Here's the line in the manifest helloworld.xml file:
<namespace path="src">Robbie\Component\Helloworld</namespace>
(The path="src" attribute means that it will expect the classes to be under the src subfolder.)
This actually creates 2 namespace prefixes, pointing to specific folders in our helloworld development structure and in our live joomla instance:
Robbie\Component\Helloworld\Administrator – will point to administrator/components/com_helloworld/src on the live instance and is associated with admin/src in our development area.
Robbie\Component\Helloworld\Site – will point to components/com_helloworld/src on the live instance, and is associated with site/src in our development area.
So Joomla now expects all classes names will be found under those base directories, with file name matching the class name, and the fully qualified classname matching the path eg
Robbie\Component\Helloworld\Administrator\Controller\DisplayController will be found in administrator/components/com_helloworld/src/Controller/DisplayController.php
Robbie\Component\Helloworld\Site\Model\CategoryModel will be found in components/com_helloworld/src/Model/CategoryModel.php
Note that capitalization is important in namespaces, names of classes and path names!
When Joomla 3 wanted to call functions in your component it looked for certain file names in particular folders, and checked that the name of the class in that file matched what it expected.
In Joomla 4 it generates the fully qualified name of the class it wants to find, and then looks to find it in the location where the PSR-4 recommendation would expect it.
Hence the namespace you specify in your class files must align with the file path to your PHP file, below the base src folder at your namespace prefix.
Note in particular how the View class files are now organised.
Also if you use one of the PHP predefined classes (eg Exception) in your code, then add a backslash before it (eg \Exception), otherwise PHP will try and find it in your namespace.
If you use custom Field or Rule definitions in your form xml files then you have to indicate to PHP which namespace prefix to use to look for them, eg in site/forms/add-form.xml:
addruleprefix="Robbie\Component\Helloworld\Administrator\Rule" addfieldprefix="Robbie\Component\Helloworld\Administrator\Field"
and also in admin/src/Model/HelloworldModel::preprocessForm where the associations are added into the form dynamically to form the custom modal_helloworld type field:
$fieldset->addAttribute('addfieldprefix', 'Robbie\Component\Helloworld\Administrator\Field');
Class Autoloading[edit]
Joomla uses the PHP mechanism for Autoloading Classes and on startup registers a number of autoloaders – functions which get called when PHP can't find a given class. In particular the Joomla PSR-4 autoloader function (in libraries/loader.php) is responsible for finding classes of Joomla extensions whose code is in source files which are organised according to the PSR-4 recommendation. (Joomla library classes are autoloaded using a different autoloader – see libraries/vendor/composer/autoload_static.php).
You can see the namespace prefixes of extensions on the Joomla instance by looking at administrator/cache/autoload_psr4.php. (These namespace strings have double backslashes, but in PHP they're the same strings as those with single backslashes). This is generated by Joomla looking through the component, module and plugin directories to find each extension's manifest file, and then processing these xml files to extract the <namespace> element – quite a lot of work, which is why it's cached I guess!
Let's consider what happens when Joomla wants our admin HelloworldController class. It might have something like
$classname = 'Robbie\Component\Helloworld\Administrator\Controller\HelloworldController' if (!class_exists($classname)) { …
Assuming PHP doesn't already know about this class, the class_exists function will result in the classname being passed to the registered autoload function(s). The PSR-4 autoloader will then:
- go through its array of namespaces to try and match $classname from the start of the string.
- find 'Robbie\Component\Helloworld\Administrator' as a match
- see that this maps to "administrator/components/com_helloworld/src"
- extract the remainder of the classname: 'Controller\HelloworldController'
- treat this as the path down to the class file
- and so look for "administrator/components/com_helloworld/src/Controller/HelloworldController.php"
- check that in this file there's actually a class with that namespace + class name.
So in this way Joomla can go from a fully qualified class name to find the source code for that class. This makes it easier for us too when the core Joomla code is looking for one of our classes. We still need to know what class it's looking for, but once we know that, we know what we should call our class and where we should save the source code for it.
Joomla Library classes[edit]
Originally Joomla's core classes (beginning with a capital J) were global classes. Over the Joomla 3 versions and into Joomla 4 these migrated to their namespaced equivalents, but the original global classnames were still available as Joomla registered them as aliases (in the libraries/classmap.php file).
In a Joomla 4 extension you can still use these global classnames, except that in a namespaced file you'd have to prefix the name with a backslash to indicate a global class rather than one in your namespace (eg \JFactory instead of JFactory). However it's pretty definite that the aliases will disappear at some stage, so the upgrade to Joomla 4 seems an appropriate opportunity to replace all your component's calls to the global JSomething classes to the namespaced equivalents. This involves adding the appropriate use statements, and all this has been done in the tutorial code. A useful mapping between the two can be found here, where the new qualified classname can be copied and pasted into a use statement in your PHP file.
With reference to the Joomla API Documentation there are 2 Joomla library namespaces:
- Joomla CMS classes – have namespaces Joomla\CMS\Something – are found under libraries/src/Something
- Joomla Framework classes – have namespaces Joomla\Something – are found under libraries/vendor/joomla/something/src
So a big advantage of namespacing is that you can immediately determine from the fully qualified class name in a PHP use statement where the source for that class exists in the libraries directories.
On the other hand class names which aren't fully qualified don't have to be unique, eg MVCFactory
- occurs as Joomla\CMS\Extension\Service\Provider\MVCFactory in libraries/src/Extension/Service/Provider/MVCFactory.php
- occurs as Joomla\CMS\MVC\Factory\MVCFactory in libraries/src/MVC/Factory/MVCFactory.php
so sometimes you have to be careful to examine the use statement to identify the right class.
Similarly our FormModel class in site/src/Model/FormModel.php can have the same FormModel classname as the Joomla FormModel in libraries/src/MVC/Model/FormModel.php.
The API documentation gives the methods available in each of the Joomla CMS and Framework classes, but just be aware that a Joomla CMS class may extend a Framework class, and in this case some methods may not be visible under the Joomla CMS documentation.
For example, $app->setHeader() which is used to set the HTTP response code is not listed under the CMS Application class API, but is available as a method because it inherits from the Framework class AbstractWebApplication.
Other Vendor Namespaces[edit]
These are also in libraries/vendor, eg
use Psr\Container\ContainerInterface;
refers to libraries/vendor/psr/Container/src/ContainerInterface.php. However there are few of these in the tutorial code.
Other File Rearrangements[edit]
With all of the namespaced classes moving under the src directories, the tmpl layout files and form xml files have been moved out into their own directories.
Extension and Dispatcher classes[edit]
Extension[edit]
In Joomla 3 the point where the Joomla core handed over to your component code was when it ran your helloworld.php code; if it was the frontend it ran your site helloworld.php code, or if was the backend it ran your admin helloworld.php code.
In Joomla 4 the entry point into your component code is your Extension class, both for site and admin, found in admin/src/Extension/HelloworldComponent.php.
When the Joomla core application wants to start executing the component code it creates an instance of HelloworldComponent, just to get a handle on the component code.
Similarly (as described below) other Joomla classes such as Router or Categories need to call certain Helloworld methods (eg custom router functions). In Joomla 3 these methods were written in certain Helper files etc. In Joomla 4 the principle is that the component creates an instance of the HelloworldComponent Extension class, and calls the required methods on that instance.
This Extension class isn't anything like a big container class for the component, it's more like a glue class, providing a way in to the Helloworld component, enabling the Joomla core and other components call the methods which they want to call.
When Joomla core wants to execute the component code, the key method it is looking for in your Extension instance is getDispatcher, and it expects to get back a Dispatcher instance. The HelloworldComponent extends MVCComponent, which in turn extends Component, and it is this class which defines getDispatcher for us.
Dispatcher[edit]
When Joomla wants to run the Hello World component, it gets the Dispatcher instance as described above and then calls the dispatch method on it. It is this dispatch function which replaces the functionality in the admin and site helloworld.php files.
Let's look at the main lines of code in our Joomla 3 admin helloworld.php file:
if (!JFactory::getUser()->authorise('core.manage', 'com_helloworld'))
{
throw new Exception(JText::_('JERROR_ALERTNOAUTHOR'));
}
// Get an instance of the controller
$controller = JControllerLegacy::getInstance('HelloWorld');
// Execute the Request task method
$input = JFactory::getApplication()->input;
$controller->execute($input->getCmd('task'));
// Redirect if set by the controller
$controller->redirect();
The code
- performs some permissions checks
- instantiates the appropriate Helloworld controller
- executes the appropriate method in that controller
- implements any redirect set by the controller
See Model-View-Controller for further details of how this works in Joomla 3. The second and third steps used the task parameter within the URL query to determine the correct controller class to instantiate and method to call. If no task parameter was present then it instantiated the controller in controller.php and called the display method. Otherwise it instantiated one of the controllers in the controllers subfolder (eg controllers/helloworld.php) and called the appropriate method on it.
The ComponentDispatcher::dispatch method (in libraries/src/Dispatcher/ComponentDispatcher.php) now provides this functionality, but in a nicer way. After checking the permissions it uses the task by considering its parts as <controller type>.<method> (taking 'display' as default if either part is missing). It will then pass control to
- an instance of the class <Controller type>Controller
- calling the function <method>
- using the admin or site controller, as appropriate
In this way the naming of the controllers is consistent - we have DisplayController, HelloworldController and HelloworldsController, all in the same folder.
The Dispatcher class can thus be considered as providing some 'routing - execution' functionality, enabling us to find the right controller and function within that controller, and it is cleaner to have this in a separate class rather than in the JControllerLegacy class as it was in Joomla 3.
Since the Hello World tutorial uses the standard Joomla pattern for selecting the class and method based on the task and has the same permissions checks, we can use the dispatch code as it stands in ComponentDispatcher, and don't need to define a special class of our own.
Factory classes[edit]
In Joomla 3 many classes had static getInstance methods which returned an instance of that class, configured with the parameters passed to getInstance.
In Joomla 4 the getInstance methods have been deprecated in favour of using Factory classes. The preferred mechanism is now to
- get an instance of the appropriate Factory class (how we do that is covered in the services/provider.php section below)
- call the createSomething() method of the Factory class with relevant parameter values to generate an instance of the Something class.
MVC Factory[edit]
In this section the MVCFactory class refers to the one in libraries/src/MVC/Factory/MVCFactory.php.
The MVCFactory class will instantiate Controller, View, Model and Table instances on our behalf, and it does this by generating the classname of the class and then calling new.
The way it works is as follows:
- Each component gets an individual MVCFactory instance which is constructed passing the component's namespace as a parameter (so in our case 'Robbie\Component\Helloworld'). (How this MVCFactory instance is created is covered in the services/provider.php section below).
- The MVCFactory createController / createView / createModel / createTable function is then called, passing the name of the Controller / Model / View / Table and the prefix (which specifies whether the administrator or site class is required).
- The create function forms the classname from this data and calls new
The MVCFactory create functions just form the classname from the parameters they're passed. The intelligence for working out what name and prefix should be used to form the appropriate classes for an incoming HTTP request is
- in the dispatcher code for defining the controller to use
- in the controller for the view and model
- in the model for the table
and, for example, the names of the classes created for a request with URL query ?option=com_helloworld&view=viewname and no task parameter will be
<namespace>\Controller\DisplayController <namespace>\View\Viewname\HtmlView <namespace>\Model\ViewnameModel <namespace>\Table\ViewnameTable
So if you need an alternative name then you can override the appropriate getView, getModel or getTable function – as for example was done in the Adding Custom Fields step, frontend Form section.
So from the outside this is pretty much the same as in Joomla 3. However, note that the $prefix parameter in calls to getModel etc is subtly different
- in Joomla 3 this referred to a prefix to be applied to the classname – in the tutorial code we used 'HelloWorldTable' as the prefix part of the 'HelloWorldTableHelloWorld' classname.
- in Joomla 4 this refers to a prefix higher up the classname path, and should be set to either 'Site' or 'Administrator' (the capital letter at the start is optional), depending on whether you want the class under 'Robbie\Component\Helloworld\Site' or 'Robbie\Component\Helloworld\Administrator'.
The MVC Factory class is now the Joomla recommended way to create the controller, view, model and table classes, by using the eg getModel() method, and the use of getInstance() for these classes is now deprecated. However, the use of these functions is restricted:
- in the controller class you can call $this->getView($viewname, $viewtype, $prefix, $config) to create a view and $this->getModel($modelname, $prefix, $config) to create a model.
- in the view class you can call $this->getModel($modelname), but only for models which the controller has previously defined using $view->setModel($modelname, $default). It doesn't create a new model instance.
- in the model class you can call $this->getTable($tableclass, $prefix, $options) to create a table instance. Controllers and views can access the table classes in this way, by going via a model.
These restrictions do have an impact on moving away from using getInstance(), and indeed in Joomla 4.2.6 you will still find several getInstance() calls within Joomla core components.
Other Factory Classes[edit]
Several other Factory classes have been introduced in Joomla 4, enabling you to get instances of core classes such as Form, User, CacheController, etc, usually by calling a createSomething function on the Factory instance.
Associated with the Factory class is a FactoryInterface, a PHP Interface which specifies the key functions which the Factory class must implement. For example, with libraries/src/Categories we have:
- CategoryFactoryInterface – which specifies the createCategory function
- CategoryFactory implements CategoryFactoryInterface – which provides the createCategory function.
Dependency Injection[edit]
Overview[edit]
The DI container is one of the significant new additions to Joomla 4. It's basically a repository of key-value pairs where:
- the key is a string which is the name of a class or interface
- the value is an instance of the relevant class, or a function that returns an instance of that class.
You put things into the container using set() passing:
- the class name or interface name
- a function which returns an instance of the class (or an instance of the class)
- a boolean defining whether the class instance may be shared or not
- a boolean defining whether this entry into the DI container is protected or not (an error will be raised if you try to overwrite a protected entry by calling set() again using the same key).
The function share() is basically the same as set() with the shared boolean set to true.
You get things out of the container by calling get() passing the key of the resource you want. The container will
- find the key in the container
- if the value isn't already a class instance, then it will run the associated function to generate an instance of the class
- if the resource is shared, it will store the class instance, so that on subsequent invocations of get() it can just return the instance
- return the class instance to you
You can also define aliases for each key in the container, which means that you call get() passing either the key or an alias of the key.
The DI container is initialised in library/src/Factory.php createContainer() which results in the register() function being called in each of the class files in libraries/src/Service/Provider. By looking through those files you can see what gets put into the DI container upon initialisation.
JConfig Example[edit]
Let's consider the example of JConfig which holds the system global configuration. If you look inside libraries/src/Service/Provider/Config.php then you'll see the register() function and that it stores in the DI container a resource with:
- key = 'JConfig'
- value = an anonymous function - which loads what's in configuration.php into a Registry object, and returns it
- shared flag = true (because the share() function is used)
- protected flag = true
Then when you do
$config = $container->get('JConfig');
it will run the anonymous function and return the Registry object to you. As the 'shared' boolean is set it will also store a copy of the class instance locally in the container to be returned on subsequent calls of get('JConfig').
Justification[edit]
What's the point of all this?
The advantage of dependency injection is that it allows classes to be easily mocked for testing. The file libraries/src/Service/Provider/Config.php can be replaced to return to a DI container get() call a mock class instead of the usual JConfig. This is in comparison to Joomla 3 where Factory::getConfig() code directly reads the configuration.php into a Registry structure and returns it – which makes it harder to mock.
The Factory API has several functions which are marked as deprecated with advice to get the object from the DI container instead.
Several of the other Factory classes (eg Form, Language, Menu, User) are in the DI container – so that we can get an instance of them via eg
Factory::getContainer()->get('Joomla\CMS\Toolbar\ToolbarFactoryInterface')
to get a Toolbar instance. However, this only works for singleton classes which aren't specific to the needs of a particular extension. We'll see shortly how to make it work for class instances which need to be specific to a component.
As indicated above, Joomla is moving away from using the class static getInstance() functions. We can see now that this is not only because it's more pure from an object oriented perspective. But also creating class instances from the DI container, or from Factory objects which are obtained from the DI container, builds in the dependency injection which allows classes to be more easily mocked, and hence more easily tested.
services/provider.php[edit]
We're now finally in a good position to understand what's happening in our services/provider.php file, and we'll explore this by walking through what happens whenever this line in libraries/src/Component/ComponentHelper.php renderComponent is run:
$app->bootComponent($option)->getDispatcher($app)->dispatch();
This is run whenever Joomla has worked out which component is to be run on the requested web page, and it wants to run that component to capture its output.
Step 1 Create Child DI Container[edit]
Joomla first creates another DI container (initially empty) just for this component, and links it to the main DI container via a parent field in the child DI container structure.
Now whenever set() is called on this DI container the key-value pair is inserted into the child container. Whenever get() is called on it, the resource is obtained from the child container, but if it's not found there then the parent container is searched also.
Step 2 Run register() in provider.php[edit]
Next we encounter these lines (in libraries/src/Extension/ExtensionManagerTrait.php), where $path contains the path of our helloworld admin/services/provider.php file:
$provider = require_once $path; // Check if the extension supports the service provider interface if ($provider instanceof ServiceProviderInterface) { $provider->register($container);
Here we see 3 things:
- our provider.php should return some object (so that $provider gets set to something)
- what is returned should implement ServiceProviderInterface (you can see in libraries/vendor/joomla/di/src/ServiceProviderInterface.php what is expected)
- the code then runs the register function on that object, passing as a parameter the child DI container.
Let's consider a minimal version of our provider.php file. This would actually support steps in the Joomla 3 tutorial up to and including the Adding Ajax step.
return new class implements ServiceProviderInterface { public function register(Container $container): void { $container->registerServiceProvider(new MVCFactory('\\Robbie\\Component\\Helloworld')); $container->registerServiceProvider(new ComponentDispatcherFactory('\\Robbie\\Component\\Helloworld')); $container->set( ComponentInterface::class, function (Container $container) { $component = new HelloworldComponent($container->get(ComponentDispatcherFactoryInterface::class)); $component->setMVCFactory($container->get(MVCFactoryInterface::class)); return $component; } ); } };
As you can see from the line:
return new class implements ServiceProviderInterface {
this file does indeed return something – it returns an instance of an anonymous class, one that implements ServiceProviderInterface.
Note that it's not sufficient to just provide the register function in the class, we also need to explicity state that the class implements ServiceProviderInterface. This now happens in several places in Joomla, so if you're debugging your component and wondering why a function you expect to be called isn't being called, then check to make sure that you don't have to explicitly add the appropriate implements <Interface> clause to your class.
Next we look at the 3 lines in the register function
Step 3a Registering our MVC Factory[edit]
$container->registerServiceProvider(new MVCFactory('\\Robbie\\Component\\Helloworld'));
Here our MVCFactory refers to Joomla\CMS\Extension\Service\Provider\MVCFactory in libraries/src/Extension/Service/Provider/MVCFactory.php, and you should have a look at this file. This is a file similar to our provider.php file, but it's a named class, and it doesn't return anything. When we do:
new MVCFactory('\\Robbie\\Component\\Helloworld')
it creates an instance of this class, and in the constructor it stores locally in $this->namespace our namespace.
The registerServiceProvider function then calls the register function of this instance, which runs (in its minimalist form):
$container->set( MVCFactoryInterface::class, function (Container $container) { $factory = new \Joomla\CMS\MVC\Factory\MVCFactory($this->namespace); return $factory; } );
So what gets entered into our child DI container is a key-value pair with:
- key = MVCFactoryInterface::class (this is just a shorthand form to obtain a string of the fully qualified name – ie 'Joomla\CMS\MVC\Factory\MVCFactoryInterface')
- value = a function which returns an instance of \Joomla\CMS\MVC\Factory\MVCFactory.
This is our MVCFactory class which generates our Controllers, Views, Models and Tables.
Notice that we're passing our namespace as a parameter $this->namespace. Because this function is a closure, this variable will still be available when we run this function later, and when the MVCFactory class is instantiated the namespace will be passed into the constructor, stored locally, and then subsequently used to form the fully qualified names of our Controllers, View, etc. So this is how we get instantiated the MVCFactory class which is specific to our component.
Step 3b Registering the Dispatcher Factory[edit]
Careful: ComponentDispatcherFactory refers to two different classes here!
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Robbie\\Component\\Helloworld'));
This ComponentDispatcherFactory is Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory in libraries/src/Extension/Service/Provider/ComponentDispatcherFactory.php. Similar to the above, the ComponentDispatcherFactory service provider class stores locally our namespace, and then when register is called it executes:
$container->set( ComponentDispatcherFactoryInterface::class, function (Container $container) { return new \Joomla\CMS\Dispatcher\ComponentDispatcherFactory($this->namespace,$container->get(MVCFactoryInterface::class)); } );
so a second entry is added to the child DI container with:
- key = ComponentDispatcherFactoryInterface::class
- value = a function which returns an instance of the ComponentDispatcherFactory (the other ComponentDispatcherFactory class!), where the class constructor will be passed the namespace ('\\Robbie\\Component\\Helloworld') and an MVCFactory instance, retrieved from the DI container.
Remember that the code
$container->get(MVCFactoryInterface::class)
will get from the container an entry with key = 'Joomla\CMS\MVC\Factory\MVCFactoryInterface' – which is what we entered into the container in step 3a – and will run the function associated with this key, and then return the object which this function returns. This returned object will be an instance of '\Joomla\CMS\MVC\Factory\MVCFactory', constructed using our namespace.
Step 3c Including the Extension[edit]
$container->set( ComponentInterface::class, function (Container $container) { $component = new HelloworldComponent($container->get(ComponentDispatcherFactoryInterface::class)); $component->setMVCFactory($container->get(MVCFactoryInterface::class)); return $component; } );
This will put into the child DI container a key-value pair with
- key = ComponentInterface::class
- value = the function which will create an instance of our HelloworldComponent (ie our Extension class), and pass into the constructor an instance of the ComponentDispatcherFactory class, retrieving what was entered into the child DI container in Step 3b.
It will call $container->get(MVCFactoryInterface::class), getting an MVC Factory instance from what was entered into the DI container in Step 3a.
It will then associate our component with this MVC Factory instance via the setMVCFactory call. Significantly this will then allow our component to access this instance later, via a call to getMVCFactory.
Step 3 Summary[edit]
We have now got 3 entries in our child DI container, keyed by
- key = MVCFactoryInterface::class, value is a function returning an MVCFactory instance
- key = ComponentDispatcherFactoryInterface::class, value is a function returning a ComponentDispatcherFactory instance
- key = ComponentInterface::class, value is a function returning our HelloworldComponent, an instance of our Extension class.
The important aspect is that these entries are all aware of our namespace '\Robbie\Component\Helloworld' and will return instances of these classes which are specific to our Helloworld component.
Step 4 Retrieving from the DI Container[edit]
Joomla will then execute the line:
$extension = $container->get($type);
where $type contains 'Joomla\CMS\Extension\ComponentInterface'.
This starts the process of retrieving from the DI container the 3 entries which we put in, and will run our code in services/provider.php:
$component = new HelloworldComponent($container->get(ComponentDispatcherFactoryInterface::class)); $component->setMVCFactory($container->get(MVCFactoryInterface::class)); return $component;
We end up with a HelloworldComponent instance, which has:
1. a ComponentDispatcherFactory instance passed into the constructor, and which is saved locally by the constructor code:
public function __construct(ComponentDispatcherFactoryInterface $dispatcherFactory) { $this->dispatcherFactory = $dispatcherFactory; }
(This code is in libraries/src/Extension/Component.php, in the class which our HelloworldComponent extends, via the class MVCComponent).
2. an MVC Factory instance, stored by the HelloworldComponent setMVCFactory call
Step 5 Call boot() on our Component[edit]
Joomla calls:
if ($extension instanceof BootableExtensionInterface) { $extension->boot($container); }
This will call boot within our HelloworldComponent, and allows us to perform some initialisation within our component. We'll return to this later, in the section on HTML snippets.
Note again that Joomla checks that our HelloworldComponent class implements BootableExtensionInterface before calling boot. It's not sufficient to just provide the boot function, we have to explicitly declare that our HelloworldComponent implements the interface.
Step 6 Get the Dispatcher[edit]
Returning to our key line of code:
$app->bootComponent($option)->getDispatcher($app)->dispatch();
we have now accomplished the first part bootComponent($option), and this has returned our HelloworldComponent instance. Now we call getDispatcher($app) on this instance.
This code is also in libraries/src/Extension/Component.php, in the Component class which our HelloworldComponent extends:
public function getDispatcher(CMSApplicationInterface $application): DispatcherInterface { return $this->dispatcherFactory->createDispatcher($application); }
We call createDispatcher on our ComponentDispatcherFactory class instance which we saved in Step 4. This code is in libraries/src/Dispatcher/ComponentDispatcherFactory.php:
public function createDispatcher(CMSApplicationInterface $application, Input $input = null): DispatcherInterface { $name = ucfirst($application->getName()); $className = '\\' . trim($this->namespace, '\\') . '\\' . $name . '\\Dispatcher\\Dispatcher'; if (!class_exists($className)) { if ($application->isClient('api')) { $className = ApiDispatcher::class; } else { $className = ComponentDispatcher::class; } } return new $className($application, $input ?: $application->input, $this->mvcFactory); }
The code checks if we have our own Dispatcher class (our Helloworld tutorial doesn't, but com_users for example has its own). So it creates a ComponentDispatcher instance, and passes into its constructor the MVC Factory instance.
Remember that this ComponentDispatcherFactory instance is what was instantiated when it was retrieved from our child DI Container (from being inserted in step 3b), and hence it already knows about our namespace, and has a pointer to our MVCFactory class instance.
Step 7 call dispatch()[edit]
This is the final aspect to our line of code:
$app->bootComponent($option)->getDispatcher($app)->dispatch();
We're now back to what was described in the Dispatcher section above, except that now we can see that this Dispatcher instance has a pointer to our MVC Factory instance (which knows about our namespace prefix), so when the Dispatcher code wants to create a Controller it can use that MVC Factory instance to instantiate it.
It's been a long and tortuous journey to get here, but the significant thing is that all of our key class instances have been obtained via the DI Container. If we want to mock out any of those classes for testing then all we need to do is replace the relevant services/provider file.
Javascript and the Web Asset Manager[edit]
The web asset manager has been introduced in Joomla 4 and is well documented here. It provides a much-improved means of managing JS and CSS resources, and a framework for handling their dependencies.
Component Javascript and CSS files should now be stored in the media folder, and the Web Asset Manager used to register them and output them on specific web pages.
The following assets have been registered in the tutorial joomla.asset.json, and you should have a look at that file now.
com_helloworld.validate-greeting[edit]
This is the code for client validation of the helloworld greeting field using a regex, and it is unchanged.
Note that it is dependent upon jQuery and the Joomla form.validator script, so those both get included as dependencies. If we didn't have specific validation on our form then we would need to include explicitly the form.validator script in order for the basic Joomla field validation to be included, eg in the tmpl file:
$this->document->getWebAssetManager()->useScript('form.validate');
Note also that this requires us to have a class="form-validate" on the <form> html element.
com_helloworld.fix-permissions-ajax-call[edit]
This is the code which copies the value from the greeting field into the title field, so that the Ajax calls setting the permissions work correctly. The js code is unchanged.
com_helloworld.enable-tooltips[edit]
This is the code which enables bootstrap tooltips to operate, including allowing them to include html code. It's used to display the image as a tooltip within the admin helloworlds view.
com_helloworld.openstreetmap[edit]
This is defined as a preset, comprising the helloworld js and css code for the map display, as well as the dependency named 'com_helloworld.openlayers' relating to the openlayers CSS and js code. The code is unchanged.
com_helloworld.admin-helloworlds-modal[edit]
This is the js code for selecting an individual helloworld record within a modal, and passing its details to the form in the main window. Its code is unchanged.
submitbutton.js[edit]
In the Joomla 3 code we introduced in Adding Verifications a js file which controlled when the form was submitted. It differentiated between buttons such as Save (which submitted the form) and Cancel (which didn't).
In Joomla 4 there is a more sophisticated version of Joomla.submitbutton (found in media/system/js/core.js) which uses the CSS classes which the Toolbar functionality sets on the buttons to determine whether the form should be submitted or not. And you can override whether the validation is performed by adding a third parameter to the submitbutton js call. For example, to avoid validation being performed when a button is clicked you can set on the button element:
onclick="Joomla.submitbutton('controller.method', , false)"
As this is sufficient for what we need in the tutorial, the custom submitbutton.js has been removed.
Interaction with other components[edit]
The way that Joomla interacts with components has changed in Joomla 4. In general the Joomla functionality will boot the component using something like:
$extension = $app->bootComponent($option);
and in our case this will return an instance of the HelloworldComponent class. It will then try to call certain methods on our extension object, as described below.
com_categories[edit]
In the backend the helloworld functionality can display to the administrator
- the view of the helloworld messages
- the view of the helloworld categories
The categories view is generated by com_categories, and in Joomla 4 these component categories views display the number of component records there are for each of the possible published status values. To do this the categories component executes the following code (in administrator/components/com_categories/src/Model/CategoriesModel.php):
$component = Factory::getApplication()->bootComponent($parts[0]); if ($component instanceof CategoryServiceInterface) { $component->countItems($items, $section); }
So our HelloworldComponent must:
- declare that it implements CategoryServiceInterface
- provide an implementation of countItems
We provide countItems by using a trait in libraries/src/Categories/CategoryServiceTrait.php, which (with the assistance of a helper function) does the query on our behalf. We just need to tell it:
- the name of our table – in the callback getTableNameForSection
- the database field in which we hold the published status – in getStateColumnForSection
com_associations[edit]
Remember that com_associations is the component which is run whenever you click on Multilingual Associations in the admin backend.
Under Joomla 3 we provided an associations helper file as described in Multilingual Associations/Developers.
In Joomla 4 the helper file remains basically the same, but com_associations primarily tries to find the helper file via the extension class; here's the code:
$component = Factory::getApplication()->bootComponent($extensionName); if ($component instanceof AssociationServiceInterface) { return $component->getAssociationsExtension(); }
It's expecting that:
- the extension (HelloworldComponent in our case) implements AssociationServiceInterface
- its getAssociationsExtension method returns the helper class instance.
The easiest thing would be just to code getAssociationsMethod and get it to return new AssociationsHelper. However, we follow the Joomla 4 principle of putting key classes into the Dependency Injection container, and retrieving them later, by having the following code in our services/provider.php:
$container->set(AssociationExtensionInterface::class, new AssociationsHelper());
The above line puts a key-value pair into our child DI container
- key = 'Joomla\CMS\Association\AssociationExtensionInterface'
- value is an instance of our AssociationsHelper class.
We then retrieve it later and use the setter:
$component->setAssociationExtension($container->get(AssociationExtensionInterface::class));
so that whenever
$component->getAssociationExtension()
is called it returns the AssociationsHelper class instance.
com_fields[edit]
com_fields is the component which is run when an administrator clicks on the Fields or Field Groups menuitem within the backend, and this functionality was included in the Adding Custom Fields tutorial step.
The com_fields component stores the custom fields and field groups for all components in the database tables #__fields and #__fields_groups, and these tables are partitioned into "sections", one section per component. For our helloworld component the section is named "helloworld".
Within the section we have 2 "contexts" – one for the helloworld messages (called "com_helloworld.helloworld") and one for the helloworld categories (called "com_helloworld.categories"). This enables us to define custom fields for both our helloworld records and helloworld category records.
When com_fields runs it checks both the section and contexts at various times. For example, when an admin selects Helloworld / Fields on the backend then com_fields checks the section which was defined in the menuitem in the manifest file (see below). Here's the code in administrator/components/com_fields/src/Helpers/FieldsHelper extract function:
$component = Factory::getApplication()->bootComponent($parts[0]); if ($component instanceof FieldsServiceInterface) { $newSection = $component->validateSection($parts[1], $item); }
And when com_fields wants to display the little dropdown menu of contexts (to allow the administrator to set custom fields on either helloworld items or categories) it calls (in administrator/components/com_fields/src/Fields/FieldcontextsField.php getOptions()):
$component = Factory::getApplication()->bootComponent($parts[0]); if ($component instanceof FieldsServiceInterface) { return $component->getContexts(); }
So it's clear that our component extension class must :
- implement FieldsServiceInterface
- provide the validateSection and getContexts methods
So in upgrading to Joomla 4 we've just moved those two methods from the helper file to the extension class.
Router[edit]
The Joomla router will access our custom router code:
- whenever it wants to parse an SEF route which it has identified as belonging to com_helloworld
- whenever it wants to build an SEF route from a call to Route::_() where the option = "com_helloworld".
To find our custom router it will:
- boot the com_helloworld extension
- check that the extension implements RouterServiceInterface
- call createRouter on our extension instance
In our services/provider.php file we have (careful again - 2 different RouterFactory classes!):
$container->registerServiceProvider(new RouterFactory('\\Robbie\\Component\\Helloworld'));
which from libraries/src/Extension/Service/Provider/RouterFactory.php puts into our child DI container an entry with:
- key = "Joomla\CMS\Component\Router\RouterFactoryInterface"
- value is a \Joomla\CMS\Component\Router\RouterFactory instance, constructed with our namespace
We then retrieve this in the line in our services/provider.php file:
$component->setRouterFactory($container->get(RouterFactoryInterface::class));
And then within our extension class we have
use RouterServiceTrait;
which includes
public function setRouterFactory(RouterFactoryInterface $routerFactory) { $this->routerFactory = $routerFactory; }
which stores the routerFactory locally and
public function createRouter(CMSApplicationInterface $application, AbstractMenu $menu): RouterInterface { return $this->routerFactory->createRouter($application, $menu); }
which provides our extension's createRouter call that the Joomla Router expects.
The routerFactory::createRouter method is in libraries/src/Component/Router/RouterFactory.php. It looks for a class in our site namespace Robbie\Component\Helloworld\Site\Service\Router which is in our site/src/Service/Router.php.
At this stage we have a choice, we can either:
- provide a traditional custom router, with the preprocess, build and parse functions, or
- use the more modern RouterView mechanism.
By default in the upgraded tutorial code the Router View option has been coded in the Router.php file.
RouterView[edit]
In the RouterView case the preprocess, build and parse functions are in the RouterView class which we extend, and we provide
- a constructor with router view configuration
- methods getHelloworldId and getCategoryId which are used to obtain from the SEF segment the id= and catid= parts of the internal URL
- methods getHelloworldSegment and getCategorySegment which are used to generate segments for the SEF URLs based on the id= and catid= parts within the Route::_() parameter.
Unfortunately there doesn't seem to be a good guide to configuring the RouterView class. There is some documentation available here.
In the helloworld tutorial we have 3 views
- a view which displays a single helloworld record
- a view which displays a single helloworld category, and the helloworld records associated with it
- a view which displays the frontend form
So we need 3 Router Views associated with these.
The 'form' view doesn't have an associated id or catid so it's just a fixed URL, and the router just has to convert between the alias of the form menuitem and the Itemid / view parameters.
The 'category' view has an associated id (of the category record), and an SEF URL will use the category path down the Categories Nested Set table structure, hence setNestable().
The 'helloworld' view has an associated id (of the helloworld record), and in an SEF URL in the category view the helloworld record alias is added to the category path (hence setParent()). But although the helloworld records use a Nested Table structure in the database, the URLs just include the alias of the single record, and so setNestable() is not used.
There doesn't seem to be a way to configure an additional SEF URL scheme which just displays a helloworld segment, without a category segment.
Traditional Router[edit]
To use the traditional router in the tutorial code you should uncomment the function in the extension class:
public function createRouter(CMSApplicationInterface $application, AbstractMenu $menu): RouterInterface { return new TraditionalRouter($application, $menu, $this->categoryFactory, $this->getDatabase()); }
This will cause the code in site/src/Service/TraditionalRouter.php to be used. However, changes in the Joomla 4 router mean that the interaction between it and the preprocess, build and parse methods have changed. This requires significant changes to the preprocess and build methods, but only a minor change to parse.
1. Previously Joomla injected into the $query parameters the Itemid of the active menuitem (ie the id of the menuitem the user is currently on). This meant that we had to change the Itemid for links generated by the Language Switcher module (which generates the flags for switching between languages), and these links being associated with a different language would obviously use a different Itemid.
In Joomla 4 you get an Itemid in the $query parameters only if you specified an Itemid in the Route::_() call. For others you have to work out which menuitem to use yourself, by using the Joomla menu/menuitems API to find details of the configured menuitems, and work out which to use based on the other $query parameters.
You do this in the preprocess method, and then the Itemid you set will be passed in the $query parameters to the build method.
2. In some instances the Route::_() URL you're generating matches exactly one of the configured menuitems on the site. In this case the $query parameters will match those in the menuitem "query" array, and you have to remove explicitly remove the 'view', 'id' and 'catid' parameters so that they don't appear as URL query parameters after the normal SEF URL of the menuitem. You should do this in your build function.
3. The custom router will be used to build the URLs for the helloworld menuitems shown in menus on the page. For these the $query parameters will have the view / id / catid associated with those menuitems, and Joomla will inject these parameters after your preprocess method is run. So you will have to use your build function to remove these parameters (as in point 2 above).
4. In the parse method you now have to unset the $segments, or else Joomla will return an HTTP 404 (page not found). Otherwise parse works as before.
(You don't have to unset all the query parameters in the build method. Joomla will remove Itemid, option and lang parameters, and any remaining parameters will be included as URL query parameters in the built URL).
5. I've implemented build only for when Multilanguage is enabled on the site. If it's not enabled an empty $segments array is just returned, without doing unset on any of the $query parameters, so these will appear as URL parameters after the SEF URL. Also it's not coded properly to deal with items which have language set to *. These include helloworld records, categories or menuitems (eg the helloworld form).
Note also that the categoryFactory is also now passed to the router. This is so that it can be used instead of Categories::getInstance() to get access to the Categories API calls.
com_tags[edit]
Whenever we setup a Tags Menu Item Type which displays items having a given tag or tags, then com_tags displays those items as links, and uses route helper functions to generate the URL parameters.
The route helpers com_tags expects to find loaded into the data of the #__content_types table, in the router field, and these have been set to:
- For links to helloworld items: 'HelloworldHelperRoute::getHelloworldRoute'
- For links to helloworld categories: 'HelloworldHelperRoute::getCategoryRoute'
However, com_tags doesn't boot the helloworld extension to get access to these, but rather seeks to find them in site/helpers/route.php, so that's where they continue to be.
(We also had a getCategoryRoute which was used by com_categories in administrator/components/com_categories/src/Helper/CategoryAssociationHelper. When a category was displayed on the frontend then the Language Switcher module (now Language Filter) used this function to generate the links to the Associated Categories. Our getCategoryRoute previously was necessary to insert the lang=<language tag> parameter into the link URL, but since this change the calling function inserts the lang parameter itself, so our getCategoryRoute function for this purposes is superfluous.)
Deprecated APIs[edit]
With Joomla 4 there are a number of methods which are deprecated, with a view to removing them in Joomla 5.
Deprecated Factory Methods[edit]
Many of the Factory methods for getting singleton classes have been deprecated in favour of getting these classes via the DI container, and it appears that only class instances which should be obtained via Factory methods are:
- the Application – Factory::getApplication()
- the DI container – Factory::getContainer()
Here is a list of what has been changed in the tutorial code. Note that with some calls there are variants available, through the aliases defined in the DI container.
Factory::getDbo
This has been replaced by
use Joomla\Database\DatabaseInterface; ... $db = Factory::getContainer()->get(DatabaseInterface::class);
However, in many classes the database instance is more easily (and efficiently) returned via
$db = $this->getDatabase();
This is available in Model classes, custom field definitions, etc.
Factory::getUser();
To get the current user use:
$user = Factory::getApplication()->getIdentity()
To get a different user (whose id you know):
use Joomla\CMS\User\UserFactoryInterface; ... $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userid);
Within models which extend Joomla\CMS\MVC\Model\BaseDatabaseModel and views which extend Joomla\CMS\MVC\View\HtmlView you can also use:
$user = $this->getCurrentUser()
Factory::getDocument()
Replaced by Factory::getApplication()->getDocument(), or within a View you can use $this->document.
Factory::getConfig()
Two options have been used:
Factory::getApplication->getConfig() – returns a Registry object containing all the configuration values
Factory::getApplication->get('access') – returns the value of the 'access' configuration parameter
Factory::getLanguage()
Replaced by Factory::getApplication->getLanguage()
JFactory::getCache('com_example', <type>)
Replaced by
use Joomla\CMS\Cache\CacheControllerFactoryInterface; ... Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController(<type>)
Deprecated getInstance Methods[edit]
getInstance()
In general getInstance() calls have been deprecated in favour of obtaining the instance, either via a factory class or via the DI container. For example, within a controller you can use getModel() repeatedly to get different models (passing the relevant model names), which results in the MVCFactory class creating the Model classes.
However, there are currently some problems with replacing some getInstance calls. I expect that the Joomla team will resolve these issues over the course of the Joomla 4 releases.
Categories::getInstance()
This is deprecated with a notice: "Use the ComponentInterface to get the categories". This means that we should get the category factory instance via the DI container. This is done in our services/provider.php by the lines:
use Joomla\CMS\Extension\Service\Provider\CategoryFactory; ... $container->registerServiceProvider(new CategoryFactory('\\Robbie\\Component\\Helloworld'));
to put into our child DI container the closure which will create a Joomla\CMS\Categories\CategoryFactory instance, constructed with our namespace.
Then when our HelloworldComponent (Extension) instance is created:
$component->setCategoryFactory($container->get(CategoryFactoryInterface::class));
We can then use:
$component->getCategoryFactory()->createCategory();
to create our Categories object, and in this way get access to the Categories APIs.
(This createCategory method creates the instance by looking for the \\Site\\Service\\Category class within our namespace. The constructor in our class sets our extension ('com_helloworld') and table ('#__helloworld') and calls the parent Categories constructor with these options).
However, this means that the section of our code where we want to use the Categories APIs must have a pointer to a CategoryFactory instance. In the tutorial code for Joomla 3 there are 2 places where Categories::getInstance is used:
- in the custom router code, to handle building and parsing of the category parts of the SEF URL
- in the site category model, where the Categories APIs are used to obtain details of categories to display on the page.
For the custom router code, the CategoryFactory instance is passed as a parameter to the TraditionalRouter constructor so that it can be used there.
However, the site category model is instantiated by the MVCFactory instance, and it doesn't make available a CategoryFactory instance. So in this category model the calls to Categories::getInstance() have been left, and you can still find a number of calls to Categories::getInstance within Joomla core components.
Categories::getInstance() itself solves the problem of access to a CategoryFactory instance by rebooting our component again to get a pointer to our Extension instance, and thus to the category factory instance. However, this involves a lot of work, so getInstance caches locally the Categories instances and just returns one of them if it can.
Pushing the CategoryFactory to the model
However, there is a way to get access to the CategoryFactory instance, by defining your own MVCFactory class, and pushing the instance down through it to the model. Here's how you can go about it:
- Define your own SuperMVCFactory class, which extends the Joomla Joomla\CMS\MVC\Factory\MVCFactory class, but which has a means of storing a CategoryFactory instance. Use the CategoryServiceTrait in libraries/src/Categories/CategoryServiceTrait.php
- Create your own version of the file libraries/src/Extension/Service/Provider/MVCFactory.php, which is the 'provider.php' like file for MVCFactory, but change it so that:
$factory = new \Joomla\CMS\MVC\Factory\MVCFactory($this->namespace);
becomes
$factory = new <namespace>\SuperMVCFactory($this->namespace);
and include
$factory->setCategoryFactory($container->get(CategoryFactoryInterface::class));
When Joomla creates a model such as CategoryModel it passes in its constructor the MVCFactory instance (so that the model can call createTable on it), so you can access it via
$this->getMVCFactory()
This will return your SuperMVCFactory instance. So in this way within the model you can access the CategoryFactory, and use it to create the Categories instance via
$this->getMVCFactory()->getCategory() (from the CategoryServiceTrait)
If you can get this working you can congratulate yourself on a good understanding of Joomla namespacing and dependency injection! A similar approach to address a similar problem is described here. If you get stuck you can always view the solution by viewing the github tutorial code selecting the branch push-category-factory-to-mvc.
Table::getInstance()
There are a couple of calls to Table::getInstance() within admin/src/Helper/AssociationsHelper.php in getItem():
- Table::getInstance('Helloworld', 'Robbie\\Component\\Helloworld\\Administrator\\Table\\') to get an instance of our HelloworldTable class.
- Table::getInstance('Category') to get an instance of the Category table in libraries/src/Table/Category.php.
We could actually arrange for the AssociationsHelper class to get injected with the MVCFactory instance, so that the HelloworldTable class could be instantiated by it. However, we couldn't use the helloworld MVCFactory instance to create the Category table instance, as it's not in the helloworld namespace, and our MVCFactory instance creates table instances within our namespace only.
Toolbar::getInstance()
The tutorial code uses Toolbar::getInstance() to get a Toolbar instance onto which it can attach the batch button in the helloworlds form. The Toolbar API documentation seems to suggest that we should replace
$bar = Toolbar::getInstance('toolbar');
with
$bar = Factory::getContainer()->get(ToolbarFactoryInterface::class)->createToolbar('toolbar');
However, this currently won't work. The problem is that all the other Joomla code uses getInstance('toolbar') to access the Toolbar class, including the code in administrator/modules/mod_toolbar/mod_toolbar.php which renders the toolbar. The getInstance method keeps track of the Toolbar instances in a local static variable $instances, and returns the same Toolbar instance on repeated invocations.
So if you use the second approach above you will get a Toolbar instance, but it won't be the same Toolbar instance as is stored in the $instances variable. It won't then be picked up by the Toolbar module, and so won't be rendered on the form.
Error Handling[edit]
Instead of
JError::raiseError(500, implode('<br />', $errors));
you should throw an exception, using or extending one of the standard PHP exceptions, eg
throw new \Exception(implode("\n", $errors), 500);
Also the function setError
is no longer available within Controller classes, as these now don't have CMSObject
in the inheritance chain. (The function is still available to Model classes). Hence instead of:
$this->setError(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()));
$this->setMessage($this->getError(), 'error');
the code is now:
$this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error');
jimport[edit]
jimport is no longer necessary in order to get access to Path or File classes. All of the Joomla classes are found by the autoloader.
[edit]
JHtml::_('behavior.framework');
In general, the use of JHtml::_('behavior.xxx') is now obsolete, and calls using this have been removed in the tutorial code. The 'behavior.framework' call referred to including jQuery. Where jQuery is necessary it has now been included as a dependency in an entry within the helloworld joomla.asset.json file.
JHtml::_('behavior.formvalidator');
This has been replaced by the form.validate javascript asset, and where necessary it has now been included as a dependency in an entry within the helloworld joomla.asset.json file.
It can also be included directly using the web asset manager:
$wa->useScript('form.validate')
JHtml::_('behavior.tooltip')
This no longer exists, and tooltips don't seem to be enabled by default in Joomla 4. This is replaced by the code in media/js/enable-tooltips.js.
JHtml::_('formbehavior.chosen')
The chosen js library seems to be no longer used, and using it will cause formatting issues if you include it in your tmpl files.
sortable
The sortable js library provided the ability in the helloworlds view to drag rows up and down to change the ordering of the records. This has been replaced in Joomla 4 by the draggable library. To enable it you have to set:
- HTMLHelper::_('draggablelist.draggable');
- on your <tbody> element define the table in which dragging occurs and the URL of the Ajax call to set the ordering:
<tbody<?php if ($saveOrder) : ?> class="js-draggable" data-url="<?php echo $saveOrderingUrl; ?>" data-direction="<?php echo strtolower($listDirn); ?>" data-nested="true"
<?php endif; ?>>
- on your tr element within your <tbody> define the group of records in which dragging occurs (by category below):
<tr class="row<?php echo $i % 2; ?>" data-draggable-group="<?php echo $row->catid; ?>">
HTML Snippets[edit]
There are two places in the tutorial where reusable snippets of HTML have beeen defined:
- For displaying the item associations in the helloworlds view:
- For displaying the position latitude and longitude fields within the batch options in the helloworlds view
These used different approaches:
- the item associations display was coded in an html helper file in admin/helpers/html/helloworlds.php and accessed via
echo JHtml::_('helloworlds.association', $row->id);
in the helloworlds tmpl file. (Although it did use a common joomla layout file underneath).
- the position field was coded as a layout in layouts/position.php, and rendered using
echo JLayoutHelper::render('position', array());
When should you use one approach rather than the other? A significant difference is that layouts are designed so that an administrator can override them, via the Templates / Details and File functionality. So, for example, an administrator could change the position to show the longitude before the latitude. So it's more appropriate for layouts to focus on the html elements rather than functional logic, and particularly if you would want to allow an administrator to override the layout.
The layouts functionality in Joomla 4 hasn't changed significantly. One correction which has been made in the updated tutorial code has been to include the position.php file in a subfolder of layouts, ie in admin/layouts/helloworld/position.php rather than admin/layouts/position.php. It appears that in the Templates / Details and Files / Create Overrides tab the com_helloworld layout appears only if it's in a subfolder of /layouts.
The JHtml::_() approach has changed in Joomla 4. Rather than Joomla having to look for a helper file when it encounters something it doesn't recognise, it's expected that you should register such classes beforehand, and you do this by obtaining an instance of the Joomla\CMS\HTML\Registry class (it's a singleton class) and then calling register on it, passing
- the key you want to use
- an instance of your class
The Joomla pattern is to put the code in a file admin/src/Service/HTML/AdministratorService.php, and we have to ensure that our key won't be used elsewhere within Joomla, so in the tutorial we've used:
use Robbie\Component\Helloworld\Administrator\Service\HTML\AdministratorService; ... register('helloworldadministrator', new AdministratorService)
Then when we call:
echo HTMLHelper::_('helloworldadministrator.association', $row->id);
it will result in Joomla calling the association method in our class, passing the $row->id parameter.
We should get the Joomla\CMS\HTML\Registry class from the Dependency Injection container. Joomla components generally do this via their services/provider.php file, and then call register within the extension's boot function, described above.
The extension uses HTMLRegistryAwareTrait which contains setRegistry and getRegistry methods. In services/provider.php there is:
use Joomla\CMS\HTML\Registry; ... $component->setRegistry($container->get(Registry::class));
which gets 'Joomla\CMS\HTML\Registry' out of the DI container and stores it within the
private $registry;
variable of the HTMLRegistryAwareTrait included in the extension.
In the boot function it calls
$this->getRegistry()->register('helloworldadministrator', new AdministratorService);
which gets the registry and calls register as required.
Note that boot is called whenever Joomla dispatches com_helloworld, both for the site and administrator, so it would make sense to check the Application isClient before deciding whether to register the HTML Helper service or not. This hasn't been included in the tutorial code though.
User Interface Changes[edit]
Joomla 4 introduced new templates, atum on administrator and cassiopeia on site, and in particular the administrator is built upon some UI rearrangements which affect the tutorial code.
Sidebar[edit]
In Joomla 3 each component incorporated a sidebar which we coded in the addSubmenu function in admin/helpers/helloworld.php.
In Joomla 4 the administrator menu is in a vertical panel on the left hand side, and the Hello World submenu is built from entries in the helloworld.xml manifest file.
<administration> <menu link="option=com_helloworld">COM_HELLOWORLD_MENU</menu> <submenu> <menu link="option=com_helloworld">COM_HELLOWORLD_MENU</menu> <menu link="option=com_categories&extension=com_helloworld">COM_HELLOWORLD_CATEGORIES</menu> <menu link="option=com_fields&view=fields&context=com_helloworld.helloworld">MOD_MENU_FIELDS</menu> <menu link="option=com_fields&view=groups&context=com_helloworld.helloworld">MOD_MENU_FIELDS_GROUP</menu> </submenu>
CSS Classes[edit]
In several places the CSS classes defined in the html elements have been changed in Joomla 4. For example, for general layout the classes now align with the Bootstrap CSS Classes, and classes such as row-fluid and span3 which were prominent in Joomla 3 have disappeared. If you're upgrading a Joomla 3 component then it's probably best (on the back-end at least) to find a similar Joomla component and use the overall html structure and css classes within its tmpl files.
In the tutorial code those classes which are specifically used by Bootstrap have been changed from eg 'data-target' to 'data-bs-target', and it has been necessary to change these to enable the functionality to work.
However, not all of CSS class changes affecting solely the presentation have been reflected in the updated tutorial code. For example, in the helloworlds view, showing the list of helloworld messages, the display of the associations is different from that of other Joomla components.
Fonts[edit]
In Joomla 3 Icomoon Fonts were used.
In Joomla 4 Font Awesome's free icons are used, and you can see what is available on their cheatsheet. The icons are implemented using CSS pseudo-elements, as described here. Basically they convert a CSS class into html content, namely a unicode value in the private use range. A couple of these have been implemented in the frontend form, in the Save and Cancel buttons, for example:
span class="icon-cancel">/span <?php echo Text::_('JCANCEL') ?>
(with angle brackets removed from the span tags to stop the icon just being displayed by the browser).
This class is defined in media/templates/administrator/atum/css/vendor/fontawesome-free/fontawesome.css (where you can find the list of icon classes supported):
.icon-cancel:before { content: "\f00d"; }
so this includes in front of the text the unicode character f00d, which you can see on the cheatsheet as "times", and displayed as . You can also use fa- plus the icon name on the font awesome cheatsheet page. If you use Icomoon icon names in your code which you run under Joomla 4, then the icon displayed will be a Font Awesome icon (if there's one with the same name) or it will use a mapping which the Joomla team have defined to display a Font Awesome icon which resembles the Icomoon one.
Buttons and Links[edit]
An accessibility principle is that <a> html elements should be used for links to other web pages, and <button> elements should be used for actions, eg on a form. To align with this, the <a> elements in the helloworld modal field (defined in admin/src/Field/Modal/Helloworld.php) have been changed to <button> elements. These are the Select and Clear buttons displayed when choosing a Helloworld record for a menuitem pointing to a Helloworld message. (The buttons also include fontawesome icons, by the way).
This change was actually made in Joomla in one of the Joomla 3 releases.
Categories View[edit]
In the Categories view of standard Joomla components there now appears columns which show for each category the number of items, split by published status. This is populated by com_categories calling each extension's countItems function, as described above.
Miscellaneous Changes[edit]
Version History[edit]
In Joomla 3 the tutorial code used the JTableObserver functionality to copy relevant changes from the Helloworld and Categories tables into the UCM tables.
In Joomla 4 it uses a plugin mechanism to subscribe to Table events 'onTableAfterStore' and 'onTableBeforeDelete'. The plugin can be found in plugins/behaviour/versionable, and it is activated by Joomla as part of its application initialisation.
The copying of fields from the table works roughly the same way as before, except that the name of the table where the versions are stored is now #__history, so the entries which have to be inserted into the #__content_types table are fairly similar. Because the folders in which forms are stored has changed in Joomla 4, the 'formFile' parameter is different (remember that this is where Joomla gets the field labels in order to display the Preview within the Versions modal window, otherwise just the names of the database columns will be output).
Here's a summary of what needs to be done to enable versioning under Joomla 4. The Versioning Traits and Interface definitions are found in libraries/src/Versioning:
- Enter data into content_types database table
Storing the history:
- The Table class must implement VersionableTableInterface and provide getTypeAlias(), which returns 'com_helloworld.helloworld'. This type alias + the helloworld record id is used as the key to identify versions in the #__history table.
- Define save_history and history_limit options in config.php, and ensure these are both set.
- Add a version_note field into the output of the edit view's tmpl file (admin/tmpl/helloworld/edit.php).
Viewing and Restoring the history:
- The edit Controller (admin/src/Controller/HelloworldController.php) should use VersionableControllerTrait.
- The edit Model (admin/src/Model/HelloworldModel.php) should use VersionableModelTrait and include a variable
public $typeAlias = 'com_helloworld.helloworld';
- The edit View (admin/src/View/Helloworld/HtmlView.php) – should display the Versions toolbar button if global config has save_history on.
Joomla 4 allows us to have different history version limits for helloworld items and categories, and this has been used in the configuration definition in admin/config.xml.
Setting Configuration Parameters in Install Script[edit]
As you may remember, global configuration parameters relating to the Hello World component are defined in admin/config.xml and stored in the #__extensions table, in a JSON string in the params field of the record relating to com_helloworld. This means that when the component is first installed the params are not set.
So, for example, even if the save_history is defined in config.xml to default to true, the versions won't be saved until an administrator actively saves the configuration parameters, because the Joomla versioning code assumes save_history to be false if it's not explicitly set.
For this reason it makes sense to set any default configuration parameters in the install script.php, in the postflight method, and that has been done in this step.
Tags[edit]
Like version history, tags functionality has been changed to use a plugin – in plugins/behaviour/taggable – rather than the JTableObserver approach.
To enable tags the component table must implement TaggableTableInterface (in libraries/src/Tag), and provide the getTypeAlias() which is common to the Versioning functionality.
Whenever com_tags outputs helloworld messages or categories relating to a tag it displays them as links, and looks in the site/helpers/route.php file to find the route functions (as described above in com_tags). The class and method in this file must match the 'router' field within the data entered into the #__content_types table.
The language constant JTAG_DESC
(used in the XML form file for tags fields) is no longer available.
setDocument[edit]
In the View code the function setDocument
was called to setup aspects of the HTML Document. However, in Joomla 4.4 they introduced into the class AbstractView (which is in the inheritance chain) a function called setDocument
whose name clashes with the tutorial function. Hence the tutorial code function has been renamed to setupDocument
.
media files[edit]
Under Joomla 4 all of the media files are stored in the /media folder; these include javascript and css files (scss files too), as well as images such as the little flag symbols shown in the language switcher. Extensions being developed should store all of the js and css files in their /media folder, and then when the extension is installed these files are put under the extension's folder under the /media folder of the Joomla instance.
Patches[edit]
Some bugs in Joomla were discovered during the course of updating the tutorial code to work under Joomla 4. If you find something doesn't work then make sure you're on at least Joomla 4.2.7 and check if you need one of the following patches. The two issues relating to categories were fixed by Joomla 4.2.7.
CategoryAssociationHelper.php
https://github.com/joomla/joomla-cms/pull/39358/files
MediaField.php
https://github.com/joomla/joomla-cms/issues/39203
Category restore
https://github.com/joomla/joomla-cms/pull/39474