diff --git a/libraries/src/Application/ApplicationAwareInterface.php b/libraries/src/Application/ApplicationAwareInterface.php new file mode 100644 index 0000000000000..342fd3e9062cb --- /dev/null +++ b/libraries/src/Application/ApplicationAwareInterface.php @@ -0,0 +1,29 @@ + + * @license GNU General Public License version 2 or later; see LICENSE + */ + +namespace Joomla\CMS\Application; + +/** + * Interface defining an object aware of the global application object + * + * @since __DEPLOY_VERSION__ + */ +interface ApplicationAwareInterface +{ + /** + * Sets the application to use. + * + * @param CMSApplicationInterface $application The application + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function setApplication(CMSApplicationInterface $application): void; +} diff --git a/libraries/src/Application/ApplicationAwareTrait.php b/libraries/src/Application/ApplicationAwareTrait.php new file mode 100644 index 0000000000000..2bd9740eeac6f --- /dev/null +++ b/libraries/src/Application/ApplicationAwareTrait.php @@ -0,0 +1,53 @@ + + * @license GNU General Public License version 2 or later; see LICENSE + */ + +namespace Joomla\CMS\Application; + +/** + * Defines the trait for an Application Aware Class. + * + * @since __DEPLOY_VERSION__ + */ +trait ApplicationAwareTrait +{ + /** + * The application object + * + * @var CMSApplicationInterface + * + * @since __DEPLOY_VERSION__ + */ + private $application; + + /** + * Sets the application to use. + * + * @param CMSApplicationInterface $application The application + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function setApplication(CMSApplicationInterface $application): void + { + $this->application = $application; + } + + /** + * Returns the internal application or null when not set. + * + * @return CMSApplicationInterface|null + * + * @since __DEPLOY_VERSION__ + */ + protected function getApplication(): ?CMSApplicationInterface + { + return $this->application; + } +} diff --git a/libraries/src/Extension/PluginInterface.php b/libraries/src/Extension/PluginInterface.php index 256bdddd87067..e5093d995b3df 100644 --- a/libraries/src/Extension/PluginInterface.php +++ b/libraries/src/Extension/PluginInterface.php @@ -10,6 +10,7 @@ namespace Joomla\CMS\Extension; use Joomla\Event\DispatcherAwareInterface; +use Joomla\Event\Event; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; @@ -28,6 +29,17 @@ interface PluginInterface extends DispatcherAwareInterface * @return void * * @since 4.0.0 + * @deprecated 5.0 Use SubscriberInterface. This method will be removed in 6.0. */ public function registerListeners(); + + /** + * Initialises the plugin before each event is handled. + * + * Override the doInitialise() method in your class with your initialisation code. + * + * @return void + * @since __DEPLOY_VERSION__ + */ + public function initialisePlugin(Event $e): void; } diff --git a/libraries/src/Plugin/CMSPlugin.php b/libraries/src/Plugin/CMSPlugin.php index 5ce566686e8c9..1f1f46fbb2ad4 100644 --- a/libraries/src/Plugin/CMSPlugin.php +++ b/libraries/src/Plugin/CMSPlugin.php @@ -9,14 +9,14 @@ namespace Joomla\CMS\Plugin; -use Joomla\CMS\Application\CMSApplicationInterface; +use Joomla\CMS\Application\ApplicationAwareInterface; +use Joomla\CMS\Application\ApplicationAwareTrait; use Joomla\CMS\Extension\PluginInterface; -use Joomla\CMS\Factory; -use Joomla\Event\AbstractEvent; use Joomla\Event\DispatcherAwareInterface; use Joomla\Event\DispatcherAwareTrait; use Joomla\Event\DispatcherInterface; -use Joomla\Event\EventInterface; +use Joomla\Event\Event; +use Joomla\Event\Priority; use Joomla\Event\SubscriberInterface; use Joomla\Registry\Registry; @@ -29,9 +29,13 @@ * * @since 1.5 */ -abstract class CMSPlugin implements DispatcherAwareInterface, PluginInterface +abstract class CMSPlugin implements ApplicationAwareInterface, DispatcherAwareInterface, PluginInterface { use DispatcherAwareTrait; + use ApplicationAwareTrait; + use LanguageAwareTrait; + use LegacyPropertiesTrait; + use LegacyListenerTrait; /** * A Registry object holding the parameters for the plugin @@ -47,7 +51,9 @@ abstract class CMSPlugin implements DispatcherAwareInterface, PluginInterface * @var string * @since 1.5 */ + //phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore protected $_name = null; + //phpcs:enable PSR2.Classes.PropertyDeclaration.Underscore /** * The plugin type @@ -55,50 +61,52 @@ abstract class CMSPlugin implements DispatcherAwareInterface, PluginInterface * @var string * @since 1.5 */ + //phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore protected $_type = null; + //phpcs:enable PSR2.Classes.PropertyDeclaration.Underscore /** * Affects constructor behavior. If true, language files will be loaded automatically. * * @var boolean * @since 3.1 + * @deprecated 5.0 Use the LanguageAwareTrait and call loadLanguage() in doInitialise(). */ protected $autoloadLanguage = false; /** - * Should I try to detect and register legacy event listeners, i.e. methods which accept unwrapped arguments? While - * this maintains a great degree of backwards compatibility to Joomla! 3.x-style plugins it is much slower. You are - * advised to implement your plugins using proper Listeners, methods accepting an AbstractEvent as their sole - * parameter, for best performance. Also bear in mind that Joomla! 5.x onwards will only allow proper listeners, - * removing support for legacy Listeners. + * Should we allow "magic" late initialisation of this plugin using the doInitialise code? * - * @var boolean - * @since 4.0.0 - * - * @deprecated + * @var bool + * @since __DEPLOY_VERSION__ */ - protected $allowLegacyListeners = true; + protected $allowLateInitialisation = true; /** - * The application object - * - * @var CMSApplicationInterface + * Flag for the initialisePlugin method. * - * @since 4.2.0 + * @var bool + * @since __DEPLOY_VERSION__ */ - private $application; + private $isPluginInitialised = false; /** - * Constructor + * Constructor. + * + * Do not put any slow initialisation code in the constructor, e.g. code which accesses the database, + * performs lengthy calculations, or calls external services over HTTP. Put that code into the + * doInitialise() method which is called ONCE, before the first event handler in your plugin is + * executed. * * @param DispatcherInterface &$subject The object to observe - * @param array $config An optional associative array of configuration settings. - * Recognized key values include 'name', 'group', 'params', 'language' - * (this list is not meant to be comprehensive). + * @param array $config An optional associative array of configuration + * settings. Recognized key values include 'name', + * 'group', 'params', 'language' + * (this list is not meant to be comprehensive). * * @since 1.5 */ - public function __construct(&$subject, $config = array()) + public function __construct(&$subject, $config = []) { // Get the parameters. if (isset($config['params'])) { @@ -109,267 +117,105 @@ public function __construct(&$subject, $config = array()) } } - // Get the plugin name. - if (isset($config['name'])) { - $this->_name = $config['name']; - } - - // Get the plugin type. - if (isset($config['type'])) { - $this->_type = $config['type']; - } + // Get the plugin name and type + $this->_name = $config['name'] ?? null; + $this->_type = $config['type'] ?? null; // Load the language files if needed. - if ($this->autoloadLanguage) { - $this->loadLanguage(); - } - - if (property_exists($this, 'app')) { - @trigger_error('The application should be injected through setApplication() and requested through getApplication().', E_USER_DEPRECATED); - $reflection = new \ReflectionClass($this); - $appProperty = $reflection->getProperty('app'); + $this->autoloadLanguage = $this->autoloadLanguage && method_exists($this, 'loadLanguage'); - if ($appProperty->isPrivate() === false && \is_null($this->app)) { - $this->app = Factory::getApplication(); - } + if (!$this->allowLateInitialisation && $this->autoloadLanguage) { + $this->loadLanguage(); } - if (property_exists($this, 'db')) { - @trigger_error('The database should be injected through the DatabaseAwareInterface and trait.', E_USER_DEPRECATED); - $reflection = new \ReflectionClass($this); - $dbProperty = $reflection->getProperty('db'); - - if ($dbProperty->isPrivate() === false && \is_null($this->db)) { - $this->db = Factory::getDbo(); + // Look for and populate the legacy $app and $db properties + if (method_exists($this, 'implementLegacyProperties')) { + try { + $this->implementLegacyProperties(); + } catch (\ReflectionException $e) { + // Do nothing; the legacy properties will be null. } } // Set the dispatcher we are to register our listeners with $this->setDispatcher($subject); - } - /** - * Loads the plugin language file - * - * @param string $extension The extension for which a language file should be loaded - * @param string $basePath The basepath to use - * - * @return boolean True, if the file has successfully loaded. - * - * @since 1.5 - */ - public function loadLanguage($extension = '', $basePath = JPATH_ADMINISTRATOR) - { - if (empty($extension)) { - $extension = 'Plg_' . $this->_type . '_' . $this->_name; + // Mark the initialisation code as not yet executed. + if ($this->allowLateInitialisation) { + $this->isPluginInitialised = false; + $this->registerLateInitialisation(); } - - $extension = strtolower($extension); - $lang = $this->getApplication() ? $this->getApplication()->getLanguage() : Factory::getLanguage(); - - // If language already loaded, don't load it again. - if ($lang->getPaths($extension)) { - return true; - } - - return $lang->load($extension, $basePath) - || $lang->load($extension, JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name); } /** - * Registers legacy Listeners to the Dispatcher, emulating how plugins worked under Joomla! 3.x and below. - * - * By default, this method will look for all public methods whose name starts with "on". It will register - * lambda functions (closures) which try to unwrap the arguments of the dispatched Event into method call - * arguments and call your on method. The result will be passed back to the Event into its 'result' - * argument. + * Initialises the plugin before each event is handled. * - * This method additionally supports Joomla\Event\SubscriberInterface and plugins implementing this will be - * registered to the dispatcher as a subscriber. + * Override the doInitialise() method in your class with your initialisation code. * * @return void - * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ - public function registerListeners() + final public function initialisePlugin(Event $e): void { - // Plugins which are SubscriberInterface implementations are handled without legacy layer support - if ($this instanceof SubscriberInterface) { - $this->getDispatcher()->addSubscriber($this); - + if ($this->isPluginInitialised) { return; } - $reflectedObject = new \ReflectionObject($this); - $methods = $reflectedObject->getMethods(\ReflectionMethod::IS_PUBLIC); - - /** @var \ReflectionMethod $method */ - foreach ($methods as $method) { - if (substr($method->name, 0, 2) !== 'on') { - continue; - } - - // Save time if I'm not to detect legacy listeners - if (!$this->allowLegacyListeners) { - $this->registerListener($method->name); - - continue; - } - - /** @var \ReflectionParameter[] $parameters */ - $parameters = $method->getParameters(); + $this->isPluginInitialised = true; - // If the parameter count is not 1 it is by definition a legacy listener - if (\count($parameters) !== 1) { - $this->registerLegacyListener($method->name); - - continue; - } - - /** @var \ReflectionParameter $param */ - $param = array_shift($parameters); - $paramName = $param->getName(); - - // No type hint / type hint class not an event or parameter name is not "event"? It's a legacy listener. - if ($paramName !== 'event' || !$this->parameterImplementsEventInterface($param)) { - $this->registerLegacyListener($method->name); - - continue; - } - - // Everything checks out, this is a proper listener. - $this->registerListener($method->name); - } + $this->doInitialise(); } /** - * Registers a legacy event listener, i.e. a method which accepts individual arguments instead of an AbstractEvent - * in its arguments. This provides backwards compatibility to Joomla! 3.x-style plugins. - * - * This method will register lambda functions (closures) which try to unwrap the arguments of the dispatched Event - * into old style method arguments and call your on method with them. The result will be passed back to - * the Event, as an element into an array argument called 'result'. + * Initialisation code for your plugin. * - * @param string $methodName The method name to register + * This method is "magically" called exactly once, right before the very first time an event + * handler in your plugin is called. This makes sure that all lengthy initialisation code in + * your plugin will only be executed if your plugin is used in a page load, drastically + * improving the site's performance on pages where your plugin is loaded but its event handlers + * are not used. * * @return void - * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ - final protected function registerLegacyListener(string $methodName) + protected function doInitialise() { - $this->getDispatcher()->addListener( - $methodName, - function (AbstractEvent $event) use ($methodName) { - // Get the event arguments - $arguments = $event->getArguments(); - - // Extract any old results; they must not be part of the method call. - $allResults = []; - - if (isset($arguments['result'])) { - $allResults = $arguments['result']; - - unset($arguments['result']); - } - - // Convert to indexed array for unpacking. - $arguments = \array_values($arguments); - - $result = $this->{$methodName}(...$arguments); - - // Ignore null results - if ($result === null) { - return; - } - - // Restore the old results and add the new result from our method call - $allResults[] = $result; - $event['result'] = $allResults; - } - ); + // Load the language files if needed. + if ($this->autoloadLanguage) { + $this->loadLanguage(); + } } /** - * Registers a proper event listener, i.e. a method which accepts an AbstractEvent as its sole argument. This is the - * preferred way to implement plugins in Joomla! 4.x and will be the only possible method with Joomla! 5.x onwards. - * - * @param string $methodName The method name to register + * Register the "magic" late initialisation code for this plugin * * @return void - * - * @since 4.0.0 - */ - final protected function registerListener(string $methodName) - { - $this->getDispatcher()->addListener($methodName, [$this, $methodName]); - } - - /** - * Checks if parameter is typehinted to accept \Joomla\Event\EventInterface. - * - * @param \ReflectionParameter $parameter - * - * @return boolean - * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ - private function parameterImplementsEventInterface(\ReflectionParameter $parameter): bool + private function registerLateInitialisation(): void { - $reflectionType = $parameter->getType(); - - // Parameter is not typehinted. - if ($reflectionType === null) { - return false; - } + $prioritisedEvents = []; - // Parameter is nullable. - if ($reflectionType->allowsNull()) { - return false; + // Collect events and their priorities plus one for plugins implementing SubscriberInterface + if ($this instanceof SubscriberInterface) { + foreach ($this->getSubscribedEvents() as $eventName => $eventHandlerInfo) { + $priority = is_array($eventHandlerInfo) ? $eventHandlerInfo[1] : Priority::NORMAL; + $priority = $priority < PHP_INT_MAX ? $priority++ : $priority; + $prioritisedEvents[$eventName] = $priority; + } } - // Handle standard typehints. - if ($reflectionType instanceof \ReflectionNamedType) { - return \is_a($reflectionType->getName(), EventInterface::class, true); + // Early return if the plugin does not listen to any events. + if (empty($prioritisedEvents)) { + return; } - // Handle PHP 8 union types. - if ($reflectionType instanceof \ReflectionUnionType) { - foreach ($reflectionType->getTypes() as $type) { - if (!\is_a($type->getName(), EventInterface::class, true)) { - return false; - } + // Make the initialisePlugin code run before each of the plugin's real event handlers. + array_walk( + $prioritisedEvents, + function (int $priority, string $eventName) { + $this->getDispatcher()->addListener($eventName, [$this, 'initialisePlugin'], $priority); } - - return true; - } - - return false; - } - - /** - * Returns the internal application or null when not set. - * - * @return CMSApplicationInterface|null - * - * @since 4.2.0 - */ - protected function getApplication(): ?CMSApplicationInterface - { - return $this->application; - } - - /** - * Sets the application to use. - * - * @param CMSApplicationInterface $application The application - * - * @return void - * - * @since 4.2.0 - */ - public function setApplication(CMSApplicationInterface $application): void - { - $this->application = $application; + ); } } diff --git a/libraries/src/Plugin/LanguageAwareTrait.php b/libraries/src/Plugin/LanguageAwareTrait.php new file mode 100644 index 0000000000000..d0d97f21b06ad --- /dev/null +++ b/libraries/src/Plugin/LanguageAwareTrait.php @@ -0,0 +1,43 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Plugin; + +use Joomla\CMS\Factory; + +trait LanguageAwareTrait +{ + /** + * Loads the plugin language file + * + * @param string $extension The extension for which a language file should be loaded + * @param string $basePath The basepath to use + * + * @return boolean True, if the file has successfully loaded. + * + * @since 1.5 + */ + public function loadLanguage($extension = '', $basePath = JPATH_ADMINISTRATOR) + { + if (empty($extension)) { + $extension = 'Plg_' . $this->_type . '_' . $this->_name; + } + + $extension = strtolower($extension); + $lang = $this->getApplication() ? $this->getApplication()->getLanguage() : Factory::getLanguage(); + + // If language already loaded, don't load it again. + if ($lang->getPaths($extension)) { + return true; + } + + return $lang->load($extension, $basePath) + || $lang->load($extension, JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name); + } +} diff --git a/libraries/src/Plugin/LegacyListenerTrait.php b/libraries/src/Plugin/LegacyListenerTrait.php new file mode 100644 index 0000000000000..8ee4352ecf597 --- /dev/null +++ b/libraries/src/Plugin/LegacyListenerTrait.php @@ -0,0 +1,218 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Plugin; + +use Joomla\Event\AbstractEvent; +use Joomla\Event\EventInterface; +use Joomla\Event\Priority; +use Joomla\Event\SubscriberInterface; + +/** + * Trait to auto-register legacy (Joomla 3.x style) listeners and event handlers based on their name. + * + * @deprecated 5.0 Use SubscriberInterface instead + */ +trait LegacyListenerTrait +{ + /** + * Should the plugin try to register legacy (Joomla! 3.x style) listeners? + * + * If enabled it also registers on(Event $e) event handler methods. This is + * deprecated since Joomla 5.0. Use the SubscriberInterface to register the event handlers + * methods instead of relying on their name for automatic registration. + * + * @var bool + * @since __DEPLOY_VERSION__ + * @deprecated 5.0 Use the SubscriberInterface instead + */ + protected $allowLegacyListeners = true; + + /** + * Registers legacy Listeners to the Dispatcher, emulating how plugins worked under Joomla! 3.x and below. + * + * By default, this method will look for all public methods whose name starts with "on". It will register + * lambda functions (closures) which try to unwrap the arguments of the dispatched Event into method call + * arguments and call your on method. The result will be passed back to the Event into its 'result' + * argument. + * + * This method additionally supports Joomla\Event\SubscriberInterface and plugins implementing this will be + * registered to the dispatcher as a subscriber. + * + * @return void + * + * @since 4.0.0 + * @deprecated 5.0 Use SubscriberInterface instead + */ + public function registerListeners() + { + // Plugins which are SubscriberInterface implementations are handled without legacy layer support + if ($this instanceof SubscriberInterface) { + return; + } + + $reflectedObject = new \ReflectionObject($this); + $methods = $reflectedObject->getMethods(\ReflectionMethod::IS_PUBLIC); + + /** @var \ReflectionMethod $method */ + foreach ($methods as $method) { + if (substr($method->name, 0, 2) !== 'on') { + continue; + } + + if ($this->allowLateInitialisation) { + $this->getDispatcher()->addListener($method->name, [$this, 'initialisePlugin'], Priority::ABOVE_NORMAL); + } + + // Save time if I'm not to detect legacy listeners + if (!$this->allowLegacyListeners) { + $this->registerListener($method->name); + + continue; + } + + /** @var \ReflectionParameter[] $parameters */ + $parameters = $method->getParameters(); + + // If the parameter count is not 1 it is by definition a legacy listener + if (\count($parameters) !== 1) { + $this->registerLegacyListener($method->name); + + continue; + } + + /** @var \ReflectionParameter $param */ + $param = array_shift($parameters); + $paramName = $param->getName(); + + /** + * We are treating the method as a legacy listener when _any_ of the following conditions are met: + * - The parameter name is not `$event`. + * - There is no type hint for the parameter. + * - There is a type hint for the parameter, but it's the Joomla Event interface. + */ + if ($paramName !== 'event' || !$this->parameterImplementsEventInterface($param)) { + $this->registerLegacyListener($method->name); + + continue; + } + + // This is not a legacy listener, therefore it is an event handler + $this->registerListener($method->name); + } + } + + /** + * Registers a legacy event listener, i.e. a method which accepts individual arguments instead of an AbstractEvent + * in its arguments. This provides backwards compatibility to Joomla! 3.x-style plugins. + * + * This method will register lambda functions (closures) which try to unwrap the arguments of the dispatched Event + * into old style method arguments and call your on method with them. The result will be passed back to + * the Event, as an element into an array argument called 'result'. + * + * @param string $methodName The method name to register + * + * @return void + * + * @since 4.0.0 + * @deprecated 5.0 Use SubscriberInterface instead + */ + final protected function registerLegacyListener(string $methodName) + { + $this->getDispatcher()->addListener( + $methodName, + function (AbstractEvent $event) use ($methodName) { + // Get the event arguments + $arguments = $event->getArguments(); + + // Extract any old results; they must not be part of the method call. + $allResults = []; + + if (isset($arguments['result'])) { + $allResults = $arguments['result']; + + unset($arguments['result']); + } + + // Convert to indexed array for unpacking. + $arguments = \array_values($arguments); + + $result = $this->{$methodName}(...$arguments); + + // Ignore null results + if ($result === null) { + return; + } + + // Restore the old results and add the new result from our method call + $allResults[] = $result; + $event['result'] = $allResults; + } + ); + } + + /** + * Registers a proper event listener, i.e. a method which accepts an AbstractEvent as its sole argument. This is the + * preferred way to implement plugins in Joomla! 4.x and will be the only possible method with Joomla! 5.x onwards. + * + * @param string $methodName The method name to register + * + * @return void + * + * @since 4.0.0 + */ + final protected function registerListener(string $methodName) + { + $this->getDispatcher()->addListener($methodName, [$this, $methodName]); + } + + /** + * Checks if parameter is typehinted to accept \Joomla\Event\EventInterface. + * + * @param \ReflectionParameter $parameter + * + * @return boolean + * + * @since 4.0.0 + * @deprecated 5.0 Use SubscriberInterface instead + */ + private function parameterImplementsEventInterface(\ReflectionParameter $parameter): bool + { + $reflectionType = $parameter->getType(); + + // Parameter is not typehinted. + if ($reflectionType === null) { + return false; + } + + // Parameter is nullable. + if ($reflectionType->allowsNull()) { + return false; + } + + // Handle standard typehints. + if ($reflectionType instanceof \ReflectionNamedType) { + return \is_a($reflectionType->getName(), EventInterface::class, true); + } + + // Handle PHP 8 union types. + /** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */ + if (version_compare(PHP_VERSION, '8.0.0', 'gt') && $reflectionType instanceof \ReflectionUnionType) { + foreach ($reflectionType->getTypes() as $type) { + if (!\is_a($type->getName(), EventInterface::class, true)) { + return false; + } + } + + return true; + } + + return false; + } +} diff --git a/libraries/src/Plugin/LegacyPropertiesTrait.php b/libraries/src/Plugin/LegacyPropertiesTrait.php new file mode 100644 index 0000000000000..79d5c491a771f --- /dev/null +++ b/libraries/src/Plugin/LegacyPropertiesTrait.php @@ -0,0 +1,65 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Plugin; + +use Joomla\CMS\Application\ApplicationAwareInterface; +use Joomla\CMS\Factory; +use Joomla\Database\DatabaseAwareInterface; +use ReflectionClass; + +/** + * A trait to support the legacy $app and $db properties in plugins. + * + * @since __DEPLOY_VERSION__ + * @deprecated 5.0 Use the ApplicationAwareInterface, DatabaseAwareInterface and their traits + */ +trait LegacyPropertiesTrait +{ + /** + * Looks for and populates the legacy $app and $db properties. + * + * @return void + * @throws \ReflectionException + * @since __DEPLOY_VERSION__ + * @deprecated 5.0 Use the ApplicationAwareInterface, DatabaseAwareInterface and their respective traits. + */ + private function implementLegacyProperties() + { + if (property_exists($this, 'app')) { + @trigger_error( + 'The application should be injected through the ApplicationAwareInterface and trait', + E_USER_DEPRECATED + ); + $reflection = new ReflectionClass($this); + $appProperty = $reflection->getProperty('app'); + + if ($appProperty->isPrivate() === false && \is_null($this->app)) { + $this->app = ($this instanceof ApplicationAwareInterface) + ? $this->getApplication() ?? Factory::getApplication() + : Factory::getApplication(); + } + } + + if (property_exists($this, 'db')) { + @trigger_error( + 'The database should be injected through the DatabaseAwareInterface and trait.', + E_USER_DEPRECATED + ); + $reflection = new ReflectionClass($this); + $dbProperty = $reflection->getProperty('db'); + + if ($dbProperty->isPrivate() === false && \is_null($this->db)) { + $this->db = ($this instanceof DatabaseAwareInterface) + ? $this->getDatabase() ?? Factory::getContainer()->get('DatabaseDriver') + : Factory::getContainer()->get('DatabaseDriver'); + } + } + } +} diff --git a/libraries/src/Plugin/PluginHelper.php b/libraries/src/Plugin/PluginHelper.php index 51f52c7410e09..88648c499a756 100644 --- a/libraries/src/Plugin/PluginHelper.php +++ b/libraries/src/Plugin/PluginHelper.php @@ -13,6 +13,7 @@ use Joomla\CMS\Factory; use Joomla\Event\DispatcherAwareInterface; use Joomla\Event\DispatcherInterface; +use Joomla\Event\SubscriberInterface; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; @@ -228,7 +229,19 @@ protected static function import($plugin, $autocreate = true, DispatcherInterfac return; } - $plugin->registerListeners(); + // Register the plugin's event handlers (preferred method) + if ($plugin instanceof SubscriberInterface) { + $dispatcher->addSubscriber($plugin); + } + + /** + * Register legacy listeners and event handlers by name. + * + * @deprecated 5.0 Use SubscriberInterface in plugin objects instead + */ + if (method_exists($plugin, 'registerListeners')) { + $plugin->registerListeners(); + } } /** diff --git a/tests/Unit/Libraries/Cms/Plugin/CMSPluginTest.php b/tests/Unit/Libraries/Cms/Plugin/CMSPluginTest.php index e4e92eb447b8f..8c7acf23ccf32 100644 --- a/tests/Unit/Libraries/Cms/Plugin/CMSPluginTest.php +++ b/tests/Unit/Libraries/Cms/Plugin/CMSPluginTest.php @@ -11,11 +11,13 @@ namespace Joomla\Tests\Unit\Libraries\Cms\Plugin; use Joomla\CMS\Application\CMSApplicationInterface; +use Joomla\CMS\Event\GenericEvent; use Joomla\CMS\Language\Language; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\Event\Dispatcher; use Joomla\Event\Event; use Joomla\Event\EventInterface; +use Joomla\Event\Priority; use Joomla\Event\SubscriberInterface; use Joomla\Registry\Registry; use Joomla\Tests\Unit\UnitTestCase; @@ -257,6 +259,44 @@ public function testRegisterListenersAsSubscriber() $plugin = new class ($dispatcher, []) extends CMSPlugin implements SubscriberInterface { + protected $allowLateInitialisation = false; + + public static function getSubscribedEvents(): array + { + return ['test' => 'unit']; + } + + public function unit() + { + } + }; + + // Since __DEPLOY_VERSION__ the PluginHelper runs this code instead of registerListeners(). + $dispatcher->addSubscriber($plugin); + + $this->assertEquals( + [ + [$plugin, 'unit'], + ], + $dispatcher->getListeners('test') + ); + } + + /** + * @testdox can register the late initialisation listener when is SubscriberInterface + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testRegisterLateInitialisationListenerAsSubscriber() + { + $dispatcher = new Dispatcher(); + + $plugin = new class ($dispatcher, []) extends CMSPlugin implements SubscriberInterface + { + protected $allowLateInitialisation = true; + public static function getSubscribedEvents(): array { return ['test' => 'unit']; @@ -266,9 +306,121 @@ public function unit() { } }; + + // Since __DEPLOY_VERSION__ the PluginHelper runs this code instead of registerListeners(). + $dispatcher->addSubscriber($plugin); + + $this->assertEquals( + [ + [$plugin, 'initialisePlugin'], + [$plugin, 'unit'], + ], + $dispatcher->getListeners('test') + ); + } + + /** + * @testdox can register the late initialisation listener when is legacy + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testRegisterLateInitialisationListenerAsLegacy() + { + $dispatcher = new Dispatcher(); + + $plugin = new class ($dispatcher, []) extends CMSPlugin + { + public function onTest(Event $e) + { + } + }; + + $plugin->registerListeners(); + + $listeners = $dispatcher->getListeners('onTest'); + + $this->assertIsArray($listeners); + $this->assertCount(2, $listeners); + $this->assertEquals([$plugin, 'initialisePlugin'], $listeners[0]); + } + + /** + * @testdox can execute the late initialisation listener when is legacy + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testExecuteLateInitialisationAsLegacy() + { + $dispatcher = new Dispatcher(); + + $plugin = new class ($dispatcher, []) extends CMSPlugin + { + private $test = false; + + public function doInitialise() + { + $this->test = true; + } + + public function onTest(Event $event) + { + $event->setArgument('result', [$this->test]); + } + }; + $plugin->registerListeners(); - $this->assertEquals([[$plugin, 'unit']], $dispatcher->getListeners('test')); + $event = new GenericEvent('onTest'); + $result = $dispatcher->dispatch('onTest', $event)->getArgument('result'); + + $this->assertIsArray($result); + $this->assertCount(1, $result); + $this->assertEquals(true, $result[0]); + } + + /** + * @testdox can execute the late initialisation listener when is SubscriberInterface + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testExecuteLateInitialisationAsSubscriberInterface() + { + $dispatcher = new Dispatcher(); + + $plugin = new class ($dispatcher, []) extends CMSPlugin implements SubscriberInterface + { + private $test = false; + + public function doInitialise() + { + $this->test = true; + } + + public function test(Event $event) + { + $event->setArgument('result', [$this->test]); + } + + public static function getSubscribedEvents(): array + { + return ['onTest' => ['test', PHP_INT_MAX]]; + } + }; + + $dispatcher->addSubscriber($plugin); + + $event = new GenericEvent('onTest'); + $result = $dispatcher->dispatch('onTest', $event)->getArgument('result'); + + $this->assertIsArray($result); + $this->assertCount(1, $result); + $this->assertEquals(true, $result[0]); } /** @@ -284,6 +436,8 @@ public function testRegisterListenersAsLegacy() $plugin = new class ($dispatcher, []) extends CMSPlugin { + protected $allowLateInitialisation = false; + public function onTest() { } @@ -306,6 +460,8 @@ public function testRegisterListenersForEventInterface() $plugin = new class ($dispatcher, []) extends CMSPlugin { + protected $allowLateInitialisation = false; + public function onTest(EventInterface $event) { } @@ -329,6 +485,7 @@ public function testRegisterListenersWithForcedEventInterface() $plugin = new class ($dispatcher, []) extends CMSPlugin { protected $allowLegacyListeners = false; + protected $allowLateInitialisation = false; public function onTest(EventInterface $event) { @@ -352,6 +509,8 @@ public function testRegisterListenersForNoEventInterface() $plugin = new class ($dispatcher, []) extends CMSPlugin { + protected $allowLateInitialisation = false; + public function onTest(string $context) { } @@ -374,6 +533,8 @@ public function testRegisterListenersNotTyped() $plugin = new class ($dispatcher, []) extends CMSPlugin { + protected $allowLateInitialisation = false; + public function onTest($event) { } @@ -396,6 +557,8 @@ public function testRegisterListenersNullable() $plugin = new class ($dispatcher, []) extends CMSPlugin { + protected $allowLateInitialisation = false; + public function onTest(stdClass $event = null) { }