From 39e0f4e7e5dc949934fed0b2d2283cfd5608d0b5 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Fri, 18 Aug 2023 16:19:26 +0300 Subject: [PATCH] Working on modules --- sections/modules.xml | 605 ++++++++++++++++++++++++++++++++++++++++++- sections/plugins.xml | 14 +- 2 files changed, 607 insertions(+), 12 deletions(-) diff --git a/sections/modules.xml b/sections/modules.xml index bc84a95..6ec0f26 100644 --- a/sections/modules.xml +++ b/sections/modules.xml @@ -155,7 +155,7 @@ require_once \Joomla\CMS\Helper\ModuleHelper::getLayoutPath('mod_example', $para The view templates are loaded from your module's tmpl directory. In the example above, the default - view template is default.php. + view template is default.php. If you're wondering where $params came from: this is a \Joomla\Registry\Registry object holding your @@ -212,10 +212,43 @@ require_once \Joomla\CMS\Helper\ModuleHelper::getLayoutPath('mod_example', $para (it runs extract() on the array of data). + + This is the big change from Joomla 3.x and earlier. + + In older versions of Joomla! you could have your module's view + template pull data from the module's helper by + calling the helper's static methods. This is no longer the + case. + + In Joomla! 4 and later it is the Dispatcher which calls the + (non-static!) methods of the helper object. Then, it + pushes the data to the view template. + + Once you understand the difference between pulling data and + pushing data the whole concept of modules under Joomla 4 and later + becomes crystal clear. + + The presence of a Helper class is assumed. All you have to do is - tell Joomla what it's called in your module's service provider file. Of - course, all executable PHP code is namespaced — just like with all - Joomla 4 and later native extensions. + tell Joomla what it's called in your module's service provider file. + + + + Unlike Joomla 3.x and earlier, the module's helper no longer has + static methods. It has regular, non-static methods. When you need to + use the helper in your Dispatcher you will be given an instantiated + object. + + This is deliberate. You cannot push services, like the database + object, to a class (at least not without violating several good + software design principles). You can trivially do so in objects which + implement some of the handy Interfaces and use some of the handy + Traits provided by Joomla. We'll see how that works a bit + later. + + + Of course, all executable PHP code is namespaced — just like with + all Joomla 4 and later native extensions. This may sound like a lot of work, but it's really not that bad. In the past you'd create the XML @@ -305,37 +338,587 @@ require_once \Joomla\CMS\Helper\ModuleHelper::getLayoutPath('mod_example', $para
- Service Locator + Service provider - + As with all native extensions since Joomla 4, modules have a service + provider named provider.php in their + services directory. The purpose of this provide is to + tell Joomla where to find the module's dispatcher, and register the + module's helper as a service. + + It's just a bit of boilerplate code, like this: + + <?php +defined('_JEXEC') || die; + +use Joomla\CMS\Extension\Service\Provider\HelperFactory; +use Joomla\CMS\Extension\Service\Provider\Module; +use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; + +return new class () implements ServiceProviderInterface { + public function register(Container $container) + { + $container->registerServiceProvider(new ModuleDispatcherFactory('\\Acme\\Module\\Example')); + $container->registerServiceProvider(new HelperFactory('\\Acme\\Module\\Example\\Site\\Helper')); + + $container->registerServiceProvider(new Module()); + } +}; + + + There are really two things you need to take note of here. + + The argument to the + Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory's + constructor is the namespace of the module without the Site or + Administrator part. In other words, it is exactly what you put in your XML + manifest. Joomla knows to add the Site (frontend modules) or Administrator + (backend modules) to the namespace, and look for + Dispatcher\Dispatcher under it. In other words, in + the example frontend module we have here, the Dispatcher class is + understood to be + Acme\Module\Example\Site\Dispatcher\Dispatcher. + + + Remember! A module always needs a Dispatcher. + This is where you tell Joomla what data to send to the module's view + template for display. If you were to not implement a Dispatcher you + would have nothing useful to display. + + This is the big change from Joomla 3.x and earlier. It is no + longer possible to pull data in the view template + from the module helper directly. It is always + pushed to the module. + + + The argument to the + Joomla\CMS\Extension\Service\Provider\HelperFactory's + constructor, however, is a fully qualified namespace to the module's + Helper classes. This means that it's the namespace you put in your XML + manifest, plus Site (frontend modules) or Administrator, plus Helper. In + other words, in the example frontend module we have here, the helper + classes' namespace is + Acme\Module\Example\Site\Helper. + + The HelperFactory does a modicum of "magic" when instantiating your + helper classes. It checks which interfaces they implement and pushes the + respective services to them. At the time of this writing, it checks if the + helper class implements the + Joomla\Database\DatabaseAwareInterface + interface and pushes the database object to the instantiated helper object + using the interface's setDatabase method. + + If you want to push custom services to your helpers you need to + subclass the + Joomla\CMS\Extension\Service\Provider\HelperFactory + class and use your custom helper factory instead of Joomla's in your + service provider. Unfortunately, merely extending the service does not + work as expected (there's been a long discussion we've already had in the + Joomla! GitHub Issues about it).
Dispatcher class - + The Dispatcher is the "brains" of the module. Its + getLayoutData method is called every time the + module is rendered. It returns an array with the data which will be + displayed by the view templates of the module. + + A minimal implementation would look like this: + + <?php + +namespace Acme\Module\Example\Site\Dispatcher; + +defined('_JEXEC') || die; + +use Joomla\CMS\Dispatcher\AbstractModuleDispatcher; +use Joomla\CMS\Helper\HelperFactoryAwareInterface; +use Joomla\CMS\Helper\HelperFactoryAwareTrait; + +class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareInterface +{ + use HelperFactoryAwareTrait; + + protected function getLayoutData() + { + $data = parent::getLayoutData(); + + $data['stuff'] = $this->getHelperFactory() + ->getHelper('ExampleHelper') + ->getStuff($data['params'], $this->getApplication()); + + return $data; + } +} + + + Let's focus on the overridden getLayoutData + method. + + We must always call the parent (overridden) method first. This gives + us an array with very important bits of information needed to render the + module: + + + + module. The raw object with the module information + as stored in the database. + + + + app. The Joomla! application object. Do NOT assume + it's always an instance of SiteApplication or + AdministratorApplication; check! + + + + input. The Joomla! input object. Use it wisely. If + you are depending on request parameters for display of your module you + need to consider how this affects caching, and whether having a + component might be a better fit for your use case. + + + + params. A Registry object with the module's + parameters. + + + + template. The name of the Joomla! template used to + render the page. This is equivalent to calling + \Joomla\CMS\Factory::getApplication()->getTemplate(). + + + + This data must always be present for Joomla! to successfully render + a module. Therefore, we need to add to it. That's why + we add a new key to the array before returning it. + + We see that we call + $this->getHelperFactory()->getHelper('ExampleHelper'). + As you might have inferred, this tells Joomla to create an object instance + of the ExampleHelper class in the module's helper namespace. As you may + recall, we registered that namespace in our service provider. In the + example plugin, the class we're asking Joomla to instantiate is + Acme\Module\Example\Site\Helper\ExampleHelper. + + + We have to stress out that this is a major change from Joomla! 3.x + and earlier. We no longer call static methods in the helper class. We + have Joomla! create an object instance of that class and call its + non-static method directly. + + + The method calls also shows a way to pass the module parameters to + your helper object: by passing $data['params']. As noted + above, the data element contains a Registry object with the module + parameters. + + The other thing you see in that method call is how you can pass the + Joomla Application object to your helpers: by sending + $this->app (or $data['app']) as an argument. + This is preferred to going through the + Joomla\CMS\Factory class as it makes Unit Testing + of your module easier. + + Even if you don't do any unit testing, it's always a good idea to + inject dependencies (e.g. sending them as arguments, + or otherwise setting them on the target object) instead of pulling + dependencies by calling static methods. In practice, you will see that + some core Joomla services, such as the UserFactory) are not readily + available for injection, breaking this golden rule. This is something + that's been actively worked on in Joomla!. Eventually, it will be possible + to do dependency injection for all core services. + + Needless to say, you can have more than one data keys for display in + your module. Just remember, the $data array you return will + be extracted into variables. Therefore, you must use array key names which + are valid PHP variable names.
Language files - + Language files in modules on Joomla! 4 and later versions follow the + same concept as all other language files, e.g. for components: they no longer have the language + tag as a prefix. + + In Joomla! 3.x and earlier versions you would have two language + files for English (Great Britain): + + + + en-GB.mod_example.ini for the language + strings you use when rendering the module, and the language strings in + your module's configuration parameters. + + + + en-GB.mod_example.sys.ini for the language + strings used in your XML manifest file for the module's name and + description. + + + + In Joomla! 4 and later versions they are now called: + + + + mod_example.ini + + + + mod_example.sys.ini + + + + Module language files are loaded automatically. You do not need to + load them yourself.
The Helper - + In Joomla 3 and earlier versions the module helper was a class with + static methods. You would call these static methods in the module's entry + point file, or a module view template, directly. This is the concept of + pulling information. + + The big limitation of this concept is that static method calls are + inflexible. If you are trying to write any kind of tests for your view + templates, you will quickly find that the static calls make your code + untestable unless you go to great lengths (create a mock class, create a + stream wrapper, load the PHP code, replace the calls to the helper with + calls to the mock class, and test the temporary file you created in the + stream wrapper). This is why this concept, while easy to use by novice + developers, is no longer considered a good practice — and that applies in + general in programming, not just Joomla! modules. + + As noted earlier, Joomla 4 introduced a different concept, one where + you push information to the view template through the + Dispatcher. This isolates the business logic from the presentation. When + you, or a site integrator, create a view template or view template + override for a module you no longer need to care how + you will get the data you need to display. From your point of view, the + display data is magically there. From a developer's point of view, the + isolation of business logic and presentation means that both are perfectly + testable; the former with Unit Tests, the latter by passing a set of + hard-coded data and inspecting the generated output. + + In practical terms, you no longer have static methods. You have + regular methods. Your class is no longer abstract; it's just a regular + class. + + The simplest helper class you can have is like this: + + <?php + +namespace Acme\Module\Example\Site\Helper; + +defined('_JEXEC') || die; + +class ExampleHelper +{ + public function getStuff(): string + { + return "Hello, world."; + } +} + + In your Dispatcher, you'd call this as + $this->getHelperFactory()->getHelper('ExampleHelper')->getStuff();. + + In practice, you will need access to some database date. In the past + you'd get Joomla's database object by doing something like $db = + \Joomla\CMS\Factory::getDbo();. This is no longer the case. You + will just implement Joomla's + \Joomla\Database\DatabaseAwareInterface in + your class and use Joomla's + \Joomla\Database\DatabaseAwareTrait trait. You then + get the database object with $db = + $this->getDatabase();. + + <?php + +namespace Acme\Module\Example\Site\Helper; + +defined('_JEXEC') || die; + +use Joomla\Database\DatabaseAwareInterface; +use Joomla\Database\DatabaseAwareTrait; + +class ExampleHelper implements DatabaseAwareInterface +{ + use DatabaseAwareTrait; + + public function getStuff(): string + { + $db = $this->getDatabase(); + + // Do some database stuff here. + + return "Hello, world."; + } +} + + + “How does this sorcery work?” you wonder. When you instantiate the + helper object through the helper factory, it checks if your class + implements the DatabaseAwareInterface. If it does, it pushes the Joomla! + database object to the helper object. That's why you can retrieve it + with $this->getDatabase() without having written code to + push it yourself. + + + If you need to push any other object, including the module + parameters, you have to do it with method parameters. We showed that when + talking about the Dispatcher. So let's see how we can pass the module + parameters and the application object to the helper: + + <?php + +namespace Acme\Module\Example\Site\Helper; + +defined('_JEXEC') || die; + +use Joomla\CMS\Application\CMSApplicationInterface; +use Joomla\CMS\Application\SiteApplication; +use Joomla\Database\DatabaseAwareInterface; +use Joomla\Database\DatabaseAwareTrait; +use Joomla\Registry\Registry; + +class ExampleHelper implements DatabaseAwareInterface +{ + use DatabaseAwareTrait; + + public function getStuff(Registry $config, CMSApplicationInterface $app): string + { + if (!$app instanceof SiteApplication) { + return ''; + } + + $db = $this->getDatabase(); + + // Do some database stuff here. + + return sprintf("Hello, %s.", $config->get('name', 'world')); + } +} + + As a reminder, here's how this was called from the + Dispatcher: + + $data['stuff'] = $this->getHelperFactory() + ->getHelper('ExampleHelper') + ->getStuff($data['params'], $this->getApplication()); + + As you can see, it is fairly easy, as long as you remember to + push information to the helper instead of the old way + of having it pull information from static + methods.
View templates - + Modules need to be able to display information in a human-readable + manner, in HTML — this is their only purpose in life. The Helper handles + all the business logic to produce this data, and the Dispatcher pushes it + to our view template which renders it as HTML. + + The view templates are placed in the tmpl + folder of your module. + + The concept is not unlike Components themselves, with a few twists. + The most obvious twist is that a module's view template always produces HTML. The second twist is how we + get our display data: as “magically” appearing variables. + + To refresh your memory, the Dispatcher's + getLayoutData method returns an array. Joomla! + puts that array through PHP's extract() function before loading our view + template. +
+ +
+ Modules and com_ajax + + Sometimes, you need to have your modules return data asynchronously + to the HTML page rendering, using client-side (JavaScript) code. The best + way to do that is by using Joomla's com_ajax + component. + + com_ajax is a special core component built into Joomla. By itself, + it can't do much. Its job is to let non-component, first-class Joomla + extension types (modules, plugins, and templates) handle requests and + return results either as JSON or as any arbitrary format. + + For modules, you would need to load a URL like this: + index.php?option=com_ajax&module=mod_example&method=something&format=raw. + For backend modules you obviously need to use + administrator/index.php. + + The way this works is that com_ajax will create an instance of the + module's DIC (that is to say, use the code in our module's + services/provider.php file), instantiate the helper + that has the name + ModulenameHelper, where + Modulename is the name of our module without + the mod_ prefix (e.g. Example for + mod_example), and call the method + MethodnameAjax where + Methodname is the value of the + method URL parameter. In the example above, it would load the + ExampleHelper helper of mod_example + and call its SomethingAjax method. + + If you use format=raw, one of the following will happen: + + + + If you throw an exception or return a Throwable, it will set the + HTTP status to the throwable's status code and return a body similar + to "RuntimeException:Your throwable's message". + + + + If you return a scalar (string, null, integer, float) it will + return its string representation. + + + + If you return an object or array it will try to first cast it as + an array, then implode() it into a string. This + is quite useless, so please don't do that. + + + + If you use format=json it will try to convert your data to a JSON + representation using Joomla's + Joomla\CMS\Response\JsonResponse class. The + returned JSON object has the following keys: + + + + success + + + Boolean true if your method has neither thrown an exception, + nor returned a Throwable. Boolean false otherwise. + + + + + message + + + Only when success is false. If your method threw an exception, + or returned a Throwable object: the message of that throwable. + Otherwise, this key is not set. + + + + + messages + + + Any enqueued Joomla! application messages. The messages are + categorised by type, therefore they may be returned out of order. + For example: + + messages: { + "warning": [ + "Some warning", + "Another warning" + ], + "error": [ + "The code went belly up. Whoopsie!" + ] +} + + + + + data + + + Only when success is true. The data returned by your + method. + + + + + Practical limitations + + com_ajax does not go through the module's Dispatcher. As a result, + you do not have access to the module parameters, nor can you push any + Joomla! object (such the application object, the input object, etc) to the + Helper. + + If you need any module parameters to be used when handling an AJAX + request you have to send them as part of the request (as GET or POST + parameters), store them in the session, or pass the module's ID as part of + the request (as a GET or POST parameter). Each method has limitations and + security considerations: + + + + Passing configuration as GET or POST parameters is unsafe. Anyone can read them. Passing secrets, + or filesystem paths, is detrimental to security. Moreover, any data + read by this method must be ASSUMED + to have been tampered with by a malicious actor who is trying to hack + your site. Do not make any assumptions about data types, ranges, or + that the data will even make any sense. For these reasons, using this + method is VERY STRONGLY DISCOURAGED. + + + + Storing data in the session does not have the security + considerations of passing data through GET or POST, but you must keep + in mind that a user may try to open several pages at once, each one + with a different instance of your module. This means that the data + read from the session may be inconsistent and irrelevant to the + current page. Depending on what your module does this could have + security or privacy implications. + + + + Passing the numeric module ID is much safer. You can very easily + try to load a module by that ID and determine if it exists, if it's + published, and if the current user should be able to see it (access + level check). You can then load its parameters into a Joomla Registry + object and process data in your code. + + + + Generally speaking, having modules do AJAX is a problematic idea. + While there are very few good use cases where this makes sense (and they + all involve data which is always meant to be public), + the majority of the uses cases I have seen in the real world display a + blatant disregard for security and privacy. If you need to do complex + processing maybe what you are doing does not belong in a module, but a + component. + + And, please, PLEASE, do test your modules with + use cases that involve multiple but differently configured instances of + your module on the same page, or what happens if you open three or four + pages with differently configured instances of your module in very quick + succession (open all pages before the first AJAX request has the chance to + be executed by the browser), ideally with users with different privileges + logged in. You will be surprised at how many problems you will catch this + way.
- Interfacing with your component + Interfacing with your component (HMVC)
diff --git a/sections/plugins.xml b/sections/plugins.xml index ecb1b71..9406d91 100644 --- a/sections/plugins.xml +++ b/sections/plugins.xml @@ -1187,6 +1187,18 @@ implements \Joomla\Database\DatabaseAwareInterface +
+ View Templates + + +
+ +
+ Plugins and com_ajax + + +
+
How NOT to break Joomla with your plugin @@ -1658,7 +1670,7 @@ $results = $event->getArgument('result', []); \Joomla\Event\Event or one of its subclasses. You can place it anywhere in your component, plugin, module, or library as long as you follow PSR-4 and the class can be autoloaded by Joomla's PSR-4 - autoloader. + autoloader. Remember that when you set up the <namespace>