Merge branch 'MDL-57846-master' of git://github.com/jleyva/moodle
[moodle.git] / lib / classes / external / exporter.php
blobdcfeb4e496f91478cdd9a07492bc5a5db83a3ad4
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.
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/>.
17 /**
18 * Generic exporter to take a stdClass and prepare it for return by webservice.
20 * @package core
21 * @copyright 2015 Damyon Wiese
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 namespace core\external;
25 defined('MOODLE_INTERNAL') || die();
27 require_once($CFG->libdir . '/externallib.php');
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;
39 /**
40 * Generic exporter to take a stdClass and prepare it for return by webservice, or as the context for a template.
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.
45 * @copyright 2015 Damyon Wiese
46 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
48 abstract class exporter {
50 /** @var array $related List of related objects used to avoid DB queries. */
51 protected $related = array();
53 /** @var stdClass|array The data of this exporter. */
54 protected $data = null;
56 /**
57 * Constructor - saves the persistent object, and the related objects.
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.
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;
69 // Allow ? to mean null is allowed.
70 if (substr($classname, -1) === '?') {
71 $classname = substr($classname, 0, -1);
72 $nullallowed = true;
75 // Allow [] to mean an array of values.
76 if (substr($classname, -2) === '[]') {
77 $classname = substr($classname, 0, -2);
78 $isarray = true;
81 $missingdataerr = 'Exporter class is missing required related data: (' . get_called_class() . ') ';
83 if ($nullallowed && array_key_exists($key, $related) && $related[$key] === null) {
84 $this->related[$key] = $related[$key];
86 } else if ($isarray) {
87 if (array_key_exists($key, $related) && is_array($related[$key])) {
88 foreach ($related[$key] as $index => $value) {
89 if (!$value instanceof $classname) {
90 throw new coding_exception($missingdataerr . $key . ' => ' . $classname . '[]');
93 $this->related[$key] = $related[$key];
94 } else {
95 throw new coding_exception($missingdataerr . $key . ' => ' . $classname . '[]');
98 } else {
99 $scalartypes = ['string', 'int', 'bool', 'float'];
100 $scalarcheck = 'is_' . $classname;
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);
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).
117 * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
118 * @return stdClass
120 final public function export(renderer_base $output) {
121 $data = new stdClass();
122 $properties = self::read_properties_definition();
123 $values = (array) $this->data;
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.');
130 $values += $othervalues;
131 $record = (object) $values;
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);
148 $data->$property = $record->$property;
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);
157 $formatparams = $this->get_format_parameters($property);
158 $format = $record->$propertyformat;
160 list($text, $format) = external_format_text($data->$property, $format, $formatparams['context']->id,
161 $formatparams['component'], $formatparams['filearea'], $formatparams['itemid'], $formatparams['options']);
163 $data->$property = $text;
164 $data->$propertyformat = $format;
166 } else if ($definition['type'] === PARAM_TEXT) {
167 $formatparams = $this->get_format_parameters($property);
169 if (!empty($definition['multiple'])) {
170 foreach ($data->$property as $key => $value) {
171 $data->{$property}[$key] = external_format_string($value, $formatparams['context']->id,
172 $formatparams['striplinks'], $formatparams['options']);
174 } else {
175 $data->$property = external_format_string($data->$property, $formatparams['context']->id,
176 $formatparams['striplinks'], $formatparams['options']);
181 return $data;
185 * Get the format parameters.
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'.
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.
200 * @param string $property The property to get the parameters for.
201 * @return array
203 final protected function get_format_parameters($property) {
204 $parameters = [
205 'component' => null,
206 'filearea' => null,
207 'itemid' => null,
208 'options' => [],
209 'striplinks' => true,
212 $candidate = 'get_format_parameters_for_' . $property;
213 if (method_exists($this, $candidate)) {
214 $parameters = array_merge($parameters, $this->{$candidate}());
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.");
223 $parameters['context'] = $this->related['context'];
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.");
230 return $parameters;
234 * Get the additional values to inject while exporting.
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.
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.
245 * Note: These must be defined in {@link self::define_other_properties()}.
247 * @param renderer_base $output The renderer.
248 * @return array Keys are the property names, values are their values.
250 protected function get_other_values(renderer_base $output) {
251 return array();
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()}.
259 * @return array Keys are the property names, and value their definition.
261 final public static function read_properties_definition() {
262 $properties = static::properties_definition();
263 $customprops = static::define_other_properties();
264 foreach ($customprops as $property => $definition) {
265 // Ensures that null is set to its default.
266 if (!isset($definition['null'])) {
267 $customprops[$property]['null'] = NULL_NOT_ALLOWED;
269 if (!isset($definition['description'])) {
270 $customprops[$property]['description'] = $property;
273 $properties += $customprops;
274 return $properties;
278 * Get the properties definition of this exporter used for create, and update structures.
279 * The read structures are returned by: {@link self::read_properties_definition()}.
281 * @return array Keys are the property names, and value their definition.
283 final public static function properties_definition() {
284 $properties = static::define_properties();
285 foreach ($properties as $property => $definition) {
286 // Ensures that null is set to its default.
287 if (!isset($definition['null'])) {
288 $properties[$property]['null'] = NULL_NOT_ALLOWED;
290 if (!isset($definition['description'])) {
291 $properties[$property]['description'] = $property;
294 return $properties;
298 * Return the list of additional properties used only for display.
300 * Additional properties are only ever used for the read structure, and during
301 * export of the persistent data.
303 * The format of the array returned by this method has to match the structure
304 * defined in {@link \core\persistent::define_properties()}. The display properties
305 * can however do some more fancy things. They can define 'multiple' => true to wrap
306 * values in an external_multiple_structure automatically - or they can define the
307 * type as a nested array of more properties in order to generate a nested
308 * external_single_structure.
310 * You can specify an array of values by including a 'multiple' => true array value. This
311 * will result in a nested external_multiple_structure.
312 * E.g.
314 * 'arrayofbools' => array(
315 * 'type' => PARAM_BOOL,
316 * 'multiple' => true
317 * ),
319 * You can return a nested array in the type field, which will result in a nested external_single_structure.
320 * E.g.
321 * 'competency' => array(
322 * 'type' => competency_exporter::read_properties_definition()
323 * ),
325 * Other properties can be specifically marked as optional, in which case they do not need
326 * to be included in the export in {@link self::get_other_values()}. This is useful when exporting
327 * a substructure which cannot be set as null due to webservices protocol constraints.
328 * E.g.
329 * 'competency' => array(
330 * 'type' => competency_exporter::read_properties_definition(),
331 * 'optional' => true
332 * ),
334 * @return array
336 protected static function define_other_properties() {
337 return array();
341 * Return the list of properties.
343 * The format of the array returned by this method has to match the structure
344 * defined in {@link \core\persistent::define_properties()}. Howewer you can
345 * add a new attribute "description" to describe the parameter for documenting the API.
347 * Note that the type PARAM_TEXT should ONLY be used for strings which need to
348 * go through filters (multilang, etc...) and do not have a FORMAT_* associated
349 * to them. Typically strings passed through to format_string().
351 * Other filtered strings which use a FORMAT_* constant (hear used with format_text)
352 * must be defined as PARAM_RAW.
354 * @return array
356 protected static function define_properties() {
357 return array();
361 * Returns a list of objects that are related to this persistent.
363 * Only objects listed here can be cached in this object.
365 * The class name can be suffixed:
366 * - with [] to indicate an array of values.
367 * - with ? to indicate that 'null' is allowed.
369 * @return array of 'propertyname' => array('type' => classname, 'required' => true)
371 protected static function define_related() {
372 return array();
376 * Get the context structure.
378 * @return external_single_structure
380 final protected static function get_context_structure() {
381 return array(
382 'contextid' => new external_value(PARAM_INT, 'The context id', VALUE_OPTIONAL),
383 'contextlevel' => new external_value(PARAM_ALPHA, 'The context level', VALUE_OPTIONAL),
384 'instanceid' => new external_value(PARAM_INT, 'The Instance id', VALUE_OPTIONAL),
389 * Get the format field name.
391 * @param array $definitions List of properties definitions.
392 * @param string $property The name of the property that may have a format field.
393 * @return bool|string False, or the name of the format property.
395 final protected static function get_format_field($definitions, $property) {
396 $formatproperty = $property . 'format';
397 if ($definitions[$property]['type'] == PARAM_RAW && isset($definitions[$formatproperty])
398 && $definitions[$formatproperty]['type'] == PARAM_INT) {
399 return $formatproperty;
401 return false;
405 * Get the format structure.
407 * @param string $property The name of the property on which the format applies.
408 * @param array $definition The definition of the format property.
409 * @param int $required Constant VALUE_*.
410 * @return external_format_value
412 final protected static function get_format_structure($property, $definition, $required = VALUE_REQUIRED) {
413 if (array_key_exists('default', $definition)) {
414 $required = VALUE_DEFAULT;
416 return new external_format_value($property, $required);
420 * Returns the create structure.
422 * @return external_single_structure
424 final public static function get_create_structure() {
425 $properties = self::properties_definition();
426 $returns = array();
428 foreach ($properties as $property => $definition) {
429 if ($property == 'id') {
430 // The can not be set on create.
431 continue;
433 } else if (isset($returns[$property]) && substr($property, -6) === 'format') {
434 // We've already treated the format.
435 continue;
438 $required = VALUE_REQUIRED;
439 $default = null;
441 // We cannot use isset here because we want to detect nulls.
442 if (array_key_exists('default', $definition)) {
443 $required = VALUE_DEFAULT;
444 $default = $definition['default'];
447 // Magically treat the contextid fields.
448 if ($property == 'contextid') {
449 if (isset($properties['context'])) {
450 throw new coding_exception('There cannot be a context and a contextid column');
452 $returns += self::get_context_structure();
454 } else {
455 $returns[$property] = new external_value($definition['type'], $definition['description'], $required, $default,
456 $definition['null']);
458 // Magically treat the format properties.
459 if ($formatproperty = self::get_format_field($properties, $property)) {
460 if (isset($returns[$formatproperty])) {
461 throw new coding_exception('The format for \'' . $property . '\' is already defined.');
463 $returns[$formatproperty] = self::get_format_structure($property,
464 $properties[$formatproperty], VALUE_REQUIRED);
469 return new external_single_structure($returns);
473 * Returns the read structure.
475 * @return external_single_structure
477 final public static function get_read_structure() {
478 $properties = self::read_properties_definition();
480 return self::get_read_structure_from_properties($properties);
484 * Returns the read structure from a set of properties (recursive).
486 * @param array $properties The properties.
487 * @param int $required Whether is required.
488 * @param mixed $default The default value.
489 * @return external_single_structure
491 final protected static function get_read_structure_from_properties($properties, $required = VALUE_REQUIRED, $default = null) {
492 $returns = array();
493 foreach ($properties as $property => $definition) {
494 if (isset($returns[$property]) && substr($property, -6) === 'format') {
495 // We've already treated the format.
496 continue;
498 $thisvalue = null;
500 $type = $definition['type'];
501 $proprequired = VALUE_REQUIRED;
502 $propdefault = null;
503 if (array_key_exists('default', $definition)) {
504 $propdefault = $definition['default'];
506 if (array_key_exists('optional', $definition)) {
507 // Mark as optional. Note that this should only apply to "reading" "other" properties.
508 $proprequired = VALUE_OPTIONAL;
511 if (is_array($type)) {
512 // This is a nested array of more properties.
513 $thisvalue = self::get_read_structure_from_properties($type, $proprequired, $propdefault);
514 } else {
515 if ($definition['type'] == PARAM_TEXT) {
516 // PARAM_TEXT always becomes PARAM_RAW because filters may be applied.
517 $type = PARAM_RAW;
519 $thisvalue = new external_value($type, $definition['description'], $proprequired, $propdefault, $definition['null']);
521 if (!empty($definition['multiple'])) {
522 $returns[$property] = new external_multiple_structure($thisvalue, $definition['description'], $proprequired,
523 $propdefault);
524 } else {
525 $returns[$property] = $thisvalue;
527 // Magically treat the format properties (not possible for arrays).
528 if ($formatproperty = self::get_format_field($properties, $property)) {
529 if (isset($returns[$formatproperty])) {
530 throw new coding_exception('The format for \'' . $property . '\' is already defined.');
532 $returns[$formatproperty] = self::get_format_structure($property, $properties[$formatproperty]);
537 return new external_single_structure($returns, '', $required, $default);
541 * Returns the update structure.
543 * This structure can never be included at the top level for an external function signature
544 * because it contains optional parameters.
546 * @return external_single_structure
548 final public static function get_update_structure() {
549 $properties = self::properties_definition();
550 $returns = array();
552 foreach ($properties as $property => $definition) {
553 if (isset($returns[$property]) && substr($property, -6) === 'format') {
554 // We've already treated the format.
555 continue;
558 $default = null;
559 $required = VALUE_OPTIONAL;
560 if ($property == 'id') {
561 $required = VALUE_REQUIRED;
564 // Magically treat the contextid fields.
565 if ($property == 'contextid') {
566 if (isset($properties['context'])) {
567 throw new coding_exception('There cannot be a context and a contextid column');
569 $returns += self::get_context_structure();
571 } else {
572 $returns[$property] = new external_value($definition['type'], $definition['description'], $required, $default,
573 $definition['null']);
575 // Magically treat the format properties.
576 if ($formatproperty = self::get_format_field($properties, $property)) {
577 if (isset($returns[$formatproperty])) {
578 throw new coding_exception('The format for \'' . $property . '\' is already defined.');
580 $returns[$formatproperty] = self::get_format_structure($property,
581 $properties[$formatproperty], VALUE_OPTIONAL);
586 return new external_single_structure($returns);