924 lines
28 KiB
PHP
924 lines
28 KiB
PHP
<?php
|
|
/**
|
|
* @package FOF
|
|
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
|
* @license GNU General Public License version 3, or later
|
|
*/
|
|
|
|
namespace FOF40\Html\FEFHelper;
|
|
|
|
defined('_JEXEC') || die;
|
|
|
|
use FOF40\Container\Container;
|
|
use FOF40\Html\SelectOptions;
|
|
use FOF40\Model\DataModel;
|
|
use FOF40\Utils\ArrayHelper;
|
|
use FOF40\View\DataView\DataViewInterface;
|
|
use FOF40\View\View;
|
|
use Joomla\CMS\HTML\HTMLHelper;
|
|
use Joomla\CMS\Language\Text;
|
|
|
|
/**
|
|
* An HTML helper for Browse views.
|
|
*
|
|
* It reintroduces a FEF-friendly of some of the functionality found in FOF 3's Header and Field classes. These
|
|
* helpers are also accessible through Blade, making the transition from XML forms to Blade templates easier.
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
abstract class BrowseView
|
|
{
|
|
/**
|
|
* Caches the results of getOptionsFromModel keyed by a hash. The hash is computed by the model
|
|
* name, the model state and the options passed to getOptionsFromModel.
|
|
*
|
|
* @var array
|
|
*/
|
|
private static $cacheModelOptions = [];
|
|
|
|
/**
|
|
* Get the translation key for a field's label
|
|
*
|
|
* @param string $fieldName The field name
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
public static function fieldLabelKey(string $fieldName): string
|
|
{
|
|
$view = self::getViewFromBacktrace();
|
|
|
|
try
|
|
{
|
|
$inflector = $view->getContainer()->inflector;
|
|
$viewName = $inflector->singularize($view->getName());
|
|
$altViewName = $inflector->pluralize($view->getName());
|
|
$componentName = $view->getContainer()->componentName;
|
|
|
|
$keys = [
|
|
strtoupper($componentName . '_' . $viewName . '_FIELD_' . $fieldName),
|
|
strtoupper($componentName . '_' . $altViewName . '_FIELD_' . $fieldName),
|
|
strtoupper($componentName . '_' . $viewName . '_' . $fieldName),
|
|
strtoupper($componentName . '_' . $altViewName . '_' . $fieldName),
|
|
];
|
|
|
|
foreach ($keys as $key)
|
|
{
|
|
if (Text::_($key) !== $key)
|
|
{
|
|
return $key;
|
|
}
|
|
}
|
|
|
|
return $keys[0];
|
|
}
|
|
catch (\Exception $e)
|
|
{
|
|
return ucfirst($fieldName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the label for a field (translated)
|
|
*
|
|
* @param string $fieldName The field name
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function fieldLabel(string $fieldName): string
|
|
{
|
|
return Text::_(self::fieldLabelKey($fieldName));
|
|
}
|
|
|
|
/**
|
|
* Return a table field header which sorts the table by that field upon clicking
|
|
*
|
|
* @param string $field The name of the field
|
|
* @param string|null $langKey (optional) The language key for the header to be displayed
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function sortgrid(string $field, ?string $langKey = null): string
|
|
{
|
|
/** @var DataViewInterface $view */
|
|
$view = self::getViewFromBacktrace();
|
|
|
|
if (is_null($langKey))
|
|
{
|
|
$langKey = self::fieldLabelKey($field);
|
|
}
|
|
|
|
return HTMLHelper::_('FEFHelp.browse.sort', $langKey, $field, $view->getLists()->order_Dir, $view->getLists()->order, $view->getTask());
|
|
}
|
|
|
|
/**
|
|
* Create a browse view filter from values returned by a model
|
|
*
|
|
* @param string $localField Field name
|
|
* @param string $modelTitleField Foreign model field for drop-down display values
|
|
* @param null $modelName Foreign model name
|
|
* @param string $placeholder Placeholder for no selection
|
|
* @param array $params Generic select display parameters
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
public static function modelFilter(string $localField, string $modelTitleField = 'title', ?string $modelName = null,
|
|
?string $placeholder = null, array $params = []): string
|
|
{
|
|
/** @var DataModel $model */
|
|
$model = self::getViewFromBacktrace()->getModel();
|
|
|
|
if (empty($modelName))
|
|
{
|
|
$modelName = $model->getForeignModelNameFor($localField);
|
|
}
|
|
|
|
if (is_null($placeholder))
|
|
{
|
|
$placeholder = self::fieldLabelKey($localField);
|
|
}
|
|
|
|
$params = array_merge([
|
|
'list.none' => '— ' . Text::_($placeholder) . ' —',
|
|
'value_field' => $modelTitleField,
|
|
'fof.autosubmit' => true,
|
|
], $params);
|
|
|
|
return self::modelSelect($localField, $modelName, $model->getState($localField), $params);
|
|
}
|
|
|
|
/**
|
|
* Display a text filter (search box)
|
|
*
|
|
* @param string $localField The name of the model field. Used when getting the filter state.
|
|
* @param string $searchField The INPUT element's name. Default: "filter_$localField".
|
|
* @param string $placeholder The Text language key for the placeholder. Default: extrapolate from $localField.
|
|
* @param array $attributes HTML attributes for the INPUT element.
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
public static function searchFilter(string $localField, ?string $searchField = null, ?string $placeholder = null,
|
|
array $attributes = []): string
|
|
{
|
|
/** @var DataModel $model */
|
|
$view = self::getViewFromBacktrace();
|
|
$model = $view->getModel();
|
|
$searchField = empty($searchField) ? $localField : $searchField;
|
|
$placeholder = empty($placeholder) ? self::fieldLabelKey($localField) : $placeholder;
|
|
$attributes['type'] = $attributes['type'] ?? 'text';
|
|
$attributes['name'] = $searchField;
|
|
$attributes['id'] = !isset($attributes['id']) ? "filter_$localField" : $attributes['id'];
|
|
$attributes['placeholder'] = !isset($attributes['placeholder']) ? $view->escape(Text::_($placeholder)) : $attributes['placeholder'];
|
|
$attributes['title'] = $attributes['title'] ?? $attributes['placeholder'];
|
|
$attributes['value'] = $view->escape($model->getState($localField));
|
|
|
|
if (!isset($attributes['onchange']))
|
|
{
|
|
$attributes['class'] = trim(($attributes['class'] ?? '') . ' akeebaCommonEventsOnChangeSubmit');
|
|
$attributes['data-akeebasubmittarget'] = $attributes['data-akeebasubmittarget'] ?? 'adminForm';
|
|
}
|
|
|
|
// Remove null attributes and collapse into a string
|
|
$attributes = array_filter($attributes, function ($v) {
|
|
return !is_null($v);
|
|
});
|
|
$attributes = ArrayHelper::toString($attributes);
|
|
|
|
return "<input $attributes />";
|
|
}
|
|
|
|
/**
|
|
* Create a browse view filter with dropdown values
|
|
*
|
|
* @param string $localField Field name
|
|
* @param array $options The HTMLHelper options list to use
|
|
* @param string $placeholder Placeholder for no selection
|
|
* @param array $params Generic select display parameters
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
public static function selectFilter(string $localField, array $options, ?string $placeholder = null,
|
|
array $params = []): string
|
|
{
|
|
/** @var DataModel $model */
|
|
$model = self::getViewFromBacktrace()->getModel();
|
|
|
|
if (is_null($placeholder))
|
|
{
|
|
$placeholder = self::fieldLabelKey($localField);
|
|
}
|
|
|
|
$params = array_merge([
|
|
'list.none' => '— ' . Text::_($placeholder) . ' —',
|
|
'fof.autosubmit' => true,
|
|
], $params);
|
|
|
|
return self::genericSelect($localField, $options, $model->getState($localField), $params);
|
|
}
|
|
|
|
/**
|
|
* View access dropdown filter
|
|
*
|
|
* @param string $localField Field name
|
|
* @param string $placeholder Placeholder for no selection
|
|
* @param array $params Generic select display parameters
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
public static function accessFilter(string $localField, ?string $placeholder = null, array $params = []): string
|
|
{
|
|
return self::selectFilter($localField, SelectOptions::getOptions('access', $params), $placeholder, $params);
|
|
}
|
|
|
|
/**
|
|
* Published state dropdown filter
|
|
*
|
|
* @param string $localField Field name
|
|
* @param string $placeholder Placeholder for no selection
|
|
* @param array $params Generic select display parameters
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
public static function publishedFilter(string $localField, ?string $placeholder = null, array $params = []): string
|
|
{
|
|
return self::selectFilter($localField, SelectOptions::getOptions('published', $params), $placeholder, $params);
|
|
}
|
|
|
|
/**
|
|
* Create a select box from the values returned by a model
|
|
*
|
|
* @param string $name Field name
|
|
* @param string $modelName The name of the model, e.g. "items" or "com_foobar.items"
|
|
* @param mixed $currentValue The currently selected value
|
|
* @param array $params Passed to optionsFromModel and genericSelect
|
|
* @param array $modelState Optional state variables to pass to the model
|
|
* @param array $options Any HTMLHelper select options you want to add in front of the model's returned
|
|
* values
|
|
*
|
|
* @return string
|
|
*
|
|
* @see self::getOptionsFromModel
|
|
* @see self::getOptionsFromSource
|
|
* @see self::genericSelect
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
public static function modelSelect(string $name, string $modelName, $currentValue, array $params = [],
|
|
array $modelState = [], array $options = []): string
|
|
{
|
|
$params = array_merge([
|
|
'fof.autosubmit' => true,
|
|
], $params);
|
|
|
|
$options = self::getOptionsFromModel($modelName, $params, $modelState, $options);
|
|
|
|
return self::genericSelect($name, $options, $currentValue, $params);
|
|
}
|
|
|
|
/**
|
|
* Get a (human readable) title from a (typically numeric, foreign key) key value using the data
|
|
* returned by a DataModel.
|
|
*
|
|
* @param string $value The key value
|
|
* @param string $modelName The name of the model, e.g. "items" or "com_foobar.items"
|
|
* @param array $params Passed to getOptionsFromModel
|
|
* @param array $modelState Optional state variables to pass to the model
|
|
* @param array $options Any HTMLHelper select options you want to add in front of the model's returned
|
|
* values
|
|
*
|
|
* @return string
|
|
*
|
|
* @see self::getOptionsFromModel
|
|
* @see self::getOptionsFromSource
|
|
* @see self::genericSelect
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
public static function modelOptionName(string $value, ?string $modelName = null, array $params = [],
|
|
array $modelState = [], array $options = []): ?string
|
|
{
|
|
if (!isset($params['cache']))
|
|
{
|
|
$params['cache'] = true;
|
|
}
|
|
|
|
if (!isset($params['none_as_zero']))
|
|
{
|
|
$params['none_as_zero'] = true;
|
|
}
|
|
|
|
$options = self::getOptionsFromModel($modelName, $params, $modelState, $options);
|
|
|
|
return self::getOptionName($value, $options);
|
|
}
|
|
|
|
/**
|
|
* Gets the active option's label given an array of HTMLHelper options
|
|
*
|
|
* @param mixed $selected The currently selected value
|
|
* @param array $data The HTMLHelper options to parse
|
|
* @param string $optKey Key name, default: value
|
|
* @param string $optText Value name, default: text
|
|
* @param bool $selectFirst Should I automatically select the first option? Default: true
|
|
*
|
|
* @return mixed The label of the currently selected option
|
|
*/
|
|
public static function getOptionName($selected, array $data, string $optKey = 'value', string $optText = 'text', bool $selectFirst = true): ?string
|
|
{
|
|
$ret = null;
|
|
|
|
foreach ($data as $elementKey => &$element)
|
|
{
|
|
if (is_array($element))
|
|
{
|
|
$key = $optKey === null ? $elementKey : $element[$optKey];
|
|
$text = $element[$optText];
|
|
}
|
|
elseif (is_object($element))
|
|
{
|
|
$key = $optKey === null ? $elementKey : $element->$optKey;
|
|
$text = $element->$optText;
|
|
}
|
|
else
|
|
{
|
|
// This is a simple associative array
|
|
$key = $elementKey;
|
|
$text = $element;
|
|
}
|
|
|
|
if (is_null($ret) && $selectFirst && ($selected == $key))
|
|
{
|
|
$ret = $text;
|
|
}
|
|
elseif ($selected == $key)
|
|
{
|
|
$ret = $text;
|
|
}
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Create a generic select list based on a bunch of options. Option sources will be merged into the provided
|
|
* options automatically.
|
|
*
|
|
* Parameters:
|
|
* - format.depth The current indent depth.
|
|
* - format.eol The end of line string, default is linefeed.
|
|
* - format.indent The string to use for indentation, default is tab.
|
|
* - groups If set, looks for keys with the value "<optgroup>" and synthesizes groups from them. Deprecated.
|
|
* Default: true.
|
|
* - list.select Either the value of one selected option or an array of selected options. Default: $currentValue.
|
|
* - list.translate If true, text and labels are translated via Text::_(). Default is false.
|
|
* - list.attr HTML element attributes (key/value array or string)
|
|
* - list.none Placeholder for no selection (creates an option with an empty string key)
|
|
* - option.id The property in each option array to use as the selection id attribute. Defaults: null.
|
|
* - option.key The property in each option array to use as the Default: "value". If set to null, the index of the
|
|
* option array is used.
|
|
* - option.label The property in each option array to use as the selection label attribute. Default: null
|
|
* - option.text The property in each option array to use as the displayed text. Default: "text". If set to null,
|
|
* the option array is assumed to be a list of displayable scalars.
|
|
* - option.attr The property in each option array to use for additional selection attributes. Defaults: null.
|
|
* - option.disable: The property that will hold the disabled state. Defaults to "disable".
|
|
* - fof.autosubmit Should I auto-submit the form on change? Default: true
|
|
* - fof.formname Form to auto-submit. Default: adminForm
|
|
* - class CSS class to apply
|
|
* - size Size attribute for the input
|
|
* - multiple Is this a multiple select? Default: false.
|
|
* - required Is this a required field? Default: false.
|
|
* - autofocus Should I focus this field automatically? Default: false
|
|
* - disabled Is this a disabled field? Default: false
|
|
* - readonly Render as a readonly field with hidden inputs? Overrides 'disabled'. Default: false
|
|
* - onchange Custom onchange handler. Overrides fof.autosubmit. Default: NULL (use fof.autosubmit).
|
|
*
|
|
* @param string $name
|
|
* @param array $options
|
|
* @param mixed $currentValue
|
|
* @param array $params
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
public static function genericSelect(string $name, array $options, $currentValue, array $params = []): string
|
|
{
|
|
$params = array_merge([
|
|
'format.depth' => 0,
|
|
'format.eol' => "\n",
|
|
'format.indent' => "\t",
|
|
'groups' => true,
|
|
'list.select' => $currentValue,
|
|
'list.translate' => false,
|
|
'option.id' => null,
|
|
'option.key' => 'value',
|
|
'option.label' => null,
|
|
'option.text' => 'text',
|
|
'option.attr' => null,
|
|
'option.disable' => 'disable',
|
|
'list.attr' => '',
|
|
'list.none' => '',
|
|
'id' => null,
|
|
'fof.autosubmit' => true,
|
|
'fof.formname' => 'adminForm',
|
|
'class' => '',
|
|
'size' => '',
|
|
'multiple' => false,
|
|
'required' => false,
|
|
'autofocus' => false,
|
|
'disabled' => false,
|
|
'onchange' => null,
|
|
'readonly' => false,
|
|
], $params);
|
|
|
|
$currentValue = $params['list.select'];
|
|
|
|
$classes = $params['class'] ?? '';
|
|
$classes = is_array($classes) ? implode(' ', $classes) : $classes;
|
|
|
|
// If fof.autosubmit is enabled and onchange is not set we will add our own handler
|
|
if ($params['fof.autosubmit'] && is_null($params['onchange']))
|
|
{
|
|
$formName = $params['fof.formname'] ?: 'adminForm';
|
|
$classes .= ' akeebaCommonEventsOnChangeSubmit';
|
|
$params['data-akeebasubmittarget'] = $formName;
|
|
}
|
|
|
|
// Construct SELECT element's attributes
|
|
$attr = [
|
|
'class' => trim($classes) ?: null,
|
|
'size' => ($params['size'] ?? null) ?: null,
|
|
'multiple' => ($params['multiple'] ?? null) ?: null,
|
|
'required' => ($params['required'] ?? false) ?: null,
|
|
'aria-required' => ($params['required'] ?? false) ? 'true' : null,
|
|
'autofocus' => ($params['autofocus'] ?? false) ?: null,
|
|
'disabled' => (($params['disabled'] ?? false) || ($params['readonly'] ?? false)) ?: null,
|
|
'onchange' => $params['onchange'] ?? null,
|
|
];
|
|
|
|
$attr = array_filter($attr, function ($x) {
|
|
return !is_null($x);
|
|
});
|
|
|
|
// We merge the constructed SELECT element's attributes with the 'list.attr' array, if it was provided
|
|
$params['list.attr'] = array_merge($attr, (($params['list.attr'] ?? []) ?: []));
|
|
|
|
// Merge the options with those fetched from a source (e.g. another Helper object)
|
|
$options = array_merge($options, self::getOptionsFromSource($params));
|
|
|
|
if (!empty($params['list.none']))
|
|
{
|
|
array_unshift($options, HTMLHelper::_('FEFHelp.select.option', '', Text::_($params['list.none'])));
|
|
}
|
|
|
|
$html = [];
|
|
|
|
// Create a read-only list (no name) with hidden input(s) to store the value(s).
|
|
if ($params['readonly'])
|
|
{
|
|
$html[] = HTMLHelper::_('FEFHelp.select.genericlist', $options, $name, $params);
|
|
|
|
// E.g. form field type tag sends $this->value as array
|
|
if ($params['multiple'] && is_array($currentValue))
|
|
{
|
|
if (count($currentValue) === 0)
|
|
{
|
|
$currentValue[] = '';
|
|
}
|
|
|
|
foreach ($currentValue as $value)
|
|
{
|
|
$html[] = '<input type="hidden" name="' . $name . '" value="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '"/>';
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$html[] = '<input type="hidden" name="' . $name . '" value="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '"/>';
|
|
}
|
|
}
|
|
else
|
|
// Create a regular list.
|
|
{
|
|
$html[] = HTMLHelper::_('FEFHelp.select.genericlist', $options, $name, $params);
|
|
}
|
|
|
|
return implode($html);
|
|
}
|
|
|
|
/**
|
|
* Replace tags that reference fields with their values
|
|
*
|
|
* @param string $text Text to process
|
|
* @param DataModel $item The DataModel instance to get values from
|
|
*
|
|
* @return string Text with tags replace
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
public static function parseFieldTags(string $text, DataModel $item): string
|
|
{
|
|
$ret = $text;
|
|
|
|
if (empty($item))
|
|
{
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Replace [ITEM:ID] in the URL with the item's key value (usually: the auto-incrementing numeric ID)
|
|
*/
|
|
$replace = $item->getId();
|
|
$ret = str_replace('[ITEM:ID]', $replace, $ret);
|
|
|
|
// Replace the [ITEMID] in the URL with the current Itemid parameter
|
|
$ret = str_replace('[ITEMID]', $item->getContainer()->input->getInt('Itemid', 0), $ret);
|
|
|
|
// Replace the [TOKEN] in the URL with the Joomla! form token
|
|
$ret = str_replace('[TOKEN]', $item->getContainer()->platform->getToken(true), $ret);
|
|
|
|
// Replace other field variables in the URL
|
|
$data = $item->getData();
|
|
|
|
foreach ($data as $field => $value)
|
|
{
|
|
// Skip non-processable values
|
|
if (is_array($value) || is_object($value))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$search = '[ITEM:' . strtoupper($field) . ']';
|
|
$ret = str_replace($search, $value, $ret);
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Get the FOF View from the backtrace of the static call. MAGIC!
|
|
*
|
|
* @return View
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
public static function getViewFromBacktrace(): View
|
|
{
|
|
// In case we are on a brain-dead host
|
|
if (!function_exists('debug_backtrace'))
|
|
{
|
|
throw new \RuntimeException("Your host has disabled the <code>debug_backtrace</code> PHP function. Please ask them to re-enable it. It's required for running this software.");
|
|
}
|
|
|
|
/**
|
|
* For performance reasons I look into the last 4 call stack entries. If I don't find a container I
|
|
* will expand my search by another 2 entries and so on until I either find a container or I stop
|
|
* finding new call stack entries.
|
|
*/
|
|
$lastNumberOfEntries = 0;
|
|
$limit = 4;
|
|
$skip = 0;
|
|
$container = null;
|
|
|
|
while (true)
|
|
{
|
|
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit);
|
|
|
|
if (count($backtrace) === $lastNumberOfEntries)
|
|
{
|
|
throw new \RuntimeException(__METHOD__ . ": Cannot retrieve FOF View from call stack. You are either calling me from a non-FEF extension or your PHP is broken.");
|
|
}
|
|
|
|
$lastNumberOfEntries = count($backtrace);
|
|
|
|
if ($skip)
|
|
{
|
|
$backtrace = array_slice($backtrace, $skip);
|
|
}
|
|
|
|
foreach ($backtrace as $bt)
|
|
{
|
|
if (!isset($bt['object']))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ($bt['object'] instanceof View)
|
|
{
|
|
return $bt['object'];
|
|
}
|
|
}
|
|
|
|
$skip = $limit;
|
|
$limit += 2;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get HTMLHelper options from an alternate source, e.g. a helper. This is useful for adding arbitrary options
|
|
* which are either dynamic or you do not want to inline to your view, e.g. reusable options across
|
|
* different views.
|
|
*
|
|
* The attribs can be:
|
|
* source_file The file to load. You can use FOF's URIs such as 'admin:com_foobar/foo/bar'
|
|
* source_class The class to use
|
|
* source_method The static method to use on source_class
|
|
* source_key Use * if you're returning a key/value array. Otherwise the array key for the key (ID)
|
|
* value.
|
|
* source_value Use * if you're returning a key/value array. Otherwise the array key for the displayed
|
|
* value. source_translate Should I pass the value field through Text? Default: true source_format Set
|
|
* to "optionsobject" if you're returning an array of HTMLHelper options. Ignored otherwise.
|
|
*
|
|
* @param array $attribs
|
|
*
|
|
* @return array
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
private static function getOptionsFromSource(array $attribs = []): array
|
|
{
|
|
$options = [];
|
|
|
|
$container = self::getContainerFromBacktrace();
|
|
|
|
$attribs = array_merge([
|
|
'source_file' => '',
|
|
'source_class' => '',
|
|
'source_method' => '',
|
|
'source_key' => '*',
|
|
'source_value' => '*',
|
|
'source_translate' => true,
|
|
'source_format' => '',
|
|
], $attribs);
|
|
|
|
$source_file = $attribs['source_file'];
|
|
$source_class = $attribs['source_class'];
|
|
$source_method = $attribs['source_method'];
|
|
$source_key = $attribs['source_key'];
|
|
$source_value = $attribs['source_value'];
|
|
$source_translate = $attribs['source_translate'];
|
|
$source_format = $attribs['source_format'];
|
|
|
|
if ($source_class && $source_method)
|
|
{
|
|
// Maybe we have to load a file?
|
|
if (!empty($source_file))
|
|
{
|
|
$source_file = $container->template->parsePath($source_file, true);
|
|
|
|
if ($container->filesystem->fileExists($source_file))
|
|
{
|
|
include $source_file;
|
|
}
|
|
}
|
|
|
|
// Make sure the class exists
|
|
// ...and so does the option
|
|
if (class_exists($source_class, true) && in_array($source_method, get_class_methods($source_class)))
|
|
{
|
|
// Get the data from the class
|
|
if ($source_format == 'optionsobject')
|
|
{
|
|
$options = array_merge($options, $source_class::$source_method());
|
|
}
|
|
else
|
|
{
|
|
$source_data = $source_class::$source_method();
|
|
|
|
// Loop through the data and prime the $options array
|
|
foreach ($source_data as $k => $v)
|
|
{
|
|
$key = (empty($source_key) || ($source_key == '*')) ? $k : @$v[$source_key];
|
|
$value = (empty($source_value) || ($source_value == '*')) ? $v : @$v[$source_value];
|
|
|
|
if ($source_translate)
|
|
{
|
|
$value = Text::_($value);
|
|
}
|
|
|
|
$options[] = HTMLHelper::_('FEFHelp.select.option', $key, $value, 'value', 'text');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
reset($options);
|
|
|
|
return $options;
|
|
}
|
|
|
|
/**
|
|
* Get HTMLHelper options from the values returned by a model.
|
|
*
|
|
* The params can be:
|
|
* key_field The model field used for the OPTION's key. Default: the model's ID field.
|
|
* value_field The model field used for the OPTION's displayed value. You must provide it.
|
|
* apply_access Should I apply Joomla ACLs to the model? Default: FALSE.
|
|
* none Placeholder for no selection. Default: NULL (no placeholder).
|
|
* none_as_zero When true, the 'none' placeholder applies to values '' **AND** '0' (empty string and zero)
|
|
* translate Should I pass the values through Text? Default: TRUE.
|
|
* with Array of relation names for eager loading.
|
|
* cache Cache the results for faster reuse
|
|
*
|
|
* @param string $modelName The name of the model, e.g. "items" or "com_foobar.items"
|
|
* @param array $params Parameters which define which options to get from the model
|
|
* @param array $modelState Optional state variables to pass to the model
|
|
* @param array $options Any HTMLHelper select options you want to add in front of the model's returned
|
|
* values
|
|
*
|
|
* @return mixed
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
private static function getOptionsFromModel(string $modelName, array $params = [], array $modelState = [],
|
|
array $options = []): array
|
|
{
|
|
// Let's find the FOF DI container from the call stack
|
|
$container = self::getContainerFromBacktrace();
|
|
|
|
// Explode model name into component name and prefix
|
|
$componentName = $container->componentName;
|
|
$mName = $modelName;
|
|
|
|
if (strpos($modelName, '.') !== false)
|
|
{
|
|
[$componentName, $mName] = explode('.', $mName, 2);
|
|
}
|
|
|
|
if ($componentName !== $container->componentName)
|
|
{
|
|
$container = Container::getInstance($componentName);
|
|
}
|
|
|
|
/** @var DataModel $model */
|
|
$model = $container->factory->model($mName)->setIgnoreRequest(true)->savestate(false);
|
|
|
|
$defaultParams = [
|
|
'key_field' => $model->getKeyName(),
|
|
'value_field' => 'title',
|
|
'apply_access' => false,
|
|
'none' => null,
|
|
'none_as_zero' => false,
|
|
'translate' => true,
|
|
'with' => [],
|
|
];
|
|
|
|
$params = array_merge($defaultParams, $params);
|
|
|
|
$cache = isset($params['cache']) && $params['cache'];
|
|
$cacheKey = null;
|
|
|
|
if ($cache)
|
|
{
|
|
$cacheKey = sha1(print_r([
|
|
$model->getContainer()->componentName,
|
|
$model->getName(),
|
|
$params['key_field'],
|
|
$params['value_field'],
|
|
$params['apply_access'],
|
|
$params['none'],
|
|
$params['translate'],
|
|
$params['with'],
|
|
$modelState,
|
|
], true));
|
|
}
|
|
|
|
if ($cache && isset(self::$cacheModelOptions[$cacheKey]))
|
|
{
|
|
return self::$cacheModelOptions[$cacheKey];
|
|
}
|
|
|
|
if (empty($params['none']) && !is_null($params['none']))
|
|
{
|
|
$langKey = strtoupper($model->getContainer()->componentName . '_TITLE_' . $model->getName());
|
|
$placeholder = Text::_($langKey);
|
|
|
|
if ($langKey !== $placeholder)
|
|
{
|
|
$params['none'] = '— ' . $placeholder . ' —';
|
|
}
|
|
}
|
|
|
|
if (!empty($params['none']))
|
|
{
|
|
$options[] = HTMLHelper::_('FEFHelp.select.option', null, Text::_($params['none']));
|
|
|
|
if ($params['none_as_zero'])
|
|
{
|
|
$options[] = HTMLHelper::_('FEFHelp.select.option', 0, Text::_($params['none']));
|
|
}
|
|
}
|
|
|
|
|
|
if ($params['apply_access'])
|
|
{
|
|
$model->applyAccessFiltering();
|
|
}
|
|
|
|
if (!is_null($params['with']))
|
|
{
|
|
$model->with($params['with']);
|
|
}
|
|
|
|
// Set the model's state, if applicable
|
|
foreach ($modelState as $stateKey => $stateValue)
|
|
{
|
|
$model->setState($stateKey, $stateValue);
|
|
}
|
|
|
|
// Set the query and get the result list.
|
|
$items = $model->get(true);
|
|
|
|
foreach ($items as $item)
|
|
{
|
|
$value = $item->{$params['value_field']};
|
|
|
|
if ($params['translate'])
|
|
{
|
|
$value = Text::_($value);
|
|
}
|
|
|
|
$options[] = HTMLHelper::_('FEFHelp.select.option', $item->{$params['key_field']}, $value);
|
|
}
|
|
|
|
if ($cache)
|
|
{
|
|
self::$cacheModelOptions[$cacheKey] = $options;
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
/**
|
|
* Get the FOF DI container from the backtrace of the static call. MAGIC!
|
|
*
|
|
* @return Container
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
private static function getContainerFromBacktrace(): Container
|
|
{
|
|
// In case we are on a brain-dead host
|
|
if (!function_exists('debug_backtrace'))
|
|
{
|
|
throw new \RuntimeException("Your host has disabled the <code>debug_backtrace</code> PHP function. Please ask them to re-enable it. It's required for running this software.");
|
|
}
|
|
|
|
/**
|
|
* For performance reasons I look into the last 4 call stack entries. If I don't find a container I
|
|
* will expand my search by another 2 entries and so on until I either find a container or I stop
|
|
* finding new call stack entries.
|
|
*/
|
|
$lastNumberOfEntries = 0;
|
|
$limit = 4;
|
|
$skip = 0;
|
|
$container = null;
|
|
|
|
while (true)
|
|
{
|
|
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit);
|
|
|
|
if (count($backtrace) === $lastNumberOfEntries)
|
|
{
|
|
throw new \RuntimeException(__METHOD__ . ": Cannot retrieve FOF container from call stack. You are either calling me from a non-FEF extension or your PHP is broken.");
|
|
}
|
|
|
|
$lastNumberOfEntries = count($backtrace);
|
|
|
|
if ($skip !== 0)
|
|
{
|
|
$backtrace = array_slice($backtrace, $skip);
|
|
}
|
|
|
|
foreach ($backtrace as $bt)
|
|
{
|
|
if (!isset($bt['object']))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!method_exists($bt['object'], 'getContainer'))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return $bt['object']->getContainer();
|
|
}
|
|
|
|
$skip = $limit;
|
|
$limit += 2;
|
|
}
|
|
}
|
|
|
|
}
|