f874ae551eb80918bb78bc4fefb18bb4481fa320
[moodle.git] / lib / classes / external / exporter.php
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/>.
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();
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.
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 {
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.
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;
69             // Allow ? to mean null is allowed.
70             if (substr($classname, -1) === '?') {
71                 $classname = substr($classname, 0, -1);
72                 $nullallowed = true;
73             }
75             // Allow [] to mean an array of values.
76             if (substr($classname, -2) === '[]') {
77                 $classname = substr($classname, 0, -2);
78                 $isarray = true;
79             }
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 . '[]');
91                         }
92                     }
93                     $this->related[$key] = $related[$key];
94                 } else {
95                     throw new coding_exception($missingdataerr . $key . ' => ' . $classname . '[]');
96                 }
98             } else {
99                 if (array_key_exists($key, $related) && $related[$key] instanceof $classname) {
100                     $this->related[$key] = $related[$key];
101                 } else {
102                     throw new coding_exception($missingdataerr . $key . ' => ' . $classname);
103                 }
104             }
105         }
106     }
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 external_format_text (or external_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;
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;
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             }
144             $data->$property = $record->$property;
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                 }
153                 $formatparams = $this->get_format_parameters($property);
154                 $format = $record->$propertyformat;
156                 list($text, $format) = external_format_text($data->$property, $format, $formatparams['context']->id,
157                     $formatparams['component'], $formatparams['filearea'], $formatparams['itemid'], $formatparams['options']);
159                 $data->$property = $text;
160                 $data->$propertyformat = $format;
162             } else if ($definition['type'] === PARAM_TEXT) {
163                 $formatparams = $this->get_format_parameters($property);
165                 if (!empty($definition['multiple'])) {
166                     foreach ($data->$property as $key => $value) {
167                         $data->{$property}[$key] = external_format_string($value, $formatparams['context']->id,
168                             $formatparams['striplinks'], $formatparams['options']);
169                     }
170                 } else {
171                     $data->$property = external_format_string($data->$property, $formatparams['context']->id,
172                             $formatparams['striplinks'], $formatparams['options']);
173                 }
174             }
175         }
177         return $data;
178     }
180     /**
181      * Get the format parameters.
182      *
183      * This method returns the parameters to use with the functions external_format_text(), and
184      * external_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 external_format_text(). Defaults to null.
191      * - filearea: The filearea to use with external_format_text(). Defaults to null.
192      * - itemid: The itemid to use with external_format_text(). Defaults to null.
193      * - options: An array of options accepted by external_format_text() or external_format_string(). Defaults to [].
194      * - striplinks: Whether to strip the links with external_format_string(). Defaults to true.
195      *
196      * @param string $property The property to get the parameters for.
197      * @return array
198      */
199     final protected function get_format_parameters($property) {
200         $parameters = [
201             'component' => null,
202             'filearea' => null,
203             'itemid' => null,
204             'options' => [],
205             'striplinks' => true,
206         ];
208         $candidate = 'get_format_parameters_for_' . $property;
209         if (method_exists($this, $candidate)) {
210             $parameters = array_merge($parameters, $this->{$candidate}());
211         }
213         if (!isset($parameters['context'])) {
214             if (!isset($this->related['context']) || !($this->related['context'] instanceof context)) {
215                 throw new coding_exception("Unknown context to use for formatting the property '$property' in the " .
216                     "exporter '" . get_class($this) . "'. You either need to add 'context' to your related objects, " .
217                     "or create the method '$candidate' and return the context from there.");
218             }
219             $parameters['context'] = $this->related['context'];
221         } else if (!($parameters['context'] instanceof context)) {
222             throw new coding_exception("The context given to format the property '$property' in the exporter '" .
223                 get_class($this) . "' is invalid.");
224         }
226         return $parameters;
227     }
229     /**
230      * Get the additional values to inject while exporting.
231      *
232      * These are additional generated values that are not passed in through $data
233      * to the exporter. For a persistent exporter - these are generated values that
234      * do not exist in the persistent class. For your convenience the format_text or
235      * format_string functions do not need to be applied to PARAM_TEXT fields,
236      * it will be done automatically during export.
237      *
238      * These values are only used when returning data via {@link self::export()},
239      * they are not used when generating any of the different external structures.
240      *
241      * Note: These must be defined in {@link self::define_other_properties()}.
242      *
243      * @param renderer_base $output The renderer.
244      * @return array Keys are the property names, values are their values.
245      */
246     protected function get_other_values(renderer_base $output) {
247         return array();
248     }
250     /**
251      * Get the read properties definition of this exporter. Read properties combines the
252      * default properties from the model (persistent or stdClass) with the properties defined
253      * by {@link self::define_other_properties()}.
254      *
255      * @return array Keys are the property names, and value their definition.
256      */
257     final public static function read_properties_definition() {
258         $properties = static::properties_definition();
259         $customprops = static::define_other_properties();
260         foreach ($customprops as $property => $definition) {
261             // Ensures that null is set to its default.
262             if (!isset($definition['null'])) {
263                 $customprops[$property]['null'] = NULL_NOT_ALLOWED;
264             }
265             if (!isset($definition['description'])) {
266                 $customprops[$property]['description'] = $property;
267             }
268         }
269         $properties += $customprops;
270         return $properties;
271     }
273     /**
274      * Get the properties definition of this exporter used for create, and update structures.
275      * The read structures are returned by: {@link self::read_properties_definition()}.
276      *
277      * @return array Keys are the property names, and value their definition.
278      */
279     final public static function properties_definition() {
280         $properties = static::define_properties();
281         foreach ($properties as $property => $definition) {
282             // Ensures that null is set to its default.
283             if (!isset($definition['null'])) {
284                 $properties[$property]['null'] = NULL_NOT_ALLOWED;
285             }
286             if (!isset($definition['description'])) {
287                 $properties[$property]['description'] = $property;
288             }
289         }
290         return $properties;
291     }
293     /**
294      * Return the list of additional properties used only for display.
295      *
296      * Additional properties are only ever used for the read structure, and during
297      * export of the persistent data.
298      *
299      * The format of the array returned by this method has to match the structure
300      * defined in {@link \core\persistent::define_properties()}. The display properties
301      * can however do some more fancy things. They can define 'multiple' => true to wrap
302      * values in an external_multiple_structure automatically - or they can define the
303      * type as a nested array of more properties in order to generate a nested
304      * external_single_structure.
305      *
306      * You can specify an array of values by including a 'multiple' => true array value. This
307      * will result in a nested external_multiple_structure.
308      * E.g.
309      *
310      *       'arrayofbools' => array(
311      *           'type' => PARAM_BOOL,
312      *           'multiple' => true
313      *       ),
314      *
315      * You can return a nested array in the type field, which will result in a nested external_single_structure.
316      * E.g.
317      *      'competency' => array(
318      *          'type' => competency_exporter::read_properties_definition()
319      *       ),
320      *
321      * Other properties can be specifically marked as optional, in which case they do not need
322      * to be included in the export in {@link self::get_other_values()}. This is useful when exporting
323      * a substructure which cannot be set as null due to webservices protocol constraints.
324      * E.g.
325      *      'competency' => array(
326      *          'type' => competency_exporter::read_properties_definition(),
327      *          'optional' => true
328      *       ),
329      *
330      * @return array
331      */
332     protected static function define_other_properties() {
333         return array();
334     }
336     /**
337      * Return the list of properties.
338      *
339      * The format of the array returned by this method has to match the structure
340      * defined in {@link \core\persistent::define_properties()}. Howewer you can
341      * add a new attribute "description" to describe the parameter for documenting the API.
342      *
343      * Note that the type PARAM_TEXT should ONLY be used for strings which need to
344      * go through filters (multilang, etc...) and do not have a FORMAT_* associated
345      * to them. Typically strings passed through to format_string().
346      *
347      * Other filtered strings which use a FORMAT_* constant (hear used with format_text)
348      * must be defined as PARAM_RAW.
349      *
350      * @return array
351      */
352     protected static function define_properties() {
353         return array();
354     }
356     /**
357      * Returns a list of objects that are related to this persistent.
358      *
359      * Only objects listed here can be cached in this object.
360      *
361      * The class name can be suffixed:
362      * - with [] to indicate an array of values.
363      * - with ? to indicate that 'null' is allowed.
364      *
365      * @return array of 'propertyname' => array('type' => classname, 'required' => true)
366      */
367     protected static function define_related() {
368         return array();
369     }
371     /**
372      * Get the context structure.
373      *
374      * @return external_single_structure
375      */
376     final protected static function get_context_structure() {
377         return array(
378             'contextid' => new external_value(PARAM_INT, 'The context id', VALUE_OPTIONAL),
379             'contextlevel' => new external_value(PARAM_ALPHA, 'The context level', VALUE_OPTIONAL),
380             'instanceid' => new external_value(PARAM_INT, 'The Instance id', VALUE_OPTIONAL),
381         );
382     }
384     /**
385      * Get the format field name.
386      *
387      * @param  array $definitions List of properties definitions.
388      * @param  string $property The name of the property that may have a format field.
389      * @return bool|string False, or the name of the format property.
390      */
391     final protected static function get_format_field($definitions, $property) {
392         $formatproperty = $property . 'format';
393         if ($definitions[$property]['type'] == PARAM_RAW && isset($definitions[$formatproperty])
394                 && $definitions[$formatproperty]['type'] == PARAM_INT) {
395             return $formatproperty;
396         }
397         return false;
398     }
400     /**
401      * Get the format structure.
402      *
403      * @param  string $property   The name of the property on which the format applies.
404      * @param  array  $definition The definition of the format property.
405      * @param  int    $required   Constant VALUE_*.
406      * @return external_format_value
407      */
408     final protected static function get_format_structure($property, $definition, $required = VALUE_REQUIRED) {
409         if (array_key_exists('default', $definition)) {
410             $required = VALUE_DEFAULT;
411         }
412         return new external_format_value($property, $required);
413     }
415     /**
416      * Returns the create structure.
417      *
418      * @return external_single_structure
419      */
420     final public static function get_create_structure() {
421         $properties = self::properties_definition();
422         $returns = array();
424         foreach ($properties as $property => $definition) {
425             if ($property == 'id') {
426                 // The can not be set on create.
427                 continue;
429             } else if (isset($returns[$property]) && substr($property, -6) === 'format') {
430                 // We've already treated the format.
431                 continue;
432             }
434             $required = VALUE_REQUIRED;
435             $default = null;
437             // We cannot use isset here because we want to detect nulls.
438             if (array_key_exists('default', $definition)) {
439                 $required = VALUE_DEFAULT;
440                 $default = $definition['default'];
441             }
443             // Magically treat the contextid fields.
444             if ($property == 'contextid') {
445                 if (isset($properties['context'])) {
446                     throw new coding_exception('There cannot be a context and a contextid column');
447                 }
448                 $returns += self::get_context_structure();
450             } else {
451                 $returns[$property] = new external_value($definition['type'], $definition['description'], $required, $default,
452                     $definition['null']);
454                 // Magically treat the format properties.
455                 if ($formatproperty = self::get_format_field($properties, $property)) {
456                     if (isset($returns[$formatproperty])) {
457                         throw new coding_exception('The format for \'' . $property . '\' is already defined.');
458                     }
459                     $returns[$formatproperty] = self::get_format_structure($property,
460                         $properties[$formatproperty], VALUE_REQUIRED);
461                 }
462             }
463         }
465         return new external_single_structure($returns);
466     }
468     /**
469      * Returns the read structure.
470      *
471      * @return external_single_structure
472      */
473     final public static function get_read_structure() {
474         $properties = self::read_properties_definition();
476         return self::get_read_structure_from_properties($properties);
477     }
479     /**
480      * Returns the read structure from a set of properties (recursive).
481      *
482      * @param array $properties The properties.
483      * @param int $required Whether is required.
484      * @param mixed $default The default value.
485      * @return external_single_structure
486      */
487     final protected static function get_read_structure_from_properties($properties, $required = VALUE_REQUIRED, $default = null) {
488         $returns = array();
489         foreach ($properties as $property => $definition) {
490             if (isset($returns[$property]) && substr($property, -6) === 'format') {
491                 // We've already treated the format.
492                 continue;
493             }
494             $thisvalue = null;
496             $type = $definition['type'];
497             $proprequired = VALUE_REQUIRED;
498             $propdefault = null;
499             if (array_key_exists('default', $definition)) {
500                 $propdefault = $definition['default'];
501             }
502             if (array_key_exists('optional', $definition)) {
503                 // Mark as optional. Note that this should only apply to "reading" "other" properties.
504                 $proprequired = VALUE_OPTIONAL;
505             }
507             if (is_array($type)) {
508                 // This is a nested array of more properties.
509                 $thisvalue = self::get_read_structure_from_properties($type, $proprequired, $propdefault);
510             } else {
511                 if ($definition['type'] == PARAM_TEXT) {
512                     // PARAM_TEXT always becomes PARAM_RAW because filters may be applied.
513                     $type = PARAM_RAW;
514                 }
515                 $thisvalue = new external_value($type, $definition['description'], $proprequired, $propdefault, $definition['null']);
516             }
517             if (!empty($definition['multiple'])) {
518                 $returns[$property] = new external_multiple_structure($thisvalue, $definition['description'], $proprequired,
519                     $propdefault);
520             } else {
521                 $returns[$property] = $thisvalue;
523                 // Magically treat the format properties (not possible for arrays).
524                 if ($formatproperty = self::get_format_field($properties, $property)) {
525                     if (isset($returns[$formatproperty])) {
526                         throw new coding_exception('The format for \'' . $property . '\' is already defined.');
527                     }
528                     $returns[$formatproperty] = self::get_format_structure($property, $properties[$formatproperty]);
529                 }
530             }
531         }
533         return new external_single_structure($returns, '', $required, $default);
534     }
536     /**
537      * Returns the update structure.
538      *
539      * This structure can never be included at the top level for an external function signature
540      * because it contains optional parameters.
541      *
542      * @return external_single_structure
543      */
544     final public static function get_update_structure() {
545         $properties = self::properties_definition();
546         $returns = array();
548         foreach ($properties as $property => $definition) {
549             if (isset($returns[$property]) && substr($property, -6) === 'format') {
550                 // We've already treated the format.
551                 continue;
552             }
554             $default = null;
555             $required = VALUE_OPTIONAL;
556             if ($property == 'id') {
557                 $required = VALUE_REQUIRED;
558             }
560             // Magically treat the contextid fields.
561             if ($property == 'contextid') {
562                 if (isset($properties['context'])) {
563                     throw new coding_exception('There cannot be a context and a contextid column');
564                 }
565                 $returns += self::get_context_structure();
567             } else {
568                 $returns[$property] = new external_value($definition['type'], $definition['description'], $required, $default,
569                     $definition['null']);
571                 // Magically treat the format properties.
572                 if ($formatproperty = self::get_format_field($properties, $property)) {
573                     if (isset($returns[$formatproperty])) {
574                         throw new coding_exception('The format for \'' . $property . '\' is already defined.');
575                     }
576                     $returns[$formatproperty] = self::get_format_structure($property,
577                         $properties[$formatproperty], VALUE_OPTIONAL);
578                 }
579             }
580         }
582         return new external_single_structure($returns);
583     }