[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |