diff --git a/libraries/cms/html/html.php b/libraries/cms/html/html.php
index 0a02fe4b561f1..9b6e2974ed87a 100644
--- a/libraries/cms/html/html.php
+++ b/libraries/cms/html/html.php
@@ -9,6 +9,8 @@
defined('JPATH_PLATFORM') or die;
+use Joomla\CMS\HTML\Registry;
+use Joomla\CMS\Log\Log;
use Joomla\Utilities\ArrayHelper;
jimport('joomla.environment.browser');
@@ -40,6 +42,7 @@ abstract class JHtml
*
* @var string[]
* @since 1.5
+ * @deprecated 5.0
*/
protected static $includePaths = array();
@@ -48,9 +51,18 @@ abstract class JHtml
*
* @var callable[]
* @since 1.6
+ * @deprecated 5.0
*/
protected static $registry = array();
+ /**
+ * The service registry for custom and overridden JHtml helpers
+ *
+ * @var Registry
+ * @since __DEPLOY_VERSION__
+ */
+ protected static $serviceRegistry;
+
/**
* Method to extract a key
*
@@ -68,6 +80,22 @@ protected static function extract($key)
// Check to see whether we need to load a helper file
$parts = explode('.', $key);
+ if (count($parts) === 3)
+ {
+ try
+ {
+ Log::add(
+ 'Support for a three segment service key is deprecated and will be removed in Joomla 5.0, use the service registry instead',
+ Log::WARNING,
+ 'deprecated'
+ );
+ }
+ catch (RuntimeException $exception)
+ {
+ // Informational message only, continue on
+ }
+ }
+
$prefix = count($parts) === 3 ? array_shift($parts) : 'JHtml';
$file = count($parts) === 2 ? array_shift($parts) : '';
$func = array_shift($parts);
@@ -105,6 +133,30 @@ public static function _($key)
return static::call($function, $args);
}
+ /*
+ * Support fetching services from the registry if a custom class prefix was not given (a three segment key),
+ * the service comes from a class other than this one, and a service has been registered for the file.
+ */
+ if ($prefix === 'JHtml' && $file !== '' && static::getServiceRegistry()->hasService($file))
+ {
+ $service = static::getServiceRegistry()->getService($file);
+
+ $toCall = array($service, $func);
+
+ if (!is_callable($toCall))
+ {
+ throw new InvalidArgumentException(sprintf('%s::%s not found.', $service, $func), 500);
+ }
+
+ static::register($key, $toCall);
+ $args = func_get_args();
+
+ // Remove function name from arguments
+ array_shift($args);
+
+ return static::call($toCall, $args);
+ }
+
$className = $prefix . ucfirst($file);
if (!class_exists($className))
@@ -152,6 +204,19 @@ public static function _($key)
*/
public static function register($key, callable $function)
{
+ try
+ {
+ Log::add(
+ 'Support for registering functions is deprecated and will be removed in Joomla 5.0, use the service registry instead',
+ Log::WARNING,
+ 'deprecated'
+ );
+ }
+ catch (RuntimeException $exception)
+ {
+ // Informational message only, continue on
+ }
+
list($key) = static::extract($key);
static::$registry[$key] = $function;
@@ -170,6 +235,19 @@ public static function register($key, callable $function)
*/
public static function unregister($key)
{
+ try
+ {
+ Log::add(
+ 'Support for registering functions is deprecated and will be removed in Joomla 5.0, use the service registry instead',
+ Log::WARNING,
+ 'deprecated'
+ );
+ }
+ catch (RuntimeException $exception)
+ {
+ // Informational message only, continue on
+ }
+
list($key) = static::extract($key);
if (isset(static::$registry[$key]))
@@ -198,6 +276,22 @@ public static function isRegistered($key)
return isset(static::$registry[$key]);
}
+ /**
+ * Retrieves the service registry.
+ *
+ * @return Registry
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public static function getServiceRegistry()
+ {
+ if (!static::$serviceRegistry)
+ {
+ static::$serviceRegistry = new Registry;
+ }
+
+ return static::$serviceRegistry;
+ }
/**
* Function caller method
*
@@ -1145,6 +1239,19 @@ public static function calendar($value, $name, $id, $format = '%Y-%m-%d', $attri
*/
public static function addIncludePath($path = '')
{
+ try
+ {
+ Log::add(
+ 'Support for registering lookup paths is deprecated and will be removed in Joomla 5.0, use the service registry instead',
+ Log::WARNING,
+ 'deprecated'
+ );
+ }
+ catch (RuntimeException $exception)
+ {
+ // Informational message only, continue on
+ }
+
// Loop through the path directories
foreach ((array) $path as $dir)
{
diff --git a/libraries/src/CMS/HTML/Registry.php b/libraries/src/CMS/HTML/Registry.php
new file mode 100644
index 0000000000000..2398d5e005bbd
--- /dev/null
+++ b/libraries/src/CMS/HTML/Registry.php
@@ -0,0 +1,142 @@
+ \JHtmlAccess::class,
+ 'actionsdropdown' => \JHtmlActionsDropdown::class,
+ 'batch' => \JHtmlBatch::class,
+ 'behavior' => \JHtmlBehavior::class,
+ 'bootstrap' => \JHtmlBootstrap::class,
+ 'category' => \JHtmlCategory::class,
+ 'content' => \JHtmlContent::class,
+ 'contentlanguage' => \JHtmlContentlanguage::class,
+ 'date' => \JHtmlDate::class,
+ 'debug' => \JHtmlDebug::class,
+ 'draggablelist' => \JHtmlDraggablelist::class,
+ 'dropdown' => \JHtmlDropdown::class,
+ 'email' => \JHtmlEmail::class,
+ 'form' => \JHtmlForm::class,
+ 'formbehavior' => \JHtmlFormbehavior::class,
+ 'grid' => \JHtmlGrid::class,
+ 'icons' => \JHtmlIcons::class,
+ 'jgrid' => \JHtmlJGrid::class,
+ 'jquery' => \JHtmlJquery::class,
+ 'links' => \JHtmlLinks::class,
+ 'list' => \JHtmlList::class,
+ 'menu' => \JHtmlMenu::class,
+ 'number' => \JHtmlNumber::class,
+ 'searchtools' => \JHtmlSearchtools::class,
+ 'select' => \JHtmlSelect::class,
+ 'sidebar' => \JHtmlSidebar::class,
+ 'sortablelist' => \JHtmlSortablelist::class,
+ 'string' => \JHtmlString::class,
+ 'tag' => \JHtmlTag::class,
+ 'tel' => \JHtmlTel::class,
+ 'user' => \JHtmlUser::class,
+ ];
+
+ /**
+ * Array holding the registered services
+ *
+ * @var array
+ * @since __DEPLOY_VERSION__
+ */
+ private $serviceMap = [];
+
+ /**
+ * Get the service for a given key
+ *
+ * @param string $key The service key to look up
+ *
+ * @return string|object
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function getService($key)
+ {
+ if (!$this->hasService($key))
+ {
+ throw new \InvalidArgumentException("The '$key' service key is not registered.");
+ }
+
+ return $this->serviceMap[$key];
+ }
+
+ /**
+ * Check if the registry has a service for the given key
+ *
+ * @param string $key The service key to look up
+ *
+ * @return boolean
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function hasService($key)
+ {
+ return isset($this->serviceMap[$key]);
+ }
+
+ /**
+ * Register a service
+ *
+ * @param string $key The service key to be registered
+ * @param string|object $handler The handler for the service as either a PHP class name or class object
+ * @param boolean $replace Flag indicating the service key may replace an existing definition
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function register($key, $handler, $replace = false)
+ {
+ // If the key exists already and we aren't instructed to replace existing services, bail early
+ if (isset($this->serviceMap[$key]) && !$replace)
+ {
+ throw new \RuntimeException("The '$key' service key is already registered.");
+ }
+
+ // If the handler is a string, it must be a class that exists
+ if (is_string($handler) && !class_exists($handler))
+ {
+ throw new \RuntimeException("The '$handler' class for service key '$key' does not exist.");
+ }
+
+ // Otherwise the handler must be a class object
+ if (!is_string($handler) && !is_object($handler))
+ {
+ throw new \RuntimeException(
+ sprintf(
+ 'The handler for service key %1$s must be a PHP class name or class object, a %2$s was given.',
+ $key,
+ gettype($handler)
+ )
+ );
+ }
+
+ $this->serviceMap[$key] = $handler;
+ }
+}