Commit | Line | Data |
---|---|---|
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 | 24 | namespace core_competency\external; |
dd1ce763 | 25 | defined('MOODLE_INTERNAL') || die(); |
d04ea166 DW |
26 | |
27 | require_once($CFG->libdir . '/externallib.php'); | |
28 | ||
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; | |
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 | */ | |
48 | abstract 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 | } |