Differences Between: [Versions 310 and 311] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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 * @param int $required Whether is required. 493 * @param mixed $default The default value. 494 * 495 * @return external_single_structure 496 */ 497 final public static function get_read_structure($required = VALUE_REQUIRED, $default = null) { 498 $properties = self::read_properties_definition(); 499 500 return self::get_read_structure_from_properties($properties, $required, $default); 501 } 502 503 /** 504 * Returns the read structure from a set of properties (recursive). 505 * 506 * @param array $properties The properties. 507 * @param int $required Whether is required. 508 * @param mixed $default The default value. 509 * @return external_single_structure 510 */ 511 final protected static function get_read_structure_from_properties($properties, $required = VALUE_REQUIRED, $default = null) { 512 $returns = array(); 513 foreach ($properties as $property => $definition) { 514 if (isset($returns[$property]) && substr($property, -6) === 'format') { 515 // We've already treated the format. 516 continue; 517 } 518 $thisvalue = null; 519 520 $type = $definition['type']; 521 $proprequired = VALUE_REQUIRED; 522 $propdefault = null; 523 if (array_key_exists('default', $definition)) { 524 $propdefault = $definition['default']; 525 } 526 if (array_key_exists('optional', $definition)) { 527 // Mark as optional. Note that this should only apply to "reading" "other" properties. 528 $proprequired = VALUE_OPTIONAL; 529 } 530 531 if (is_array($type)) { 532 // This is a nested array of more properties. 533 $thisvalue = self::get_read_structure_from_properties($type, $proprequired, $propdefault); 534 } else { 535 if ($definition['type'] == PARAM_TEXT || $definition['type'] == PARAM_CLEANHTML) { 536 // PARAM_TEXT always becomes PARAM_RAW because filters may be applied. 537 $type = PARAM_RAW; 538 } 539 $thisvalue = new external_value($type, $definition['description'], $proprequired, $propdefault, $definition['null']); 540 } 541 if (!empty($definition['multiple'])) { 542 $returns[$property] = new external_multiple_structure($thisvalue, $definition['description'], $proprequired, 543 $propdefault); 544 } else { 545 $returns[$property] = $thisvalue; 546 547 // Magically treat the format properties (not possible for arrays). 548 if ($formatproperty = self::get_format_field($properties, $property)) { 549 if (isset($returns[$formatproperty])) { 550 throw new coding_exception('The format for \'' . $property . '\' is already defined.'); 551 } 552 $returns[$formatproperty] = self::get_format_structure($property, $properties[$formatproperty]); 553 } 554 } 555 } 556 557 return new external_single_structure($returns, '', $required, $default); 558 } 559 560 /** 561 * Returns the update structure. 562 * 563 * This structure can never be included at the top level for an external function signature 564 * because it contains optional parameters. 565 * 566 * @return external_single_structure 567 */ 568 final public static function get_update_structure() { 569 $properties = self::properties_definition(); 570 $returns = array(); 571 572 foreach ($properties as $property => $definition) { 573 if (isset($returns[$property]) && substr($property, -6) === 'format') { 574 // We've already treated the format. 575 continue; 576 } 577 578 $default = null; 579 $required = VALUE_OPTIONAL; 580 if ($property == 'id') { 581 $required = VALUE_REQUIRED; 582 } 583 584 // Magically treat the contextid fields. 585 if ($property == 'contextid') { 586 if (isset($properties['context'])) { 587 throw new coding_exception('There cannot be a context and a contextid column'); 588 } 589 $returns += self::get_context_structure(); 590 591 } else { 592 $returns[$property] = new external_value($definition['type'], $definition['description'], $required, $default, 593 $definition['null']); 594 595 // Magically treat the format properties. 596 if ($formatproperty = self::get_format_field($properties, $property)) { 597 if (isset($returns[$formatproperty])) { 598 throw new coding_exception('The format for \'' . $property . '\' is already defined.'); 599 } 600 $returns[$formatproperty] = self::get_format_structure($property, 601 $properties[$formatproperty], VALUE_OPTIONAL); 602 } 603 } 604 } 605 606 return new external_single_structure($returns); 607 } 608 609 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body