Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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