MDL-56459 competency: Invalid use of indirect property key
[moodle.git] / competency / classes / external / exporter.php
CommitLineData
d04ea166
DW
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 *
72018c0a 20 * @package core_competency
d04ea166
DW
21 * @copyright 2015 Damyon Wiese
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
72018c0a 24namespace core_competency\external;
dd1ce763 25defined('MOODLE_INTERNAL') || die();
d04ea166
DW
26
27require_once($CFG->libdir . '/externallib.php');
28
29use stdClass;
30use renderer_base;
31use context;
32use context_system;
33use coding_exception;
34use external_single_structure;
35use external_multiple_structure;
36use external_value;
37use external_format_value;
38
39/**
96c2b847 40 * Generic exporter to take a stdClass and prepare it for return by webservice, or as the context for a template.
c80630da
DW
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.
d04ea166
DW
44 *
45 * @copyright 2015 Damyon Wiese
46 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
47 */
48abstract class exporter {
49
50 /** @var array $related List of related objects used to avoid DB queries. */
51 protected $related = array();
52
96c2b847 53 /** @var stdClass|array The data of this exporter. */
d04ea166
DW
54 protected $data = null;
55
56 /**
57 * Constructor - saves the persistent object, and the related objects.
58 *
59 * @param mixed $data - Either an stdClass or an array of values.
60 * @param array $related - An optional list of pre-loaded objects related to this object.
61 */
62 public function __construct($data, $related = array()) {
63 $this->data = $data;
64 // Cache the valid related objects.
65 foreach (static::define_related() as $key => $classname) {
66 $isarray = false;
0875cc18
FM
67 $nullallowed = false;
68
69 // Allow ? to mean null is allowed.
70 if (substr($classname, -1) === '?') {
71 $classname = substr($classname, 0, -1);
72 $nullallowed = true;
73 }
74
d04ea166
DW
75 // Allow [] to mean an array of values.
76 if (substr($classname, -2) === '[]') {
77 $classname = substr($classname, 0, -2);
78 $isarray = true;
79 }
0875cc18 80
d04ea166 81 $missingdataerr = 'Exporter class is missing required related data: (' . get_called_class() . ') ';
0875cc18
FM
82
83 if ($nullallowed && array_key_exists($key, $related) && $related[$key] === null) {
84 $this->related[$key] = $related[$key];
85
86 } else if ($isarray) {
d04ea166
DW
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 }
0875cc18 97
d04ea166 98 } else {
0875cc18 99 if (array_key_exists($key, $related) && $related[$key] instanceof $classname) {
d04ea166
DW
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 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();
c80630da 118 $properties = self::read_properties_definition();
d04ea166
DW
119 $context = $this->get_context();
120 $values = (array) $this->data;
c80630da
DW
121
122 $othervalues = $this->get_other_values($output);
123 if (array_intersect_key($values, $othervalues)) {
124 // Attempt to replace a standard property.
1f769953 125 throw new coding_exception('Cannot override a standard property value.');
c80630da
DW
126 }
127 $values += $othervalues;
d04ea166
DW
128 $record = (object) $values;
129
130 foreach ($properties as $property => $definition) {
131 if (isset($data->$property)) {
132 // This happens when we have already defined the format properties.
133 continue;
b274c295
FM
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;
d04ea166
DW
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 }
144
145 $data->$property = $record->$property;
146
e54f8c4d 147 // If the field is PARAM_RAW and has a format field.
d04ea166 148 if ($propertyformat = self::get_format_field($properties, $property)) {
c80630da
DW
149 if (!property_exists($record, $propertyformat)) {
150 // Whoops, we got something that wasn't defined.
151 throw new coding_exception('Unexpected property ' . $propertyformat);
152 }
d04ea166 153 $format = $record->$propertyformat;
72018c0a 154 list($text, $format) = external_format_text($data->$property, $format, $context->id, 'core_competency', '', 0);
d04ea166
DW
155 $data->$property = $text;
156 $data->$propertyformat = $format;
157
d04ea166 158 } else if ($definition['type'] === PARAM_TEXT) {
c80630da
DW
159 if (!empty($definition['multiple'])) {
160 foreach ($data->$property as $key => $value) {
0eda810d 161 $data->{$property}[$key] = external_format_string($value, $context->id);
c80630da
DW
162 }
163 } else {
164 $data->$property = external_format_string($data->$property, $context->id);
165 }
d04ea166
DW
166 }
167 }
168
169 return $data;
170 }
171
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 }
186
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 *
96c2b847 201 * @param renderer_base $output The renderer.
d04ea166
DW
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 }
207
208 /**
c80630da
DW
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 }
227
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()}.
d04ea166 231 *
d04ea166
DW
232 * @return array Keys are the property names, and value their definition.
233 */
c80630da 234 final public static function properties_definition() {
d04ea166 235 $properties = static::define_properties();
c80630da
DW
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;
d04ea166 240 }
d04ea166
DW
241 }
242 return $properties;
243 }
244
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
67bc0eaf 252 * defined in {@link \core_competency\persistent::define_properties()}. The display properties
d04ea166
DW
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 *
c80630da
DW
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 * ),
d04ea166 272 *
b274c295
FM
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 *
d04ea166
DW
282 * @return array
283 */
284 protected static function define_other_properties() {
285 return array();
286 }
287
288 /**
289 * Return the list of properties.
290 *
291 * The format of the array returned by this method has to match the structure
67bc0eaf 292 * defined in {@link \core_competency\persistent::define_properties()}.
d04ea166
DW
293 *
294 * @return array
295 */
296 protected static function define_properties() {
297 return array();
298 }
299
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 *
0875cc18
FM
305 * The class name can be suffixed:
306 * - with [] to indicate an array of values.
307 * - with ? to indicate that 'null' is allowed.
c80630da 308 *
d04ea166
DW
309 * @return array of 'propertyname' => array('type' => classname, 'required' => true)
310 */
311 protected static function define_related() {
312 return array();
313 }
314
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 }
327
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';
e54f8c4d 337 if ($definitions[$property]['type'] == PARAM_RAW && isset($definitions[$formatproperty])
d04ea166
DW
338 && $definitions[$formatproperty]['type'] == PARAM_INT) {
339 return $formatproperty;
340 }
341 return false;
342 }
343
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 }
358
359 /**
360 * Returns the create structure.
361 *
362 * @return external_single_structure
363 */
364 final public static function get_create_structure() {
c80630da 365 $properties = self::properties_definition();
d04ea166
DW
366 $returns = array();
367
368 foreach ($properties as $property => $definition) {
369 if ($property == 'id') {
370 // The can not be set on create.
371 continue;
372
373 } else if (isset($returns[$property]) && substr($property, -6) === 'format') {
374 // We've already treated the format.
375 continue;
376 }
377
378 $required = VALUE_REQUIRED;
379 $default = null;
380
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 }
386
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();
393
394 } else {
395 $returns[$property] = new external_value($definition['type'], $property, $required, $default, $definition['null']);
396
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 }
407
408 return new external_single_structure($returns);
409 }
410
411 /**
412 * Returns the read structure.
413 *
414 * @return external_single_structure
415 */
416 final public static function get_read_structure() {
c80630da 417 $properties = self::read_properties_definition();
d04ea166
DW
418
419 return self::get_read_structure_from_properties($properties);
420 }
421
422 /**
423 * Returns the read structure from a set of properties (recursive).
424 *
96c2b847
FM
425 * @param array $properties The properties.
426 * @param int $required Whether is required.
427 * @param mixed $default The default value.
d04ea166
DW
428 * @return external_single_structure
429 */
b274c295 430 final protected static function get_read_structure_from_properties($properties, $required = VALUE_REQUIRED, $default = null) {
d04ea166
DW
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;
438
439 $type = $definition['type'];
b274c295
FM
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 }
449
d04ea166
DW
450 if (is_array($type)) {
451 // This is a nested array of more properties.
b274c295 452 $thisvalue = self::get_read_structure_from_properties($type, $proprequired, $propdefault);
d04ea166
DW
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 }
b274c295 458 $thisvalue = new external_value($type, $property, $proprequired, $propdefault, $definition['null']);
d04ea166
DW
459 }
460 if (!empty($definition['multiple'])) {
b274c295 461 $returns[$property] = new external_multiple_structure($thisvalue, '', $proprequired, $propdefault);
d04ea166
DW
462 } else {
463 $returns[$property] = $thisvalue;
464
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 }
474
b274c295 475 return new external_single_structure($returns, '', $required, $default);
d04ea166
DW
476 }
477
478 /**
479 * Returns the update structure.
480 *
b274c295
FM
481 * This structure can never be included at the top level for an external function signature
482 * because it contains optional parameters.
483 *
d04ea166
DW
484 * @return external_single_structure
485 */
486 final public static function get_update_structure() {
c80630da 487 $properties = self::properties_definition();
d04ea166
DW
488 $returns = array();
489
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 }
495
496 $default = null;
497 $required = VALUE_OPTIONAL;
498 if ($property == 'id') {
499 $required = VALUE_REQUIRED;
500 }
501
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();
508
509 } else {
510 $returns[$property] = new external_value($definition['type'], $property, $required, $default, $definition['null']);
511
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 }
522
523 return new external_single_structure($returns);
524 }
525
526}