Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

   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   * Generic exporter to take a stdClass and prepare it for return by webservice.
  19   *
  20   * @package    core
  21   * @copyright  2015 Damyon Wiese
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  namespace core\external;
  25  
  26  use stdClass;
  27  use renderer_base;
  28  use context;
  29  use coding_exception;
  30  use core_external\external_format_value;
  31  use core_external\external_multiple_structure;
  32  use core_external\external_single_structure;
  33  use core_external\external_value;
  34  
  35  /**
  36   * Generic exporter to take a stdClass and prepare it for return by webservice, or as the context for a template.
  37   *
  38   * templatable classes implementing export_for_template, should always use a standard exporter if it exists.
  39   * External functions should always use a standard exporter if it exists.
  40   *
  41   * @copyright  2015 Damyon Wiese
  42   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  43   */
  44  abstract class exporter {
  45  
  46      /** @var array $related List of related objects used to avoid DB queries. */
  47      protected $related = array();
  48  
  49      /** @var stdClass|array The data of this exporter. */
  50      protected $data = null;
  51  
  52      /**
  53       * Constructor - saves the persistent object, and the related objects.
  54       *
  55       * @param mixed $data - Either an stdClass or an array of values.
  56       * @param array $related - An optional list of pre-loaded objects related to this object.
  57       */
  58      public function __construct($data, $related = array()) {
  59          $this->data = $data;
  60          // Cache the valid related objects.
  61          foreach (static::define_related() as $key => $classname) {
  62              $isarray = false;
  63              $nullallowed = false;
  64  
  65              // Allow ? to mean null is allowed.
  66              if (substr($classname, -1) === '?') {
  67                  $classname = substr($classname, 0, -1);
  68                  $nullallowed = true;
  69              }
  70  
  71              // Allow [] to mean an array of values.
  72              if (substr($classname, -2) === '[]') {
  73                  $classname = substr($classname, 0, -2);
  74                  $isarray = true;
  75              }
  76  
  77              $missingdataerr = 'Exporter class is missing required related data: (' . get_called_class() . ') ';
  78              $scalartypes = ['string', 'int', 'bool', 'float'];
  79              $scalarcheck = 'is_' . $classname;
  80  
  81              if ($nullallowed && (!array_key_exists($key, $related) || $related[$key] === null)) {
  82                  $this->related[$key] = null;
  83  
  84              } else if ($isarray) {
  85                  if (array_key_exists($key, $related) && is_array($related[$key])) {
  86                      foreach ($related[$key] as $index => $value) {
  87                          if (!$value instanceof $classname && !$scalarcheck($value)) {
  88                              throw new coding_exception($missingdataerr . $key . ' => ' . $classname . '[]');
  89                          }
  90                      }
  91                      $this->related[$key] = $related[$key];
  92                  } else {
  93                      throw new coding_exception($missingdataerr . $key . ' => ' . $classname . '[]');
  94                  }
  95  
  96              } else {
  97                  if (array_key_exists($key, $related) &&
  98                          ((in_array($classname, $scalartypes) && $scalarcheck($related[$key])) ||
  99                          ($related[$key] instanceof $classname))) {
 100                      $this->related[$key] = $related[$key];
 101                  } else {
 102                      throw new coding_exception($missingdataerr . $key . ' => ' . $classname);
 103                  }
 104              }
 105          }
 106      }
 107  
 108      /**
 109       * Function to export the renderer data in a format that is suitable for a
 110       * mustache template. This means raw records are generated as in to_record,
 111       * but all strings are correctly passed through \core_external\util::format_text (or \core_external\util::format_string).
 112       *
 113       * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
 114       * @return stdClass
 115       */
 116      final public function export(renderer_base $output) {
 117          $data = new stdClass();
 118          $properties = self::read_properties_definition();
 119          $values = (array) $this->data;
 120  
 121          $othervalues = $this->get_other_values($output);
 122          if (array_intersect_key($values, $othervalues)) {
 123              // Attempt to replace a standard property.
 124              throw new coding_exception('Cannot override a standard property value.');
 125          }
 126          $values += $othervalues;
 127          $record = (object) $values;
 128  
 129          foreach ($properties as $property => $definition) {
 130              if (isset($data->$property)) {
 131                  // This happens when we have already defined the format properties.
 132                  continue;
 133              } else if (!property_exists($record, $property) && array_key_exists('default', $definition)) {
 134                  // We have a default value for this property.
 135                  $record->$property = $definition['default'];
 136              } else if (!property_exists($record, $property) && !empty($definition['optional'])) {
 137                  // Fine, this property can be omitted.
 138                  continue;
 139              } else if (!property_exists($record, $property)) {
 140                  // Whoops, we got something that wasn't defined.
 141                  throw new coding_exception('Unexpected property ' . $property);
 142              }
 143  
 144              $data->$property = $record->$property;
 145  
 146              // If the field is PARAM_RAW and has a format field.
 147              if ($propertyformat = self::get_format_field($properties, $property)) {
 148                  if (!property_exists($record, $propertyformat)) {
 149                      // Whoops, we got something that wasn't defined.
 150                      throw new coding_exception('Unexpected property ' . $propertyformat);
 151                  }
 152  
 153                  $formatparams = $this->get_format_parameters($property);
 154                  $format = $record->$propertyformat;
 155  
 156                  list($text, $format) = \core_external\util::format_text($data->$property, $format, $formatparams['context'],
 157                      $formatparams['component'], $formatparams['filearea'], $formatparams['itemid'], $formatparams['options']);
 158  
 159                  $data->$property = $text;
 160                  $data->$propertyformat = $format;
 161  
 162              } else if ($definition['type'] === PARAM_TEXT) {
 163                  $formatparams = $this->get_format_parameters($property);
 164  
 165                  if (!empty($definition['multiple'])) {
 166                      foreach ($data->$property as $key => $value) {
 167                          $data->{$property}[$key] = \core_external\util::format_string($value, $formatparams['context'],
 168                              $formatparams['striplinks'], $formatparams['options']);
 169                      }
 170                  } else {
 171                      $data->$property = \core_external\util::format_string($data->$property, $formatparams['context'],
 172                              $formatparams['striplinks'], $formatparams['options']);
 173                  }
 174              }
 175          }
 176  
 177          return $data;
 178      }
 179  
 180      /**
 181       * Get the format parameters.
 182       *
 183       * This method returns the parameters to use with the functions \core_external\util::format_text(), and
 184       * \core_external\util::format_string(). To override the default parameters, you can define a protected method
 185       * called 'get_format_parameters_for_<propertyName>'. For example, 'get_format_parameters_for_description',
 186       * if your property is 'description'.
 187       *
 188       * Your method must return an array containing any of the following keys:
 189       * - context: The context to use. Defaults to $this->related['context'] if defined, else throws an exception.
 190       * - component: The component to use with \core_external\util::format_text(). Defaults to null.
 191       * - filearea: The filearea to use with \core_external\util::format_text(). Defaults to null.
 192       * - itemid: The itemid to use with \core_external\util::format_text(). Defaults to null.
 193       * - options: An array of options accepted by \core_external\util::format_text()
 194       *            or \core_external\util::format_string().
 195       *            Defaults to [].
 196       * - striplinks: Whether to strip the links with \core_external\util::format_string(). Defaults to true.
 197       *
 198       * @param string $property The property to get the parameters for.
 199       * @return array
 200       */
 201      final protected function get_format_parameters($property) {
 202          $parameters = [
 203              'component' => null,
 204              'filearea' => null,
 205              'itemid' => null,
 206              'options' => [],
 207              'striplinks' => true,
 208          ];
 209  
 210          $candidate = 'get_format_parameters_for_' . $property;
 211          if (method_exists($this, $candidate)) {
 212              $parameters = array_merge($parameters, $this->{$candidate}());
 213          }
 214  
 215          if (!isset($parameters['context'])) {
 216              if (!isset($this->related['context']) || !($this->related['context'] instanceof context)) {
 217                  throw new coding_exception("Unknown context to use for formatting the property '$property' in the " .
 218                      "exporter '" . get_class($this) . "'. You either need to add 'context' to your related objects, " .
 219                      "or create the method '$candidate' and return the context from there.");
 220              }
 221              $parameters['context'] = $this->related['context'];
 222  
 223          } else if (!($parameters['context'] instanceof context)) {
 224              throw new coding_exception("The context given to format the property '$property' in the exporter '" .
 225                  get_class($this) . "' is invalid.");
 226          }
 227  
 228          return $parameters;
 229      }
 230  
 231      /**
 232       * Get the additional values to inject while exporting.
 233       *
 234       * These are additional generated values that are not passed in through $data
 235       * to the exporter. For a persistent exporter - these are generated values that
 236       * do not exist in the persistent class. For your convenience the format_text or
 237       * format_string functions do not need to be applied to PARAM_TEXT fields,
 238       * it will be done automatically during export.
 239       *
 240       * These values are only used when returning data via {@link self::export()},
 241       * they are not used when generating any of the different external structures.
 242       *
 243       * Note: These must be defined in {@link self::define_other_properties()}.
 244       *
 245       * @param renderer_base $output The renderer.
 246       * @return array Keys are the property names, values are their values.
 247       */
 248      protected function get_other_values(renderer_base $output) {
 249          return array();
 250      }
 251  
 252      /**
 253       * Get the read properties definition of this exporter. Read properties combines the
 254       * default properties from the model (persistent or stdClass) with the properties defined
 255       * by {@link self::define_other_properties()}.
 256       *
 257       * @return array Keys are the property names, and value their definition.
 258       */
 259      final public static function read_properties_definition() {
 260          $properties = static::properties_definition();
 261          $customprops = static::define_other_properties();
 262          $customprops = static::format_properties($customprops);
 263          $properties += $customprops;
 264          return $properties;
 265      }
 266  
 267      /**
 268       * Recursively formats a given property definition with the default fields required.
 269       *
 270       * @param array $properties List of properties to format
 271       * @return array Formatted array
 272       */
 273      final public static function format_properties($properties) {
 274          foreach ($properties as $property => $definition) {
 275              // Ensures that null is set to its default.
 276              if (!isset($definition['null'])) {
 277                  $properties[$property]['null'] = NULL_NOT_ALLOWED;
 278              }
 279              if (!isset($definition['description'])) {
 280                  $properties[$property]['description'] = $property;
 281              }
 282  
 283              // If an array is provided, it may be a nested array that is unformatted so rinse and repeat.
 284              if (is_array($definition['type'])) {
 285                  $properties[$property]['type'] = static::format_properties($definition['type']);
 286              }
 287          }
 288          return $properties;
 289      }
 290  
 291      /**
 292       * Get the properties definition of this exporter used for create, and update structures.
 293       * The read structures are returned by: {@link self::read_properties_definition()}.
 294       *
 295       * @return array Keys are the property names, and value their definition.
 296       */
 297      final public static function properties_definition() {
 298          $properties = static::define_properties();
 299          foreach ($properties as $property => $definition) {
 300              // Ensures that null is set to its default.
 301              if (!isset($definition['null'])) {
 302                  $properties[$property]['null'] = NULL_NOT_ALLOWED;
 303              }
 304              if (!isset($definition['description'])) {
 305                  $properties[$property]['description'] = $property;
 306              }
 307          }
 308          return $properties;
 309      }
 310  
 311      /**
 312       * Return the list of additional properties used only for display.
 313       *
 314       * Additional properties are only ever used for the read structure, and during
 315       * export of the persistent data.
 316       *
 317       * The format of the array returned by this method has to match the structure
 318       * defined in {@link \core\persistent::define_properties()}. The display properties
 319       * can however do some more fancy things. They can define 'multiple' => true to wrap
 320       * values in an external_multiple_structure automatically - or they can define the
 321       * type as a nested array of more properties in order to generate a nested
 322       * external_single_structure.
 323       *
 324       * You can specify an array of values by including a 'multiple' => true array value. This
 325       * will result in a nested external_multiple_structure.
 326       * E.g.
 327       *
 328       *       'arrayofbools' => array(
 329       *           'type' => PARAM_BOOL,
 330       *           'multiple' => true
 331       *       ),
 332       *
 333       * You can return a nested array in the type field, which will result in a nested external_single_structure.
 334       * E.g.
 335       *      'competency' => array(
 336       *          'type' => competency_exporter::read_properties_definition()
 337       *       ),
 338       *
 339       * Other properties can be specifically marked as optional, in which case they do not need
 340       * to be included in the export in {@link self::get_other_values()}. This is useful when exporting
 341       * a substructure which cannot be set as null due to webservices protocol constraints.
 342       * E.g.
 343       *      'competency' => array(
 344       *          'type' => competency_exporter::read_properties_definition(),
 345       *          'optional' => true
 346       *       ),
 347       *
 348       * @return array
 349       */
 350      protected static function define_other_properties() {
 351          return array();
 352      }
 353  
 354      /**
 355       * Return the list of properties.
 356       *
 357       * The format of the array returned by this method has to match the structure
 358       * defined in {@link \core\persistent::define_properties()}. Howewer you can
 359       * add a new attribute "description" to describe the parameter for documenting the API.
 360       *
 361       * Note that the type PARAM_TEXT should ONLY be used for strings which need to
 362       * go through filters (multilang, etc...) and do not have a FORMAT_* associated
 363       * to them. Typically strings passed through to format_string().
 364       *
 365       * Other filtered strings which use a FORMAT_* constant (hear used with format_text)
 366       * must be defined as PARAM_RAW.
 367       *
 368       * @return array
 369       */
 370      protected static function define_properties() {
 371          return array();
 372      }
 373  
 374      /**
 375       * Returns a list of objects that are related to this persistent.
 376       *
 377       * Only objects listed here can be cached in this object.
 378       *
 379       * The class name can be suffixed:
 380       * - with [] to indicate an array of values.
 381       * - with ? to indicate that 'null' is allowed.
 382       *
 383       * @return array of 'propertyname' => array('type' => classname, 'required' => true)
 384       */
 385      protected static function define_related() {
 386          return array();
 387      }
 388  
 389      /**
 390       * Get the context structure.
 391       *
 392       * @return array
 393       */
 394      final protected static function get_context_structure() {
 395          return array(
 396              'contextid' => new external_value(PARAM_INT, 'The context id', VALUE_OPTIONAL),
 397              'contextlevel' => new external_value(PARAM_ALPHA, 'The context level', VALUE_OPTIONAL),
 398              'instanceid' => new external_value(PARAM_INT, 'The Instance id', VALUE_OPTIONAL),
 399          );
 400      }
 401  
 402      /**
 403       * Get the format field name.
 404       *
 405       * @param  array $definitions List of properties definitions.
 406       * @param  string $property The name of the property that may have a format field.
 407       * @return bool|string False, or the name of the format property.
 408       */
 409      final protected static function get_format_field($definitions, $property) {
 410          $formatproperty = $property . 'format';
 411          if (($definitions[$property]['type'] == PARAM_RAW || $definitions[$property]['type'] == PARAM_CLEANHTML)
 412                  && isset($definitions[$formatproperty])
 413                  && $definitions[$formatproperty]['type'] == PARAM_INT) {
 414              return $formatproperty;
 415          }
 416          return false;
 417      }
 418  
 419      /**
 420       * Get the format structure.
 421       *
 422       * @param  string $property   The name of the property on which the format applies.
 423       * @param  array  $definition The definition of the format property.
 424       * @param  int    $required   Constant VALUE_*.
 425       * @return external_format_value
 426       */
 427      final protected static function get_format_structure($property, $definition, $required = VALUE_REQUIRED) {
 428          if (array_key_exists('default', $definition)) {
 429              $required = VALUE_DEFAULT;
 430          }
 431          return new external_format_value($property, $required);
 432      }
 433  
 434      /**
 435       * Returns the create structure.
 436       *
 437       * @return external_single_structure
 438       */
 439      final public static function get_create_structure() {
 440          $properties = self::properties_definition();
 441          $returns = array();
 442  
 443          foreach ($properties as $property => $definition) {
 444              if ($property == 'id') {
 445                  // The can not be set on create.
 446                  continue;
 447  
 448              } else if (isset($returns[$property]) && substr($property, -6) === 'format') {
 449                  // We've already treated the format.
 450                  continue;
 451              }
 452  
 453              $required = VALUE_REQUIRED;
 454              $default = null;
 455  
 456              // We cannot use isset here because we want to detect nulls.
 457              if (array_key_exists('default', $definition)) {
 458                  $required = VALUE_DEFAULT;
 459                  $default = $definition['default'];
 460              }
 461  
 462              // Magically treat the contextid fields.
 463              if ($property == 'contextid') {
 464                  if (isset($properties['context'])) {
 465                      throw new coding_exception('There cannot be a context and a contextid column');
 466                  }
 467                  $returns += self::get_context_structure();
 468  
 469              } else {
 470                  $returns[$property] = new external_value($definition['type'], $definition['description'], $required, $default,
 471                      $definition['null']);
 472  
 473                  // Magically treat the format properties.
 474                  if ($formatproperty = self::get_format_field($properties, $property)) {
 475                      if (isset($returns[$formatproperty])) {
 476                          throw new coding_exception('The format for \'' . $property . '\' is already defined.');
 477                      }
 478                      $returns[$formatproperty] = self::get_format_structure($property,
 479                          $properties[$formatproperty], VALUE_REQUIRED);
 480                  }
 481              }
 482          }
 483  
 484          return new external_single_structure($returns);
 485      }
 486  
 487      /**
 488       * Returns the read structure.
 489       *
 490       * @param int $required Whether is required.
 491       * @param mixed $default The default value.
 492       *
 493       * @return external_single_structure
 494       */
 495      final public static function get_read_structure($required = VALUE_REQUIRED, $default = null) {
 496          $properties = self::read_properties_definition();
 497  
 498          return self::get_read_structure_from_properties($properties, $required, $default);
 499      }
 500  
 501      /**
 502       * Returns the read structure from a set of properties (recursive).
 503       *
 504       * @param array $properties The properties.
 505       * @param int $required Whether is required.
 506       * @param mixed $default The default value.
 507       * @return external_single_structure
 508       */
 509      final protected static function get_read_structure_from_properties($properties, $required = VALUE_REQUIRED, $default = null) {
 510          $returns = array();
 511          foreach ($properties as $property => $definition) {
 512              if (isset($returns[$property]) && substr($property, -6) === 'format') {
 513                  // We've already treated the format.
 514                  continue;
 515              }
 516              $thisvalue = null;
 517  
 518              $type = $definition['type'];
 519              $proprequired = VALUE_REQUIRED;
 520              $propdefault = null;
 521              if (array_key_exists('default', $definition)) {
 522                  $propdefault = $definition['default'];
 523              }
 524              if (array_key_exists('optional', $definition)) {
 525                  // Mark as optional. Note that this should only apply to "reading" "other" properties.
 526                  $proprequired = VALUE_OPTIONAL;
 527              }
 528  
 529              if (is_array($type)) {
 530                  // This is a nested array of more properties.
 531                  $thisvalue = self::get_read_structure_from_properties($type, $proprequired, $propdefault);
 532              } else {
 533                  if ($definition['type'] == PARAM_TEXT || $definition['type'] == PARAM_CLEANHTML) {
 534                      // PARAM_TEXT always becomes PARAM_RAW because filters may be applied.
 535                      $type = PARAM_RAW;
 536                  }
 537                  $thisvalue = new external_value($type, $definition['description'], $proprequired, $propdefault, $definition['null']);
 538              }
 539              if (!empty($definition['multiple'])) {
 540                  $returns[$property] = new external_multiple_structure($thisvalue, $definition['description'], $proprequired,
 541                      $propdefault);
 542              } else {
 543                  $returns[$property] = $thisvalue;
 544  
 545                  // Magically treat the format properties (not possible for arrays).
 546                  if ($formatproperty = self::get_format_field($properties, $property)) {
 547                      if (isset($returns[$formatproperty])) {
 548                          throw new coding_exception('The format for \'' . $property . '\' is already defined.');
 549                      }
 550                      $returns[$formatproperty] = self::get_format_structure($property, $properties[$formatproperty]);
 551                  }
 552              }
 553          }
 554  
 555          return new external_single_structure($returns, '', $required, $default);
 556      }
 557  
 558      /**
 559       * Returns the update structure.
 560       *
 561       * This structure can never be included at the top level for an external function signature
 562       * because it contains optional parameters.
 563       *
 564       * @return external_single_structure
 565       */
 566      final public static function get_update_structure() {
 567          $properties = self::properties_definition();
 568          $returns = array();
 569  
 570          foreach ($properties as $property => $definition) {
 571              if (isset($returns[$property]) && substr($property, -6) === 'format') {
 572                  // We've already treated the format.
 573                  continue;
 574              }
 575  
 576              $default = null;
 577              $required = VALUE_OPTIONAL;
 578              if ($property == 'id') {
 579                  $required = VALUE_REQUIRED;
 580              }
 581  
 582              // Magically treat the contextid fields.
 583              if ($property == 'contextid') {
 584                  if (isset($properties['context'])) {
 585                      throw new coding_exception('There cannot be a context and a contextid column');
 586                  }
 587                  $returns += self::get_context_structure();
 588  
 589              } else {
 590                  $returns[$property] = new external_value($definition['type'], $definition['description'], $required, $default,
 591                      $definition['null']);
 592  
 593                  // Magically treat the format properties.
 594                  if ($formatproperty = self::get_format_field($properties, $property)) {
 595                      if (isset($returns[$formatproperty])) {
 596                          throw new coding_exception('The format for \'' . $property . '\' is already defined.');
 597                      }
 598                      $returns[$formatproperty] = self::get_format_structure($property,
 599                          $properties[$formatproperty], VALUE_OPTIONAL);
 600                  }
 601              }
 602          }
 603  
 604          return new external_single_structure($returns);
 605      }
 606  
 607  }