[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/plugins/fields/subform/ -> subform.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Plugin
   5   * @subpackage  Fields.Subform
   6   *
   7   * @copyright   (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
   8   * @license     GNU General Public License version 2 or later; see LICENSE.txt
   9  
  10   * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
  11   */
  12  
  13  use Joomla\CMS\Factory;
  14  use Joomla\CMS\Form\Form;
  15  use Joomla\Component\Fields\Administrator\Helper\FieldsHelper;
  16  use Joomla\Component\Fields\Administrator\Plugin\FieldsPlugin;
  17  
  18  // phpcs:disable PSR1.Files.SideEffects
  19  \defined('_JEXEC') or die;
  20  // phpcs:enable PSR1.Files.SideEffects
  21  
  22  /**
  23   * Fields subform Plugin
  24   *
  25   * @since  4.0.0
  26   */
  27  class PlgFieldsSubform extends FieldsPlugin
  28  {
  29      /**
  30       * Two-dimensional array to hold to do a fast in-memory caching of rendered
  31       * subfield values.
  32       *
  33       * @var array
  34       *
  35       * @since 4.0.0
  36       */
  37      protected $renderCache = array();
  38  
  39      /**
  40       * Array to do a fast in-memory caching of all custom field items.
  41       *
  42       * @var array
  43       *
  44       * @since 4.0.0
  45       */
  46      protected static $customFieldsCache = null;
  47  
  48      /**
  49       * Handles the onContentPrepareForm event. Adds form definitions to relevant forms.
  50       *
  51       * @param   Form          $form  The form to manipulate
  52       * @param   array|object  $data  The data of the form
  53       *
  54       * @return  void
  55       *
  56       * @since  4.0.0
  57       */
  58      public function onContentPrepareForm(Form $form, $data)
  59      {
  60          // Get the path to our own form definition (basically ./params/subform.xml)
  61          $path = $this->getFormPath($form, $data);
  62  
  63          if ($path === null) {
  64              return;
  65          }
  66  
  67          // Ensure it is an object
  68          $formData = (object) $data;
  69  
  70          // Now load our own form definition into a DOMDocument, because we want to manipulate it
  71          $xml = new DOMDocument();
  72          $xml->load($path);
  73  
  74          // Prepare a DOMXPath object
  75          $xmlxpath = new DOMXPath($xml);
  76  
  77          /**
  78           * Get all fields of type "subfields" in our own XML
  79           *
  80           * @var $valuefields \DOMNodeList
  81           */
  82          $valuefields = $xmlxpath->evaluate('//field[@type="subfields"]');
  83  
  84          // If we haven't found it, something is wrong
  85          if (!$valuefields || $valuefields->length != 1) {
  86              return;
  87          }
  88  
  89          // Now iterate over those fields and manipulate them, set its parameter `context` to our context
  90          foreach ($valuefields as $valuefield) {
  91              $valuefield->setAttribute('context', $formData->context);
  92          }
  93  
  94          // When this is not a new instance (editing an existing instance)
  95          if (isset($formData->id) && $formData->id > 0) {
  96              // Don't allow the 'repeat' attribute to be edited
  97              foreach ($xmlxpath->evaluate('//field[@name="repeat"]') as $field) {
  98                  $field->setAttribute('readonly', '1');
  99              }
 100          }
 101  
 102          // And now load our manipulated form definition into the JForm
 103          $form->load($xml->saveXML(), true, '/form/*');
 104      }
 105  
 106      /**
 107       * Manipulates the $field->value before the field is being passed to
 108       * onCustomFieldsPrepareField.
 109       *
 110       * @param   string     $context  The context
 111       * @param   object     $item     The item
 112       * @param   \stdClass  $field    The field
 113       *
 114       * @return  void
 115       *
 116       * @since 4.0.0
 117       */
 118      public function onCustomFieldsBeforePrepareField($context, $item, $field)
 119      {
 120          // Check if the field should be processed by us
 121          if (!$this->isTypeSupported($field->type)) {
 122              return;
 123          }
 124  
 125          $decoded_value = json_decode($field->value, true);
 126  
 127          if (!$decoded_value || !is_array($decoded_value)) {
 128              return;
 129          }
 130  
 131          $field->value = $decoded_value;
 132      }
 133  
 134      /**
 135       * Renders this fields value by rendering all sub fields and joining all those rendered sub fields together.
 136       *
 137       * @param   string     $context  The context
 138       * @param   object     $item     The item
 139       * @param   \stdClass  $field    The field
 140       *
 141       * @return  string
 142       *
 143       * @since 4.0.0
 144       */
 145      public function onCustomFieldsPrepareField($context, $item, $field)
 146      {
 147          // Check if the field should be processed by us
 148          if (!$this->isTypeSupported($field->type)) {
 149              return;
 150          }
 151  
 152          // If we don't have any subfields (or values for them), nothing to do.
 153          if (!is_array($field->value) || count($field->value) < 1) {
 154              return;
 155          }
 156  
 157          // Get the field params
 158          $field_params = $this->getParamsFromField($field);
 159  
 160          /**
 161           * Placeholder to hold all rows (if this field is repeatable).
 162           * Each array entry is another array representing a row, containing all of the sub fields that
 163           * are valid for this row and their raw and rendered values.
 164           */
 165          $subform_rows = array();
 166  
 167          // Create an array with entries being subfields forms, and if not repeatable, containing only one element.
 168          $rows = $field->value;
 169  
 170          if ($field_params->get('repeat', '1') == '0') {
 171              $rows = array($field->value);
 172          }
 173  
 174          // Iterate over each row of the data
 175          foreach ($rows as $row) {
 176              // Holds all sub fields of this row, incl. their raw and rendered value
 177              $row_subfields = array();
 178  
 179              // For each row, iterate over all the subfields
 180              foreach ($this->getSubfieldsFromField($field) as $subfield) {
 181                  // Fill value (and rawvalue) if we have data for this subfield in the current row, otherwise set them to empty
 182                  $subfield->rawvalue = $subfield->value = $row[$subfield->name] ?? '';
 183  
 184                  // Do we want to render the value of this field, and is the value non-empty?
 185                  if ($subfield->value !== '' && $subfield->render_values == '1') {
 186                      /**
 187                       * Construct the cache-key for our renderCache. It is important that the cache key
 188                       * is as unique as possible to avoid false duplicates (e.g. type and rawvalue is not
 189                       * enough for the cache key, because type 'list' and value '1' can have different
 190                       * rendered values, depending on the list items), but it also must be as general as possible
 191                       * to not cause too many unneeded rendering processes (e.g. the type 'text' will always be
 192                       * rendered the same when it has the same rawvalue).
 193                       */
 194                      $renderCache_key = serialize(
 195                          array(
 196                              $subfield->type,
 197                              $subfield->id,
 198                              $subfield->rawvalue,
 199                          )
 200                      );
 201  
 202                      // Let's see if we have a fast in-memory result for this
 203                      if (isset($this->renderCache[$renderCache_key])) {
 204                          $subfield->value = $this->renderCache[$renderCache_key];
 205                      } else {
 206                          // Render this virtual subfield
 207                          $subfield->value = Factory::getApplication()->triggerEvent(
 208                              'onCustomFieldsPrepareField',
 209                              array($context, $item, $subfield)
 210                          );
 211                          $this->renderCache[$renderCache_key] = $subfield->value;
 212                      }
 213                  }
 214  
 215                  // Flatten the value if it is an array (list, checkboxes, etc.) [independent of render_values]
 216                  if (is_array($subfield->value)) {
 217                      $subfield->value = implode(' ', $subfield->value);
 218                  }
 219  
 220                  // Store the subfield (incl. its raw and rendered value) into this rows sub fields
 221                  $row_subfields[$subfield->fieldname] = $subfield;
 222              }
 223  
 224              // Store all the sub fields of this row
 225              $subform_rows[] = $row_subfields;
 226          }
 227  
 228          // Store all the rows and their corresponding sub fields in $field->subform_rows
 229          $field->subform_rows = $subform_rows;
 230  
 231          // Call our parent to combine all those together for the final $field->value
 232          return parent::onCustomFieldsPrepareField($context, $item, $field);
 233      }
 234  
 235      /**
 236       * Returns a DOMElement which is the child of $parent and represents
 237       * the form XML definition for this field.
 238       *
 239       * @param   \stdClass   $field   The field
 240       * @param   DOMElement  $parent  The original parent element
 241       * @param   Form        $form    The form
 242       *
 243       * @return  \DOMElement
 244       *
 245       * @since 4.0.0
 246       */
 247      public function onCustomFieldsPrepareDom($field, DOMElement $parent, Form $form)
 248      {
 249          // Call the onCustomFieldsPrepareDom method on FieldsPlugin
 250          $parent_field = parent::onCustomFieldsPrepareDom($field, $parent, $form);
 251  
 252          if (!$parent_field) {
 253              return $parent_field;
 254          }
 255  
 256          // Override the fieldname attribute of the subform - this is being used to index the rows
 257          $parent_field->setAttribute('fieldname', 'row');
 258  
 259          // If the user configured this subform instance as required
 260          if ($field->required) {
 261              // Then we need to have at least one row
 262              $parent_field->setAttribute('min', '1');
 263          }
 264  
 265          // Get the configured parameters for this field
 266          $field_params = $this->getParamsFromField($field);
 267  
 268          // If this fields should be repeatable, set some attributes on the subform element
 269          if ($field_params->get('repeat', '1') == '1') {
 270              $parent_field->setAttribute('multiple', 'true');
 271              $parent_field->setAttribute('layout', 'joomla.form.field.subform.repeatable-table');
 272          }
 273  
 274          // Create a child 'form' DOMElement under the field[type=subform] element.
 275          $parent_fieldset = $parent_field->appendChild(new DOMElement('form'));
 276          $parent_fieldset->setAttribute('hidden', 'true');
 277          $parent_fieldset->setAttribute('name', ($field->name . '_modal'));
 278  
 279          if ($field_params->get('max_rows')) {
 280              $parent_field->setAttribute('max', $field_params->get('max_rows'));
 281          }
 282  
 283          // If this field should be repeatable, set some attributes on the modal
 284          if ($field_params->get('repeat', '1') == '1') {
 285              $parent_fieldset->setAttribute('repeat', 'true');
 286          }
 287  
 288          // Get the configured sub fields for this field
 289          $subfields = $this->getSubfieldsFromField($field);
 290  
 291          // If we have 5 or more of them, use the `repeatable` layout instead of the `repeatable-table`
 292          if (count($subfields) >= 5) {
 293              $parent_field->setAttribute('layout', 'joomla.form.field.subform.repeatable');
 294          }
 295  
 296          // Iterate over the sub fields to call prepareDom on each of those sub-fields
 297          foreach ($subfields as $subfield) {
 298              // Let the relevant plugins do their work and insert the correct
 299              // DOMElement's into our $parent_fieldset.
 300              Factory::getApplication()->triggerEvent(
 301                  'onCustomFieldsPrepareDom',
 302                  array($subfield, $parent_fieldset, $form)
 303              );
 304          }
 305  
 306          return $parent_field;
 307      }
 308  
 309      /**
 310       * Returns an array of all options configured for this field.
 311       *
 312       * @param   \stdClass  $field  The field
 313       *
 314       * @return  \stdClass[]
 315       *
 316       * @since 4.0.0
 317       */
 318      protected function getOptionsFromField(\stdClass $field)
 319      {
 320          $result = array();
 321  
 322          // Fetch the options from the plugin
 323          $params = $this->getParamsFromField($field);
 324  
 325          foreach ($params->get('options', array()) as $option) {
 326              $result[] = (object) $option;
 327          }
 328  
 329          return $result;
 330      }
 331  
 332      /**
 333       * Returns the configured params for a given field.
 334       *
 335       * @param   \stdClass  $field  The field
 336       *
 337       * @return  \Joomla\Registry\Registry
 338       *
 339       * @since 4.0.0
 340       */
 341      protected function getParamsFromField(\stdClass $field)
 342      {
 343          $params = (clone $this->params);
 344  
 345          if (isset($field->fieldparams) && is_object($field->fieldparams)) {
 346              $params->merge($field->fieldparams);
 347          }
 348  
 349          return $params;
 350      }
 351  
 352      /**
 353       * Returns an array of all subfields for a given field. This will always return a bare clone
 354       * of a sub field, so manipulating it is safe.
 355       *
 356       * @param   \stdClass  $field  The field
 357       *
 358       * @return  \stdClass[]
 359       *
 360       * @since 4.0.0
 361       */
 362      protected function getSubfieldsFromField(\stdClass $field)
 363      {
 364          if (static::$customFieldsCache === null) {
 365              // Prepare our cache
 366              static::$customFieldsCache = array();
 367  
 368              // Get all custom field instances
 369              $customFields = FieldsHelper::getFields('', null, false, null, true);
 370  
 371              foreach ($customFields as $customField) {
 372                  // Store each custom field instance in our cache with its id as key
 373                  static::$customFieldsCache[$customField->id] = $customField;
 374              }
 375          }
 376  
 377          $result = array();
 378  
 379          // Iterate over all configured options for this field
 380          foreach ($this->getOptionsFromField($field) as $option) {
 381              // Check whether the wanted sub field really is an existing custom field
 382              if (!isset(static::$customFieldsCache[$option->customfield])) {
 383                  continue;
 384              }
 385  
 386              // Get a clone of the sub field, so we and the caller can do some manipulation with it.
 387              $cur_field = (clone static::$customFieldsCache[$option->customfield]);
 388  
 389              // Manipulate it and add our custom configuration to it
 390              $cur_field->render_values = $option->render_values;
 391  
 392              /**
 393               * Set the name of the sub field to its id so that the values in the database are being saved
 394               * based on the id of the sub fields, not on their name. Actually we do not need the name of
 395               * the sub fields to render them, but just to make sure we have the name when we need it, we
 396               * store it as `fieldname`.
 397               */
 398              $cur_field->fieldname = $cur_field->name;
 399              $cur_field->name = 'field' . $cur_field->id;
 400  
 401              // And add it to our result
 402              $result[] = $cur_field;
 403          }
 404  
 405          return $result;
 406      }
 407  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer