Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 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 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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body