Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 39 and 401]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Persistent form abstract.
  19   *
  20   * @package    core
  21   * @copyright  2015 Frédéric Massart - FMCorz.net
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core\form;
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  use coding_exception;
  29  use moodleform;
  30  use stdClass;
  31  
  32  require_once($CFG->libdir.'/formslib.php');
  33  
  34  /**
  35   * Persistent form abstract class.
  36   *
  37   * This provides some shortcuts to validate objects based on the persistent model.
  38   *
  39   * Note that all mandatory fields (non-optional) of your model should be included in the
  40   * form definition. Mandatory fields which are not editable by the user should be
  41   * as hidden and constant.
  42   *
  43   *    $mform->addElement('hidden', 'userid');
  44   *    $mform->setType('userid', PARAM_INT);
  45   *    $mform->setConstant('userid', $this->_customdata['userid']);
  46   *
  47   * You may exclude some fields from the validation should your form include other
  48   * properties such as files. To do so use the $foreignfields property.
  49   *
  50   * @package    core
  51   * @copyright  2015 Frédéric Massart - FMCorz.net
  52   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  53   */
  54  abstract class persistent extends moodleform {
  55  
  56      /** @var string The fully qualified classname. */
  57      protected static $persistentclass = null;
  58  
  59      /** @var array Fields to remove when getting the final data. */
  60      protected static $fieldstoremove = array('submitbutton');
  61  
  62      /** @var array Fields to remove from the persistent validation. */
  63      protected static $foreignfields = array();
  64  
  65      /** @var \core\peristent Reference to the persistent. */
  66      private $persistent = null;
  67  
  68      /**
  69       * Constructor.
  70       *
  71       * The 'persistent' has to be passed as custom data when 'editing'.
  72       *
  73       * Note that in order for your persistent to be reloaded after form submission you should
  74       * either override the URL to include the ID to your resource, or add the ID to the form
  75       * fields.
  76       *
  77       * @param mixed $action
  78       * @param mixed $customdata
  79       * @param string $method
  80       * @param string $target
  81       * @param mixed $attributes
  82       * @param bool $editable
  83       * @param array $ajaxformdata
  84       */
  85      public function __construct($action = null, $customdata = null, $method = 'post', $target = '',
  86                                  $attributes = null, $editable = true, $ajaxformdata=null) {
  87          if (empty(static::$persistentclass)) {
  88              throw new coding_exception('Static property $persistentclass must be set.');
  89          } else if (!is_subclass_of(static::$persistentclass, 'core\\persistent')) {
  90              throw new coding_exception('Static property $persistentclass is not valid.');
  91          } else if (!array_key_exists('persistent', $customdata)) {
  92              throw new coding_exception('The custom data \'persistent\' key must be set, even if it is null.');
  93          }
  94  
  95          // Make a copy of the persistent passed, this ensures validation and object reference issues.
  96          $persistendata = new stdClass();
  97          $persistent = isset($customdata['persistent']) ? $customdata['persistent'] : null;
  98          if ($persistent) {
  99              if (!($persistent instanceof static::$persistentclass)) {
 100                  throw new coding_exception('Invalid persistent');
 101              }
 102              $persistendata = $persistent->to_record();
 103              unset($persistent);
 104          }
 105  
 106          $this->persistent = new static::$persistentclass();
 107          $this->persistent->from_record($persistendata);
 108  
 109          unset($customdata['persistent']);
 110          parent::__construct($action, $customdata, $method, $target, $attributes, $editable, $ajaxformdata);
 111  
 112          // Load the defaults.
 113          $this->set_data($this->get_default_data());
 114      }
 115  
 116      /**
 117       * Convert some fields.
 118       *
 119       * @param  stdClass $data The whole data set.
 120       * @return stdClass The amended data set.
 121       */
 122      protected static function convert_fields(stdClass $data) {
 123          $class = static::$persistentclass;
 124          $properties = $class::get_formatted_properties();
 125  
 126          foreach ($data as $field => $value) {
 127              // Replace formatted properties.
 128              if (isset($properties[$field])) {
 129                  $formatfield = $properties[$field];
 130                  $data->$formatfield = $data->{$field}['format'];
 131                  $data->$field = $data->{$field}['text'];
 132              }
 133          }
 134  
 135          return $data;
 136      }
 137  
 138      /**
 139       * After definition hook.
 140       *
 141       * Automatically try to set the types of simple fields using the persistent properties definition.
 142       * This only applies to hidden, text and url types. Groups are also ignored as they are most likely custom.
 143       *
 144       * @return void
 145       */
 146      protected function after_definition() {
 147          parent::after_definition();
 148          $mform = $this->_form;
 149  
 150          $class = static::$persistentclass;
 151          $properties = $class::properties_definition();
 152  
 153          foreach ($mform->_elements as $element) {
 154              $name = $element->getName();
 155  
 156              if (isset($mform->_types[$name])) {
 157                  // We already have a PARAM_* type for this field.
 158                  continue;
 159  
 160              } else if (!isset($properties[$name]) || in_array($name, static::$fieldstoremove)
 161                      || in_array($name, static::$foreignfields)) {
 162                  // Ignoring foreign and unknown fields.
 163                  continue;
 164              }
 165  
 166              // Set the type on the element.
 167              switch ($element->getType()) {
 168                  case 'hidden':
 169                  case 'text':
 170                  case 'url':
 171                      $mform->setType($name, $properties[$name]['type']);
 172                      break;
 173              }
 174          }
 175      }
 176  
 177      /**
 178       * Define extra validation mechanims.
 179       *
 180       * The data here:
 181       * - does not include {@link self::$fieldstoremove}.
 182       * - does include {@link self::$foreignfields}.
 183       * - was converted to map persistent-like data, e.g. array $description to string $description + int $descriptionformat.
 184       *
 185       * You can modify the $errors parameter in order to remove some validation errors should you
 186       * need to. However, the best practice is to return new or overriden errors. Only modify the
 187       * errors passed by reference when you have no other option.
 188       *
 189       * Do not add any logic here, it is only intended to be used by child classes.
 190       *
 191       * @param  stdClass $data Data to validate.
 192       * @param  array $files Array of files.
 193       * @param  array $errors Currently reported errors.
 194       * @return array of additional errors, or overridden errors.
 195       */
 196      protected function extra_validation($data, $files, array &$errors) {
 197          return array();
 198      }
 199  
 200      /**
 201       * Filter out the foreign fields of the persistent.
 202       *
 203       * This can be overridden to filter out more complex fields.
 204       *
 205       * @param stdClass $data The data to filter the fields out of.
 206       * @return stdClass.
 207       */
 208      protected function filter_data_for_persistent($data) {
 209          return (object) array_diff_key((array) $data, array_flip((array) static::$foreignfields));
 210      }
 211  
 212      /**
 213       * Get the default data.
 214       *
 215       * This is the data that is prepopulated in the form at it loads, we automatically
 216       * fetch all the properties of the persistent however some needs to be converted
 217       * to map the form structure.
 218       *
 219       * Extend this class if you need to add more conversion.
 220       *
 221       * @return stdClass
 222       */
 223      protected function get_default_data() {
 224          $data = $this->get_persistent()->to_record();
 225          $class = static::$persistentclass;
 226          $properties = $class::get_formatted_properties();
 227          $allproperties = $class::properties_definition();
 228  
 229          foreach ($data as $field => $value) {
 230              // Clean data if it is to be displayed in a form.
 231              if (isset($allproperties[$field]['type'])) {
 232                  $data->$field = clean_param($data->$field, $allproperties[$field]['type']);
 233              }
 234  
 235              if (isset($properties[$field])) {
 236                  $data->$field = array(
 237                      'text' => $data->$field,
 238                      'format' => $data->{$properties[$field]}
 239                  );
 240                  unset($data->{$properties[$field]});
 241              }
 242          }
 243  
 244          return $data;
 245      }
 246  
 247      /**
 248       * Get form data.
 249       *
 250       * Conveniently removes non-desired properties and add the ID property.
 251       *
 252       * @return object|null
 253       */
 254      public function get_data() {
 255          $data = parent::get_data();
 256          if (is_object($data)) {
 257              foreach (static::$fieldstoremove as $field) {
 258                  unset($data->{$field});
 259              }
 260              $data = static::convert_fields($data);
 261  
 262              // Ensure that the ID is set.
 263              $data->id = $this->persistent->get('id');
 264          }
 265          return $data;
 266      }
 267  
 268      /**
 269       * Return the persistent object associated with this form instance.
 270       *
 271       * @return \core\persistent
 272       */
 273      final protected function get_persistent() {
 274          return $this->persistent;
 275      }
 276  
 277      /**
 278       * Get the submitted form data.
 279       *
 280       * Conveniently removes non-desired properties.
 281       *
 282       * @return object|null
 283       */
 284      public function get_submitted_data() {
 285          $data = parent::get_submitted_data();
 286          if (is_object($data)) {
 287              foreach (static::$fieldstoremove as $field) {
 288                  unset($data->{$field});
 289              }
 290              $data = static::convert_fields($data);
 291          }
 292          return $data;
 293      }
 294  
 295      /**
 296       * Form validation.
 297       *
 298       * If you need extra validation, use {@link self::extra_validation()}.
 299       *
 300       * @param  array $data
 301       * @param  array $files
 302       * @return array
 303       */
 304      public final function validation($data, $files) {
 305          $errors = parent::validation($data, $files);
 306          $data = $this->get_submitted_data();
 307  
 308          // Only validate compatible fields.
 309          $persistentdata = $this->filter_data_for_persistent($data);
 310          $persistent = $this->get_persistent();
 311          $persistent->from_record((object) $persistentdata);
 312          $errors = array_merge($errors, $persistent->get_errors());
 313  
 314          // Apply extra validation.
 315          $extraerrors = $this->extra_validation($data, $files, $errors);
 316          $errors = array_merge($errors, (array) $extraerrors);
 317  
 318          return $errors;
 319      }
 320  }