MDL-56459 competency: Invalid use of indirect property key
[moodle.git] / competency / 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_competency
21  * @copyright  2015 Damyon Wiese
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 namespace core_competency\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         $context = $this->get_context();
120         $values = (array) $this->data;
122         $othervalues = $this->get_other_values($output);
123         if (array_intersect_key($values, $othervalues)) {
124             // Attempt to replace a standard property.
125             throw new coding_exception('Cannot override a standard property value.');
126         }
127         $values += $othervalues;
128         $record = (object) $values;
130         foreach ($properties as $property => $definition) {
131             if (isset($data->$property)) {
132                 // This happens when we have already defined the format properties.
133                 continue;
134             } else if (!property_exists($record, $property) && array_key_exists('default', $definition)) {
135                 // We have a default value for this property.
136                 $record->$property = $definition['default'];
137             } else if (!property_exists($record, $property) && !empty($definition['optional'])) {
138                 // Fine, this property can be omitted.
139                 continue;
140             } else if (!property_exists($record, $property)) {
141                 // Whoops, we got something that wasn't defined.
142                 throw new coding_exception('Unexpected property ' . $property);
143             }
145             $data->$property = $record->$property;
147             // If the field is PARAM_RAW and has a format field.
148             if ($propertyformat = self::get_format_field($properties, $property)) {
149                 if (!property_exists($record, $propertyformat)) {
150                     // Whoops, we got something that wasn't defined.
151                     throw new coding_exception('Unexpected property ' . $propertyformat);
152                 }
153                 $format = $record->$propertyformat;
154                 list($text, $format) = external_format_text($data->$property, $format, $context->id, 'core_competency', '', 0);
155                 $data->$property = $text;
156                 $data->$propertyformat = $format;
158             } else if ($definition['type'] === PARAM_TEXT) {
159                 if (!empty($definition['multiple'])) {
160                     foreach ($data->$property as $key => $value) {
161                         $data->{$property}[$key] = external_format_string($value, $context->id);
162                     }
163                 } else {
164                     $data->$property = external_format_string($data->$property, $context->id);
165                 }
166             }
167         }
169         return $data;
170     }
172     /**
173      * Function to guess the correct context, falling back to system context.
174      *
175      * @return context
176      */
177     protected function get_context() {
178         $context = null;
179         if (isset($this->related['context']) && $this->related['context'] instanceof context) {
180             $context = $this->related['context'];
181         } else {
182             $context = context_system::instance();
183         }
184         return $context;
185     }
187     /**
188      * Get the additional values to inject while exporting.
189      *
190      * These are additional generated values that are not passed in through $data
191      * to the exporter. For a persistent exporter - these are generated values that
192      * do not exist in the persistent class. For your convenience the format_text or
193      * format_string functions do not need to be applied to PARAM_TEXT fields,
194      * it will be done automatically during export.
195      *
196      * These values are only used when returning data via {@link self::export()},
197      * they are not used when generating any of the different external structures.
198      *
199      * Note: These must be defined in {@link self::define_other_properties()}.
200      *
201      * @param renderer_base $output The renderer.
202      * @return array Keys are the property names, values are their values.
203      */
204     protected function get_other_values(renderer_base $output) {
205         return array();
206     }
208     /**
209      * Get the read properties definition of this exporter. Read properties combines the
210      * default properties from the model (persistent or stdClass) with the properties defined
211      * by {@link self::define_other_properties()}.
212      *
213      * @return array Keys are the property names, and value their definition.
214      */
215     final public static function read_properties_definition() {
216         $properties = static::properties_definition();
217         $customprops = static::define_other_properties();
218         foreach ($customprops as $property => $definition) {
219             // Ensures that null is set to its default.
220             if (!isset($definition['null'])) {
221                 $customprops[$property]['null'] = NULL_NOT_ALLOWED;
222             }
223         }
224         $properties += $customprops;
225         return $properties;
226     }
228     /**
229      * Get the properties definition of this exporter used for create, and update structures.
230      * The read structures are returned by: {@link self::read_properties_definition()}.
231      *
232      * @return array Keys are the property names, and value their definition.
233      */
234     final public static function properties_definition() {
235         $properties = static::define_properties();
236         foreach ($properties as $property => $definition) {
237             // Ensures that null is set to its default.
238             if (!isset($definition['null'])) {
239                 $properties[$property]['null'] = NULL_NOT_ALLOWED;
240             }
241         }
242         return $properties;
243     }
245     /**
246      * Return the list of additional properties used only for display.
247      *
248      * Additional properties are only ever used for the read structure, and during
249      * export of the persistent data.
250      *
251      * The format of the array returned by this method has to match the structure
252      * defined in {@link \core_competency\persistent::define_properties()}. The display properties
253      * can however do some more fancy things. They can define 'multiple' => true to wrap
254      * values in an external_multiple_structure automatically - or they can define the
255      * type as a nested array of more properties in order to generate a nested
256      * external_single_structure.
257      *
258      * You can specify an array of values by including a 'multiple' => true array value. This
259      * will result in a nested external_multiple_structure.
260      * E.g.
261      *
262      *       'arrayofbools' => array(
263      *           'type' => PARAM_BOOL,
264      *           'multiple' => true
265      *       ),
266      *
267      * You can return a nested array in the type field, which will result in a nested external_single_structure.
268      * E.g.
269      *      'competency' => array(
270      *          'type' => competency_exporter::read_properties_definition()
271      *       ),
272      *
273      * Other properties can be specifically marked as optional, in which case they do not need
274      * to be included in the export in {@link self::get_other_values()}. This is useful when exporting
275      * a substructure which cannot be set as null due to webservices protocol constraints.
276      * E.g.
277      *      'competency' => array(
278      *          'type' => competency_exporter::read_properties_definition(),
279      *          'optional' => true
280      *       ),
281      *
282      * @return array
283      */
284     protected static function define_other_properties() {
285         return array();
286     }
288     /**
289      * Return the list of properties.
290      *
291      * The format of the array returned by this method has to match the structure
292      * defined in {@link \core_competency\persistent::define_properties()}.
293      *
294      * @return array
295      */
296     protected static function define_properties() {
297         return array();
298     }
300     /**
301      * Returns a list of objects that are related to this persistent.
302      *
303      * Only objects listed here can be cached in this object.
304      *
305      * The class name can be suffixed:
306      * - with [] to indicate an array of values.
307      * - with ? to indicate that 'null' is allowed.
308      *
309      * @return array of 'propertyname' => array('type' => classname, 'required' => true)
310      */
311     protected static function define_related() {
312         return array();
313     }
315     /**
316      * Get the context structure.
317      *
318      * @return external_single_structure
319      */
320     final protected static function get_context_structure() {
321         return array(
322             'contextid' => new external_value(PARAM_INT, 'The context id', VALUE_OPTIONAL),
323             'contextlevel' => new external_value(PARAM_ALPHA, 'The context level', VALUE_OPTIONAL),
324             'instanceid' => new external_value(PARAM_INT, 'The Instance id', VALUE_OPTIONAL),
325         );
326     }
328     /**
329      * Get the format field name.
330      *
331      * @param  array $definitions List of properties definitions.
332      * @param  string $property The name of the property that may have a format field.
333      * @return bool|string False, or the name of the format property.
334      */
335     final protected static function get_format_field($definitions, $property) {
336         $formatproperty = $property . 'format';
337         if ($definitions[$property]['type'] == PARAM_RAW && isset($definitions[$formatproperty])
338                 && $definitions[$formatproperty]['type'] == PARAM_INT) {
339             return $formatproperty;
340         }
341         return false;
342     }
344     /**
345      * Get the format structure.
346      *
347      * @param  string $property   The name of the property on which the format applies.
348      * @param  array  $definition The definition of the format property.
349      * @param  int    $required   Constant VALUE_*.
350      * @return external_format_value
351      */
352     final protected static function get_format_structure($property, $definition, $required = VALUE_REQUIRED) {
353         if (array_key_exists('default', $definition)) {
354             $required = VALUE_DEFAULT;
355         }
356         return new external_format_value($property, $required);
357     }
359     /**
360      * Returns the create structure.
361      *
362      * @return external_single_structure
363      */
364     final public static function get_create_structure() {
365         $properties = self::properties_definition();
366         $returns = array();
368         foreach ($properties as $property => $definition) {
369             if ($property == 'id') {
370                 // The can not be set on create.
371                 continue;
373             } else if (isset($returns[$property]) && substr($property, -6) === 'format') {
374                 // We've already treated the format.
375                 continue;
376             }
378             $required = VALUE_REQUIRED;
379             $default = null;
381             // We cannot use isset here because we want to detect nulls.
382             if (array_key_exists('default', $definition)) {
383                 $required = VALUE_DEFAULT;
384                 $default = $definition['default'];
385             }
387             // Magically treat the contextid fields.
388             if ($property == 'contextid') {
389                 if (isset($properties['context'])) {
390                     throw new coding_exception('There cannot be a context and a contextid column');
391                 }
392                 $returns += self::get_context_structure();
394             } else {
395                 $returns[$property] = new external_value($definition['type'], $property, $required, $default, $definition['null']);
397                 // Magically treat the format properties.
398                 if ($formatproperty = self::get_format_field($properties, $property)) {
399                     if (isset($returns[$formatproperty])) {
400                         throw new coding_exception('The format for \'' . $property . '\' is already defined.');
401                     }
402                     $returns[$formatproperty] = self::get_format_structure($property,
403                         $properties[$formatproperty], VALUE_REQUIRED);
404                 }
405             }
406         }
408         return new external_single_structure($returns);
409     }
411     /**
412      * Returns the read structure.
413      *
414      * @return external_single_structure
415      */
416     final public static function get_read_structure() {
417         $properties = self::read_properties_definition();
419         return self::get_read_structure_from_properties($properties);
420     }
422     /**
423      * Returns the read structure from a set of properties (recursive).
424      *
425      * @param array $properties The properties.
426      * @param int $required Whether is required.
427      * @param mixed $default The default value.
428      * @return external_single_structure
429      */
430     final protected static function get_read_structure_from_properties($properties, $required = VALUE_REQUIRED, $default = null) {
431         $returns = array();
432         foreach ($properties as $property => $definition) {
433             if (isset($returns[$property]) && substr($property, -6) === 'format') {
434                 // We've already treated the format.
435                 continue;
436             }
437             $thisvalue = null;
439             $type = $definition['type'];
440             $proprequired = VALUE_REQUIRED;
441             $propdefault = null;
442             if (array_key_exists('default', $definition)) {
443                 $propdefault = $definition['default'];
444             }
445             if (array_key_exists('optional', $definition)) {
446                 // Mark as optional. Note that this should only apply to "reading" "other" properties.
447                 $proprequired = VALUE_OPTIONAL;
448             }
450             if (is_array($type)) {
451                 // This is a nested array of more properties.
452                 $thisvalue = self::get_read_structure_from_properties($type, $proprequired, $propdefault);
453             } else {
454                 if ($definition['type'] == PARAM_TEXT) {
455                     // PARAM_TEXT always becomes PARAM_RAW because filters may be applied.
456                     $type = PARAM_RAW;
457                 }
458                 $thisvalue = new external_value($type, $property, $proprequired, $propdefault, $definition['null']);
459             }
460             if (!empty($definition['multiple'])) {
461                 $returns[$property] = new external_multiple_structure($thisvalue, '', $proprequired, $propdefault);
462             } else {
463                 $returns[$property] = $thisvalue;
465                 // Magically treat the format properties (not possible for arrays).
466                 if ($formatproperty = self::get_format_field($properties, $property)) {
467                     if (isset($returns[$formatproperty])) {
468                         throw new coding_exception('The format for \'' . $property . '\' is already defined.');
469                     }
470                     $returns[$formatproperty] = self::get_format_structure($property, $properties[$formatproperty]);
471                 }
472             }
473         }
475         return new external_single_structure($returns, '', $required, $default);
476     }
478     /**
479      * Returns the update structure.
480      *
481      * This structure can never be included at the top level for an external function signature
482      * because it contains optional parameters.
483      *
484      * @return external_single_structure
485      */
486     final public static function get_update_structure() {
487         $properties = self::properties_definition();
488         $returns = array();
490         foreach ($properties as $property => $definition) {
491             if (isset($returns[$property]) && substr($property, -6) === 'format') {
492                 // We've already treated the format.
493                 continue;
494             }
496             $default = null;
497             $required = VALUE_OPTIONAL;
498             if ($property == 'id') {
499                 $required = VALUE_REQUIRED;
500             }
502             // Magically treat the contextid fields.
503             if ($property == 'contextid') {
504                 if (isset($properties['context'])) {
505                     throw new coding_exception('There cannot be a context and a contextid column');
506                 }
507                 $returns += self::get_context_structure();
509             } else {
510                 $returns[$property] = new external_value($definition['type'], $property, $required, $default, $definition['null']);
512                 // Magically treat the format properties.
513                 if ($formatproperty = self::get_format_field($properties, $property)) {
514                     if (isset($returns[$formatproperty])) {
515                         throw new coding_exception('The format for \'' . $property . '\' is already defined.');
516                     }
517                     $returns[$formatproperty] = self::get_format_structure($property,
518                         $properties[$formatproperty], VALUE_OPTIONAL);
519                 }
520             }
521         }
523         return new external_single_structure($returns);
524     }