The bulk of the implementation logic for Joomla 4 MVC Controllers is the same as in Joomla 3 MVC.
The logic is that most views are displayed using the component's
DisplayController
which extends from Joomla's
Joomla\CMS\MVC\Controller\BaseController
. You only write
custom controllers when there's an action needed or when you need access
to a Form object.
Tip | |
---|---|
The kind of MVC practiced by Joomla and described above is a bit “dirty”; all views share a common DisplayController for displaying. This is fine with Joomla's one-trick-pony components, only able to display one kind of information. When you have something more complex this may not be at all convenient. For example, a document management system might need separate views to display nested categories, available items (including the actual downloading part!) and uploading items, as well as viewing, adding and managing comments per document. In these cases it's typically much more efficient to have a separate controller per view. In components like that each view consists of a Controller, Model and View class (an MVC triad) which is more in line with how the normative MVC coding pattern is meant to work. This is perfectly possible in Joomla as long as you have a custom Dispatcher. You can see how I am doing that in the Dispatcher of Akeeba Release System, the software downloads management software used by Joomla's official Downloads site. |
The Controller classes extend from one of the base Joomla MVC Controller super-classes:
- Joomla\CMS\MVC\Controller\BaseController
-
The most basic controller you can get. It is mainly used to power the
DisplayController
of the component or whenever you want to create custom controllers to display stuff and perform non-administrative actions (e.g. in the frontend).Warning Heads up! In Joomla 3 the default display controller was in the root of the component as the file
controller.php
e.g.administrator/components/com_example/controller.php
. This is no longer the case in Joomla 4 and later. All controllers are under thesrc/Controller
folder and the default controller is namedDisplayController
which means that in the above example it would be located inadministrator/components/com_example/srs/Controller/DisplayController.php
.In Joomla 4 and later the
DisplayController
is mostly empty except for a line like this:protected $default_view = 'foobar';
This is the name of the “default view”. That's the view which will be displayed if the
view
URL parameter is not provided in the request. - \Joomla\CMS\MVC\Controller\AdminController
-
This is an extension to BaseController. It is meant for displaying list views in the backend of your site, e.g. a list of articles.
Tip This is the controller you will be typically using for controllers whose name is plural such as
\Acme\Component\Example\Administrator\Controller\ItemsController
. Since this is the Items controller and “items” is plural it gives us a hint that it will be displaying multiple items which, in the Joomla backend, is very likely to be a list.A mnemonic way to remember this is PASiFIC (yes, with the misspelling and all) "Plural Admin Singular Form Identifies the Controller".
It also handles some of the admin tasks you will have to perform on records in a Joomla list view: publish, unpulish, archive, trash, report, orderup, orderdown, delete, reorder, saveorder, checkin, checkout, saveOrderAjax and runTransition. Everything else has to be handled by a singular named Controller which extends FormController. Yes, that's confusing and yes, it's the source of many bugs — but that's the same as Joomla 3 and earlier so at least it's consistently confusing.
- \Joomla\CMS\MVC\Controller\FormController
-
This is an extension to the BaseController which additionally implements Form handling. This is meant to be used in views which add or edit a database record. Additionally, it will be used in views which handle an XML Form e.g. a configuration page.
Tip This is the controller you will be typically using for controllers whose name is singular such as
\Acme\Component\Example\Administrator\Controller\ItemController
. Since this is the Item controller and “item” is singular it gives us a hint that it will be displaying a single item which, in the Joomla backend, is very likely to be an add / edit record page.In the frontend of the site you may have two controllers for the same data type. One DisplayController extending from BaseController to display the item and one singular name controller extending from FormController to add or edit an item. It is possible to have a single controller, extending from FormController. You see, FormController itself extends from BaseController which has the display method which is used to display stuff, i.e. it already contains the functionality of the typical DisplayController.
- \Joomla\CMS\MVC\Controller\ApiController
-
This is a very special kind of controller which is only used in the JSON API application part of your component e.g. the code under
api/components/com_example
.This type of controller does not have a direct equivalent in the other controllers. It can do everything. Produce a list of records, return a single record, create a new record and modify or delete an existing record.
A Controller object created by Joomla has a few basic services. In
the olden days before Joomla 4 we had to use static methods in
\Joomla\CMS\Factory
(formerly JFactory
) or the
base MVC objects themselves to get access to these services. You are no
longer supposed to do that, you are supposed to use the services
provided in the Controller object itself.
Application
In Joomla 3 and earlier we would get the currently active
application object through
\Joomla\CMS\Factory::getApplication()
. Do NOT do
that.
In Joomla 4 and later you can get the currently active application
object through $this->app
.
Please remember that the application you are getting is not
necessarily a SiteApplication
or
AdministratorApplication
. It may very well be an
ApiApplication
(JSON API), a
ConsoleApplication
(Joomla CLI app) or even a custom
application extending from
\Joomla\CMS\Application\CMSApplication
.
Dispatcher
In Joomla 3 and earlier you'd run plugin events by doing something like this:
$results = \Joomla\CMS\Factory::getApplication() ->triggerEvent('onSomething', [$param1, $param2]);
In Joomla 4 and later there are two ways to call plugin events.
The first method is the legacy method, going through the
application's triggerEvent
method, which is discouraged and
will eventually go away.
$results = $this->app->triggerEvent('onSomething', [$param1, $param2]);
The second and recommended method is going through the Joomla events dispatcher and using real events.
$event = new \Joomla\Event\Event('onSomething', [$param1, $param2]); $this->getDispatcher()->dispatch($event->getName(), $event); $results = $event->getArgument('result', []);
While this looks a bit more complicated it has some benefits.
Using Events to access plugins means that you get access to a concrete event object. All core Joomla events will correspond to concrete event objects in Joomla 5.0, meaning that you will be able to add typehinting and have your IDE (e.g. phpStorm, Visual Studio Code, NetBeans, Eclipse, ...) auto-complete the argument names when manipulating an event.
The real Events implemented in Joomla 4 and later, unlike plain old plugin handlers we had in Joomla 1.0 to 3.10, can prevent some of their arguments to be modified, prescribe exactly what can be modified and even allow plugins to stop the processing of an event when we reach a point that further processing is unnecessary. If you slowly move from just using custom event names to concrete event classes you can implement far more complex features in your code with much less code — believe me, I've been there and done that! Event handling is one of those fundamental things which you are really upset it's changed but once you start getting the idea of how the new system works you start wondering how you could have ever written software without it.
Input
Back in the olden days we'd get the user's input by doing one of these (the more of these you remember the older you are — some date back to the early 00s!):
// Joomla 1.0 $foo = \JRequest::getCmd('foo'); // Joomla 1.5 $input = new \Joomla\CMS\Input\Input(); $foo = $input->getCmd('foo'); // Joomla 1.6 $foo = Joomla\CMS\Factory::getApplication()->input->getCmd('foo');
This had always been a bad idea because our Controller had to know about the global application object and its request variables.
Since Joomla 3.0 the Controller object has an input
property which holds its own \Joomla\CMS\Input\Input
object. In Joomla 3 this was typically just the application's input
object unless you were manually constructing a controller (with a lot of
effort).
In Joomla 4.0 and later the input property contains its own \Joomla\CMS\Input\Input object which comes from the DI Container of the component. Remember that when booting a component we get access to its component extension object which has access to the component's DI Container. This of course means that we can add a method to that object to access the DI Container and set a custom input object, thereby implementing HMVC very easily — something that Joomla core maintainers didn't think was possible with the core architecture a few years ago! This means that whenever we want to create a module which displays the same information an existing view of our component already does, albeit in a slightly different format, all we need to do is create a new view template or layout and use HMVC, without having to rewrite our business logic in the module. We can write less code to do more. That's awesome!
That is (one of the many reasons) why you should
always be accessing the input as
$this->input
in your controllers. Now you know.
MVCFactory
Back in Joomla 1.0 to 3.10 creating an MVC object such as a Model, View, Table, or even another Controller from inside our Controller required calling static methods in the base MVC objects or proxy methods in the controller, like so:
$someController = \Joomla\CMS\MVC\Controller\BaseController::getInstance('Some'); $someModel = \Joomla\CMS\MVC\Model\BaseDatabaseModel::getInstance('Some', 'ExampleModel'); // or $someModel = $this->createModel('Some', 'ExampleModel'); $someView = $this->createView('Some', 'ExampleView', 'Html'); $someTable = \Joomla\CMS\Table\Table::getInstance('Some', 'ExampleTable');
There are two things which strike us as suboptimal:
-
There is no consistency in how you create MVC objects. Some are static calls, some are method calls to proxy functions.
-
There is no consistency in the arguments you provide. Creating a controller only needs its name (and requires the magic configuration array key
base_path
if you want to get a controller from a component other than the current one), creating anything else requires us to give it the prefix of the MVC class name (not just the component name).
This is what we'd call “sanity came here to die”.
Joomla 3.10 introduced the MVCFactory for creating models, views and tables and Joomla 4.0 extended it by also letting it create controllers. See how easier and more consistent everything is now:
// Only on Joomla 4.0 and later $someController = $this->getMVCFactory()->createController('Some', 'Administrator'); // Joomla 3.10 and later $someModel = $this->getMVCFactory()->createModel('Some', 'Administrator'); $someView = $this->getMVCFactory()->createView('Some', 'Administrator', 'Html'); $someTable = $this->getMVCFactory()->createTable('Some', 'Administrator'); // Inside the Controller I can still do this (4.0 and later): $someModel = $this->getModel('Some', 'Administrator'); $someView = $this->getView('Some', 'Administrator', 'Html');
First of all, we see that creating MVC objects is now very consistent and easier.
Second, we notice that the $prefix
argument is no
longer something component-specific (such as ExampleModel
)
but simply the side of the application (Site, Administrator or Api) that
we want to get the MVC object from.
Note | |
---|---|
Tables are only available in the Administrator side of the
application. While you can theoretically define different Table
classes in the frontend (Site) or JSON API (Api) side it's not a good
idea as you will very likely run into issues where the “wrong” table
class is being used. As a result, it's advisable to always call
|
Legacy Joomla 3 components still work despite the apparent
backwards compatibility (b/c) break in the getModel
and
getView
methods. Why is that? Well, it's because Joomla 4
uses the \Joomla\CMS\MVC\Factory\LegacyFactory
instead of
\Joomla\CMS\MVC\Factory\MVCFactory
for these components.
The LegacyFactory is aware of the b/c break and acts accordingly so you,
the extensions developer, do not suffer. This is possible because each
component has its own DI Container. So that's another way how using a DI
Container allowed Joomla to create an updated, richer MVC API without
breaking backwards compatibility with Joomla 3 — at least until Joomla
6.0. Neat, huh?
As a developer, you should remember this when you are inside a Controller writing code:
-
If you want to get a Model or View use the controller's
getModel
andgetView
methods. -
If you want to get another
Controller
or aTable
do not instantiate them directly! Always go through the MVCFactory. -
The second argument to all these methods is the application side you are getting an object from. Leave it empty to use the current application side or pass an explicit value. If you leave it empty keep in mind that, for example, your backend controller may be running in the frontend. In this case, do you really want to get the frontend model?