From 0de3ef79329a7f2d0d1ef8a73d50917b35143223 Mon Sep 17 00:00:00 2001 From: Robbie Jackson Date: Sun, 19 Apr 2026 20:06:47 +0100 Subject: [PATCH 1/2] lazy plugin general documentation --- .../plugins/basic-content-plugin.md | 48 ++---- .../plugins/how-plugins-work.md | 6 +- .../plugins/methods-and-arguments.md | 143 +++++++++++++++++- .../plugin-examples/_assets/plg_ajax_jobs.zip | Bin 1989 -> 1942 bytes .../plugins/plugin-examples/ajax-plugin.md | 20 ++- .../dependency-injection/DIC.md | 20 ++- 6 files changed, 188 insertions(+), 49 deletions(-) diff --git a/docs/building-extensions/plugins/basic-content-plugin.md b/docs/building-extensions/plugins/basic-content-plugin.md index bad37b2a7..b8089402b 100644 --- a/docs/building-extensions/plugins/basic-content-plugin.md +++ b/docs/building-extensions/plugins/basic-content-plugin.md @@ -128,6 +128,8 @@ Ensure that your plugin language files are named correctly. You must include in ## Service Provider file +In this example we implement the plugin class as a [lazy object](./methods-and-arguments.md#lazy-objects). + ```php title="plg_shortcodes/services/provider.php" set( PluginInterface::class, - function (Container $container) { - - $config = (array) PluginHelper::getPlugin('content', 'shortcodes'); - $subject = $container->get(DispatcherInterface::class); - $app = Factory::getApplication(); + $container->lazy(Shortcode::class, + function (Container $container) { + $config = (array) PluginHelper::getPlugin('content', 'shortcodes'); + $app = Factory::getApplication(); + $plugin = new Shortcode($config); + $plugin->setApplication($app); - $plugin = new Shortcode($subject, $config); - $plugin->setApplication($app); - - return $plugin; - } + return $plugin; + } + ); ); } }; @@ -176,32 +176,14 @@ $config = (array) PluginHelper::getPlugin('content', 'shortcodes'); Ensure that this contains your plugin type and element, matching the manifest file. ```php -$plugin = new Shortcode($subject, $config); +$plugin = new Shortcode($config); ``` Ensure that this matches your class in your `src/Extension` directory. -:::tip -Since Joomla 6.1 it is possible to use [PHP 8.4 Lazy Objects](https://www.php.net/manual/en/language.oop5.lazy-objects.php) for Plugins. This helps to improve overall CMS performance. - -To do so, update service provider to use `$container->lazy(, )` as in following example: - -```php -return new class() implements ServiceProviderInterface -{ - public function register(Container $container) - { - $container->set( - PluginInterface::class, - $container->lazy(Shortcode::class, function (Container $container) { - // ... existing code - }) - ); - } -}; -``` - -::: +The plugin uses [lazy instantiation](../methods-and-arguments/#lazy-objects), +which means that the plugin's Extension class will be instantiated +only if one of the events to which it subscribes is triggered. ### Extension Class This is the main code of the plugin. Hopefully the comments in the code explain what is going on. diff --git a/docs/building-extensions/plugins/how-plugins-work.md b/docs/building-extensions/plugins/how-plugins-work.md index 27c8f159c..53cf0f062 100644 --- a/docs/building-extensions/plugins/how-plugins-work.md +++ b/docs/building-extensions/plugins/how-plugins-work.md @@ -38,7 +38,7 @@ For example, after Joomla is initialised the system plugins are imported (ie tho Importing a plugin type involves: - finding from the database all the plugins associated with a particular type - the type matches the associated subfolder of the `/plugins` folder in the file system. In the diagram these plugins are shown as `plugin1`, `plugin2` and `plugin3`, -- instantiating each of these plugins, +- instantiating each of these plugins (in specific terms, executing the services/provider.php file of each plugin) - determining which events each plugin wants to subscribe to. - writing the subscriptions to a data store represented by the `Listeners` box in the diagram. @@ -60,7 +60,7 @@ Once a plugin has been imported and its subscriptions logged in the `Listeners` The system plugins imported in the first step can subscribe to the `onContentPrepare` event ok, even though it's associated with content plugins, and will receive the `onContentPrepare` event if they have subscribed to it, without having to be imported again. -You should also be aware that unlike components and modules, there aren't different site plugins and administrator plugins. The plugins which you install are run for all the different contexts - site, administrator and API applications - so it's a good idea to in your plugins to check the application context. +You should also be aware that, unlike components and modules, there aren't different site plugins and administrator plugins. The plugins which you install are run for all the different contexts - site, administrator and API applications - so it's a good idea to in your plugins to check the application context. ## Sequence Diagram @@ -74,7 +74,7 @@ sequenceDiagram Note over PluginHelper, Plugins Table: Only once at runtime, and
result is cached in memory Plugins Table-->>PluginHelper: Return all active PluginHelper-->>Dispatcher: Import only
group of $type
(once at runtime) - PluginHelper->>Plugin: Instantiate plugin + PluginHelper->>Plugin: Execute each plugin's service/provider.php Dispatcher->>Plugin: Call $plugin->getSubscribedEvents()
for each plugin in group Plugin->>Dispatcher: Return list of listeners Code->>Dispatcher: dispatch('onFooBar',
$eventObject) diff --git a/docs/building-extensions/plugins/methods-and-arguments.md b/docs/building-extensions/plugins/methods-and-arguments.md index 07e255e17..eed34f39e 100644 --- a/docs/building-extensions/plugins/methods-and-arguments.md +++ b/docs/building-extensions/plugins/methods-and-arguments.md @@ -13,7 +13,7 @@ There are 2 functions which you must implement within your plugin: 1. The `getSubscribedEvents()` method. You tell Joomla - which events you want to subscribe to - - the code to run when one of those events is triggered + - the code (handler function) to run when one of those events is triggered 2. The handler function which Joomla should call when one of those events is triggered. @@ -218,6 +218,147 @@ public function onInstallerBeforeInstaller(\Joomla\CMS\Event\Installer\BeforeIns } ``` +## Instantiating your Plugin Class + +You should instantiate your plugin in your services/provider.php file. +Here is an example of such a file which would instantiate the shortcodes plugin in the [plugin tutorial](./basic-content-plugin.md) + +```php title="plg_shortcodes/services/provider.php" +set( + PluginInterface::class, + function (Container $container) { + + $config = (array) PluginHelper::getPlugin('content', 'shortcodes'); + $plugin = new Shortcode($config); + + return $plugin; + } + ); + } + }; +``` + +This `register()` function is called whenever Joomla [imports the plugin](./how-plugins-work.md#importing-a-plugin). + +The code creates an entry in the [Dependency Injection container](../../../general-concepts/dependency-injection/DIC.md), +specifically in this plugin's [child container](../../../general-concepts/dependency-injection/extension-child-containers.md). + +The entry is then read from the DIC, and this results in running the `function (Container $container) { ... }`, +which instantiates the plugin. + +### Lazy Objects + +Since version 8.4 PHP supports the concept of [lazy objects](https://www.php.net/manual/en/language.oop5.lazy-objects.php), +where the object is instantiated only whenever one of its properties is accessed. + +This is particularly useful for plugins because only a subset of plugins are actually triggered in handling an HTTP request. +If plugins are implemented as lazy objects then this avoids many of them being instantiated unnecessarily, +because they're instantiated only if one of their handler functions is called. + +(Note that calling the plugin's `getSubscribedEvents()` function does not cause the lazy object to be instantiated because it's a static function). + +To define as plugin as a lazy object you can use the Dependency Injection Container's `lazy()` function: + +```php +$callable = $container->lazy(Shortcode::class, + function (Container $container) { + $config = (array) PluginHelper::getPlugin('content', 'shortcodes'); + $plugin = new Shortcode($config); + return $plugin; + }); +``` + +You pass 2 parameters: + +1. Shortcode::class - really just a shorthand for the string of the class's FQN: "My\Plugin\Content\Shortcodes\Extension\Shortcode" + +2. The initializer of the class - the function which instantiates the plugin + +The returned `$callable` is what you then set in the DIC: + +```php +$container->set(PluginInterface::class, $callable); +``` + +When the entry is retrieved from the DIC (which happens when the plugin is imported) then the `$callable` is run, +which is the `lazy()` function above, and this creates the lazy proxy. + +When a property of the Shortcode::class object is accessed (ie when one of the plugin handler functions is called), +then PHP runs the initializer function (2nd parameter of the `lazy()` function), +and this instantiates the plugin object. + +### Performance Considerations + +If you use lazy plugins and your plugin is not triggered +then the time that you save is the time which would have been taken to instantiate your plugin +(specifically the plugin Extension class instance): + +- allocation of memory for your plugin and its properties + +- executing of your plugin's constructor (by default, the constructor in CMSPlugin). + +Your plugin code is still read and processed by PHP, whenever Joomla imports plugins of that type. + +However the use of lazy objects does have some overhead - PHP needs to instantiate a ReflectionClass, +but as this is a standard PHP class its instantiation will be optimised. + +So the key question to answer is this: + +**Once my plugin's type is imported, how likely is it that one of my plugin's events will be triggered?** + +If it's highly likely then don't use lazy instantiation, otherwise do use it. + +For example, if your plugin is just listening for onContentPrepareForm +then you should definitely use lazy objects because content plugins are imported for any page displaying an article, contact, etc, +but most pages will not display a form. + +On the other hand, the system onAfterInitialise event is dispatched pretty much for every HTTP request, +so if your plugin is a system plugin listening for this event then you should definitely not use a lazy object. +If you use a lazy object here then not only will your plugin be instantiated anyway, +but also you will have the overhead of instantiating ReflectionClass as well, so you will actually slow your site down. + +Console plugins are imported only if Joomla is running a console job, +and in this case the \Joomla\Application\ApplicationEvents::BEFORE_EXECUTE event is always dispatched, +so for best performance don't use lazy plugins for console jobs. + +[Ajax plugins](./plugin-examples/ajax-plugin.md) are imported when there is an Ajax request to com_ajax, +but if your plugin is likely to be handling only one of several com_ajax requests, +it makes sense to use lazy instantiation for your Ajax plugins. + +If in doubt, you're probably best to use lazy plugins by default. + +If your plugin is, for example, only appropriate to the administrator functionality, +then you can also check the application context in the static `getSubscribedEvents` function, +and register for events only if the context is 'administrator': + +```php +public static function getSubscribedEvents(): array +{ + if (\Joomla\CMS\Factory::getApplication()->isClient('administrator')) { + return [ + 'onModuleRender' => 'checkAdminModule', + ]; + } else { + return []; + } +} +``` + +This enhances the performance of your site, +because your plugin won't be instantiated for HTTP requests to your "site" application. + ## Methods available to Plugins Inside your plugin you have access to several methods. diff --git a/docs/building-extensions/plugins/plugin-examples/_assets/plg_ajax_jobs.zip b/docs/building-extensions/plugins/plugin-examples/_assets/plg_ajax_jobs.zip index 9102c5ba8e9a12e47cd50d4bd788a6f077956ae8..255105b7ad3902a4229c42c803866cfc57917033 100644 GIT binary patch delta 484 zcmX@gKaGEb9U}`*<>Z*jPK<`UJT;SJ@`4@f^%xl#3MV%(YSs4!oz81E5ZL2>%Kptl zAvdpjhF<|w!?KiIyQXN1x9eS*xN&0cr3+gB<7T@%DP+!MHhc5t4bQj3#s6mdt6%n9 zC^#)BZI0XLt7=u&5+bua1=m(iG}`omBm;ag}{~=(b<__10A@*Ep z(w&pSyDq<=nV)@310h%a?`Kl1&P(!S!|ua$DJ7 zoIP+m^7lOXzANWf=dFIvcTH9O?1^Zu$0f6GUpmomQ2b~1qxfmNpUpa#{DV8do0&y~ z0R+H-D$1+_4&BYJ%q+~1aQ0<$L<{HHY%U5Q)nIKbza0|Cq_fw(&a@Fl`T`Z#xgQ6oSNLgs8wGYbh_)1fyiC%f6ije zz6(DyzqJu4m0)!&xb1r;CVPpm)1OcFDmlDs6J@8_q7>Y-l~P_-{n_-gj4@*9l)|OmS}1 z6m^%W6Fl2_&BA5P1WS=`V!bufq~67UE>b*Qdrre@${eZc_NixQY)h!o7Tv=F2h< z8g8($3v&5wTkSZb;!DX+dq&f+4e^Vn9#o#3)BUMSP;+g-?$)g{XHMDn_sG@mT9f`6 zYhG`t%45Bg_2692m8}P}mpr_-_?2}5oAo)1&3AW&CAj?iRdJ>K!m@QI?E}1-Swt8> z02Bvc29yv2(git_e=te0Y*set( PluginInterface::class, - function (Container $container) { - - $config = (array) PluginHelper::getPlugin('ajax', 'jobs'); - $subject = $container->get(DispatcherInterface::class); - $app = Factory::getApplication(); + $container->lazy(Jobs::class, + function (Container $container) { - $plugin = new Jobs($subject, $config); - $plugin->setApplication($app); - - return $plugin; - } + $config = (array) PluginHelper::getPlugin('ajax', 'jobs'); + $plugin = new Jobs($config); + + return $plugin; + } + ) ); } }; diff --git a/docs/general-concepts/dependency-injection/DIC.md b/docs/general-concepts/dependency-injection/DIC.md index 5216295ac..e9c3335ec 100644 --- a/docs/general-concepts/dependency-injection/DIC.md +++ b/docs/general-concepts/dependency-injection/DIC.md @@ -28,4 +28,22 @@ You get things out of the container by calling `get()` passing the key of the re - 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. \ No newline at end of file +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. + +## Lazy Objects + +Since version 8.4 PHP supports the concept of [lazy objects](https://www.php.net/manual/en/language.oop5.lazy-objects.php), +and you can store entries in the DIC to create lazy objects. + +The container [`lazy()`](framework-api://classes/Joomla-DI-Container.html#method_lazy) function +provides a mechanism for storing in the DIC an entry which creates a PHP lazy proxy object. + +You pass the name of your class and the initializer function to instantiate your class (as described in the PHP manual). + +It returns the callable which you then `set` in the DIC. + +This is particularly useful for plugins, because in handling a particular HTTP request many of them will not get triggered. +If they are implemented as lazy objects then this avoids the expense of instantiating them, +hence improving the site performance. + +See [Instantiating your Plugin Class - Lazy Objects](../../building-extensions/plugins/methods-and-arguments.md#lazy-objects) for more details. From 6bf61d06226fd90eee18b95373223fcdcc806a98 Mon Sep 17 00:00:00 2001 From: Robbie Jackson Date: Sun, 19 Apr 2026 20:37:28 +0100 Subject: [PATCH 2/2] fix broken links --- docs/building-extensions/plugins/methods-and-arguments.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/building-extensions/plugins/methods-and-arguments.md b/docs/building-extensions/plugins/methods-and-arguments.md index eed34f39e..77468d572 100644 --- a/docs/building-extensions/plugins/methods-and-arguments.md +++ b/docs/building-extensions/plugins/methods-and-arguments.md @@ -252,8 +252,8 @@ use My\Plugin\Content\Shortcodes\Extension\Shortcode; This `register()` function is called whenever Joomla [imports the plugin](./how-plugins-work.md#importing-a-plugin). -The code creates an entry in the [Dependency Injection container](../../../general-concepts/dependency-injection/DIC.md), -specifically in this plugin's [child container](../../../general-concepts/dependency-injection/extension-child-containers.md). +The code creates an entry in the [Dependency Injection container](../../general-concepts/dependency-injection/DIC.md), +specifically in this plugin's [child container](../../general-concepts/dependency-injection/extension-child-containers.md). The entry is then read from the DIC, and this results in running the `function (Container $container) { ... }`, which instantiates the plugin.