Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 *
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
 
26
use stdClass;
27
use renderer_base;
28
use context;
29
use coding_exception;
30
use core_external\external_format_value;
31
use core_external\external_multiple_structure;
32
use core_external\external_single_structure;
33
use core_external\external_value;
34
 
35
/**
36
 * Generic exporter to take a stdClass and prepare it for return by webservice, or as the context for a template.
37
 *
38
 * templatable classes implementing export_for_template, should always use a standard exporter if it exists.
39
 * External functions should always use a standard exporter if it exists.
40
 *
41
 * @copyright  2015 Damyon Wiese
42
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43
 */
44
abstract class exporter {
45
 
46
    /** @var array $related List of related objects used to avoid DB queries. */
47
    protected $related = array();
48
 
49
    /** @var stdClass|array The data of this exporter. */
50
    protected $data = null;
51
 
52
    /**
53
     * Constructor - saves the persistent object, and the related objects.
54
     *
55
     * @param mixed $data - Either an stdClass or an array of values.
56
     * @param array $related - An optional list of pre-loaded objects related to this object.
57
     */
58
    public function __construct($data, $related = array()) {
59
        $this->data = $data;
60
        // Cache the valid related objects.
61
        foreach (static::define_related() as $key => $classname) {
62
            $isarray = false;
63
            $nullallowed = false;
64
 
65
            // Allow ? to mean null is allowed.
66
            if (substr($classname, -1) === '?') {
67
                $classname = substr($classname, 0, -1);
68
                $nullallowed = true;
69
            }
70
 
71
            // Allow [] to mean an array of values.
72
            if (substr($classname, -2) === '[]') {
73
                $classname = substr($classname, 0, -2);
74
                $isarray = true;
75
            }
76
 
77
            $missingdataerr = 'Exporter class is missing required related data: (' . get_called_class() . ') ';
78
            $scalartypes = ['string', 'int', 'bool', 'float'];
79
            $scalarcheck = 'is_' . $classname;
80
 
81
            if ($nullallowed && (!array_key_exists($key, $related) || $related[$key] === null)) {
82
                $this->related[$key] = null;
83
 
84
            } else if ($isarray) {
85
                if (array_key_exists($key, $related) && is_array($related[$key])) {
86
                    foreach ($related[$key] as $index => $value) {
87
                        if (!$value instanceof $classname && !$scalarcheck($value)) {
88
                            throw new coding_exception($missingdataerr . $key . ' => ' . $classname . '[]');
89
                        }
90
                    }
91
                    $this->related[$key] = $related[$key];
92
                } else {
93
                    throw new coding_exception($missingdataerr . $key . ' => ' . $classname . '[]');
94
                }
95
 
96
            } else {
97
                if (array_key_exists($key, $related) &&
98
                        ((in_array($classname, $scalartypes) && $scalarcheck($related[$key])) ||
99
                        ($related[$key] instanceof $classname))) {
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 \core_external\util::format_text (or \core_external\util::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;
120
 
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;
128
 
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
            }
143
 
144
            $data->$property = $record->$property;
145
 
146
            // If the field is PARAM_RAW and has a format field.
147
            if ($propertyformat = self::get_format_field($properties, $property)) {
148
                $formatdefinition = $properties[$propertyformat];
149
                if (!property_exists($record, $propertyformat) && !array_key_exists('default', $formatdefinition)) {
150
                    // Whoops, we got something that wasn't defined.
151
                    throw new coding_exception('Unexpected property ' . $propertyformat);
152
                }
153
 
154
                $formatparams = $this->get_format_parameters($property);
155
                $format = $record->$propertyformat ?? $formatdefinition['default'];
156
 
157
                list($text, $format) = \core_external\util::format_text($data->$property, $format, $formatparams['context'],
158
                    $formatparams['component'], $formatparams['filearea'], $formatparams['itemid'], $formatparams['options']);
159
 
160
                $data->$property = $text;
161
                $data->$propertyformat = $format;
162
 
163
            } else if ($definition['type'] === PARAM_TEXT) {
164
                $formatparams = $this->get_format_parameters($property);
165
 
166
                if (!empty($definition['multiple'])) {
167
                    foreach ($data->$property as $key => $value) {
168
                        $data->{$property}[$key] = \core_external\util::format_string($value, $formatparams['context'],
169
                            $formatparams['striplinks'], $formatparams['options']);
170
                    }
171
                } else {
172
                    $data->$property = \core_external\util::format_string($data->$property, $formatparams['context'],
173
                            $formatparams['striplinks'], $formatparams['options']);
174
                }
175
            }
176
        }
177
 
178
        return $data;
179
    }
180
 
181
    /**
182
     * Get the format parameters.
183
     *
184
     * This method returns the parameters to use with the functions \core_external\util::format_text(), and
185
     * \core_external\util::format_string(). To override the default parameters, you can define a protected method
186
     * called 'get_format_parameters_for_<propertyName>'. For example, 'get_format_parameters_for_description',
187
     * if your property is 'description'.
188
     *
189
     * Your method must return an array containing any of the following keys:
190
     * - context: The context to use. Defaults to $this->related['context'] if defined, else throws an exception.
191
     * - component: The component to use with \core_external\util::format_text(). Defaults to null.
192
     * - filearea: The filearea to use with \core_external\util::format_text(). Defaults to null.
193
     * - itemid: The itemid to use with \core_external\util::format_text(). Defaults to null.
194
     * - options: An array of options accepted by \core_external\util::format_text()
195
     *            or \core_external\util::format_string().
196
     *            Defaults to [].
197
     * - striplinks: Whether to strip the links with \core_external\util::format_string(). Defaults to true.
198
     *
199
     * @param string $property The property to get the parameters for.
200
     * @return array
201
     */
202
    final protected function get_format_parameters($property) {
203
        $parameters = [
204
            'component' => null,
205
            'filearea' => null,
206
            'itemid' => null,
207
            'options' => [],
208
            'striplinks' => true,
209
        ];
210
 
211
        $candidate = 'get_format_parameters_for_' . $property;
212
        if (method_exists($this, $candidate)) {
213
            $parameters = array_merge($parameters, $this->{$candidate}());
214
        }
215
 
216
        if (!isset($parameters['context'])) {
217
            if (!isset($this->related['context']) || !($this->related['context'] instanceof context)) {
218
                throw new coding_exception("Unknown context to use for formatting the property '$property' in the " .
219
                    "exporter '" . get_class($this) . "'. You either need to add 'context' to your related objects, " .
220
                    "or create the method '$candidate' and return the context from there.");
221
            }
222
            $parameters['context'] = $this->related['context'];
223
 
224
        } else if (!($parameters['context'] instanceof context)) {
225
            throw new coding_exception("The context given to format the property '$property' in the exporter '" .
226
                get_class($this) . "' is invalid.");
227
        }
228
 
229
        return $parameters;
230
    }
231
 
232
    /**
233
     * Get the additional values to inject while exporting.
234
     *
235
     * These are additional generated values that are not passed in through $data
236
     * to the exporter. For a persistent exporter - these are generated values that
237
     * do not exist in the persistent class. For your convenience the format_text or
238
     * format_string functions do not need to be applied to PARAM_TEXT fields,
239
     * it will be done automatically during export.
240
     *
241
     * These values are only used when returning data via {@link self::export()},
242
     * they are not used when generating any of the different external structures.
243
     *
244
     * Note: These must be defined in {@link self::define_other_properties()}.
245
     *
246
     * @param renderer_base $output The renderer.
247
     * @return array Keys are the property names, values are their values.
248
     */
249
    protected function get_other_values(renderer_base $output) {
250
        return array();
251
    }
252
 
253
    /**
254
     * Get the read properties definition of this exporter. Read properties combines the
255
     * default properties from the model (persistent or stdClass) with the properties defined
256
     * by {@link self::define_other_properties()}.
257
     *
258
     * @return array Keys are the property names, and value their definition.
259
     */
260
    final public static function read_properties_definition() {
261
        $properties = static::properties_definition();
262
        $customprops = static::define_other_properties();
263
        $customprops = static::format_properties($customprops);
264
        $properties += $customprops;
265
        return $properties;
266
    }
267
 
268
    /**
269
     * Recursively formats a given property definition with the default fields required.
270
     *
271
     * @param array $properties List of properties to format
272
     * @return array Formatted array
273
     */
274
    final public static function format_properties($properties) {
275
        foreach ($properties as $property => $definition) {
276
            // Ensures that null is set to its default.
277
            if (!isset($definition['null'])) {
278
                $properties[$property]['null'] = NULL_NOT_ALLOWED;
279
            }
280
            if (!isset($definition['description'])) {
281
                $properties[$property]['description'] = $property;
282
            }
283
 
284
            // If an array is provided, it may be a nested array that is unformatted so rinse and repeat.
285
            if (is_array($definition['type'])) {
286
                $properties[$property]['type'] = static::format_properties($definition['type']);
287
            }
288
        }
289
        return $properties;
290
    }
291
 
292
    /**
293
     * Get the properties definition of this exporter used for create, and update structures.
294
     * The read structures are returned by: {@link self::read_properties_definition()}.
295
     *
296
     * @return array Keys are the property names, and value their definition.
297
     */
298
    final public static function properties_definition() {
299
        $properties = static::define_properties();
300
        foreach ($properties as $property => $definition) {
301
            // Ensures that null is set to its default.
302
            if (!isset($definition['null'])) {
303
                $properties[$property]['null'] = NULL_NOT_ALLOWED;
304
            }
305
            if (!isset($definition['description'])) {
306
                $properties[$property]['description'] = $property;
307
            }
308
        }
309
        return $properties;
310
    }
311
 
312
    /**
313
     * Return the list of additional properties used only for display.
314
     *
315
     * Additional properties are only ever used for the read structure, and during
316
     * export of the persistent data.
317
     *
318
     * The format of the array returned by this method has to match the structure
319
     * defined in {@link \core\persistent::define_properties()}. The display properties
320
     * can however do some more fancy things. They can define 'multiple' => true to wrap
321
     * values in an external_multiple_structure automatically - or they can define the
322
     * type as a nested array of more properties in order to generate a nested
323
     * external_single_structure.
324
     *
325
     * You can specify an array of values by including a 'multiple' => true array value. This
326
     * will result in a nested external_multiple_structure.
327
     * E.g.
328
     *
329
     *       'arrayofbools' => array(
330
     *           'type' => PARAM_BOOL,
331
     *           'multiple' => true
332
     *       ),
333
     *
334
     * You can return a nested array in the type field, which will result in a nested external_single_structure.
335
     * E.g.
336
     *      'competency' => array(
337
     *          'type' => competency_exporter::read_properties_definition()
338
     *       ),
339
     *
340
     * Other properties can be specifically marked as optional, in which case they do not need
341
     * to be included in the export in {@link self::get_other_values()}. This is useful when exporting
342
     * a substructure which cannot be set as null due to webservices protocol constraints.
343
     * E.g.
344
     *      'competency' => array(
345
     *          'type' => competency_exporter::read_properties_definition(),
346
     *          'optional' => true
347
     *       ),
348
     *
349
     * @return array
350
     */
351
    protected static function define_other_properties() {
352
        return array();
353
    }
354
 
355
    /**
356
     * Return the list of properties.
357
     *
358
     * The format of the array returned by this method has to match the structure
359
     * defined in {@link \core\persistent::define_properties()}. Howewer you can
360
     * add a new attribute "description" to describe the parameter for documenting the API.
361
     *
362
     * Note that the type PARAM_TEXT should ONLY be used for strings which need to
363
     * go through filters (multilang, etc...) and do not have a FORMAT_* associated
364
     * to them. Typically strings passed through to format_string().
365
     *
366
     * Other filtered strings which use a FORMAT_* constant (hear used with format_text)
367
     * must be defined as PARAM_RAW.
368
     *
369
     * @return array
370
     */
371
    protected static function define_properties() {
372
        return array();
373
    }
374
 
375
    /**
376
     * Returns a list of objects that are related to this persistent.
377
     *
378
     * Only objects listed here can be cached in this object.
379
     *
380
     * The class name can be suffixed:
381
     * - with [] to indicate an array of values.
382
     * - with ? to indicate that 'null' is allowed.
383
     *
384
     * @return array of 'propertyname' => array('type' => classname, 'required' => true)
385
     */
386
    protected static function define_related() {
387
        return array();
388
    }
389
 
390
    /**
391
     * Get the context structure.
392
     *
393
     * @return array
394
     */
395
    final protected static function get_context_structure() {
396
        return array(
397
            'contextid' => new external_value(PARAM_INT, 'The context id', VALUE_OPTIONAL),
398
            'contextlevel' => new external_value(PARAM_ALPHA, 'The context level', VALUE_OPTIONAL),
399
            'instanceid' => new external_value(PARAM_INT, 'The Instance id', VALUE_OPTIONAL),
400
        );
401
    }
402
 
403
    /**
404
     * Get the format field name.
405
     *
406
     * @param  array $definitions List of properties definitions.
407
     * @param  string $property The name of the property that may have a format field.
408
     * @return bool|string False, or the name of the format property.
409
     */
410
    final protected static function get_format_field($definitions, $property) {
411
        $formatproperty = $property . 'format';
412
        if (($definitions[$property]['type'] == PARAM_RAW || $definitions[$property]['type'] == PARAM_CLEANHTML)
413
                && isset($definitions[$formatproperty])
414
                && $definitions[$formatproperty]['type'] == PARAM_INT) {
415
            return $formatproperty;
416
        }
417
        return false;
418
    }
419
 
420
    /**
421
     * Get the format structure.
422
     *
423
     * @param  string $property   The name of the property on which the format applies.
424
     * @param  array  $definition The definition of the format property.
425
     * @param  int    $required   Constant VALUE_*.
426
     * @return external_format_value
427
     */
428
    final protected static function get_format_structure($property, $definition, $required = VALUE_REQUIRED) {
429
        $default = null;
430
        if (array_key_exists('default', $definition)) {
431
            $required = VALUE_DEFAULT;
432
            $default = $definition['default'];
433
        }
434
        return new external_format_value($property, $required, $default);
435
    }
436
 
437
    /**
438
     * Returns the create structure.
439
     *
440
     * @return external_single_structure
441
     */
442
    final public static function get_create_structure() {
443
        $properties = self::properties_definition();
444
        $returns = array();
445
 
446
        foreach ($properties as $property => $definition) {
447
            if ($property == 'id') {
448
                // The can not be set on create.
449
                continue;
450
 
451
            } else if (isset($returns[$property]) && substr($property, -6) === 'format') {
452
                // We've already treated the format.
453
                continue;
454
            }
455
 
456
            $required = VALUE_REQUIRED;
457
            $default = null;
458
 
459
            // We cannot use isset here because we want to detect nulls.
460
            if (array_key_exists('default', $definition)) {
461
                $required = VALUE_DEFAULT;
462
                $default = $definition['default'];
463
            }
464
 
465
            // Magically treat the contextid fields.
466
            if ($property == 'contextid') {
467
                if (isset($properties['context'])) {
468
                    throw new coding_exception('There cannot be a context and a contextid column');
469
                }
470
                $returns += self::get_context_structure();
471
 
472
            } else {
473
                $returns[$property] = new external_value($definition['type'], $definition['description'], $required, $default,
474
                    $definition['null']);
475
 
476
                // Magically treat the format properties.
477
                if ($formatproperty = self::get_format_field($properties, $property)) {
478
                    if (isset($returns[$formatproperty])) {
479
                        throw new coding_exception('The format for \'' . $property . '\' is already defined.');
480
                    }
481
                    $returns[$formatproperty] = self::get_format_structure($property,
482
                        $properties[$formatproperty], VALUE_REQUIRED);
483
                }
484
            }
485
        }
486
 
487
        return new external_single_structure($returns);
488
    }
489
 
490
    /**
491
     * Returns the read structure.
492
     *
493
     * @param int $required Whether is required.
494
     * @param mixed $default The default value.
495
     *
496
     * @return external_single_structure
497
     */
498
    final public static function get_read_structure($required = VALUE_REQUIRED, $default = null) {
499
        $properties = self::read_properties_definition();
500
 
501
        return self::get_read_structure_from_properties($properties, $required, $default);
502
    }
503
 
504
    /**
505
     * Returns the read structure from a set of properties (recursive).
506
     *
507
     * @param array $properties The properties.
508
     * @param int $required Whether is required.
509
     * @param mixed $default The default value.
510
     * @return external_single_structure
511
     */
512
    final protected static function get_read_structure_from_properties($properties, $required = VALUE_REQUIRED, $default = null) {
513
        $returns = array();
514
        foreach ($properties as $property => $definition) {
515
            if (isset($returns[$property]) && substr($property, -6) === 'format') {
516
                // We've already treated the format.
517
                continue;
518
            }
519
            $thisvalue = null;
520
 
521
            $type = $definition['type'];
522
            $proprequired = VALUE_REQUIRED;
523
            $propdefault = null;
524
            if (array_key_exists('default', $definition)) {
525
                $propdefault = $definition['default'];
526
            }
527
            if (array_key_exists('optional', $definition)) {
528
                // Mark as optional. Note that this should only apply to "reading" "other" properties.
529
                $proprequired = VALUE_OPTIONAL;
530
            }
531
 
532
            if (is_array($type)) {
533
                // This is a nested array of more properties.
534
                $thisvalue = self::get_read_structure_from_properties($type, $proprequired, $propdefault);
535
            } else {
536
                if ($definition['type'] == PARAM_TEXT || $definition['type'] == PARAM_CLEANHTML) {
537
                    // PARAM_TEXT always becomes PARAM_RAW because filters may be applied.
538
                    $type = PARAM_RAW;
539
                }
540
                $thisvalue = new external_value($type, $definition['description'], $proprequired, $propdefault, $definition['null']);
541
            }
542
            if (!empty($definition['multiple'])) {
543
                $returns[$property] = new external_multiple_structure($thisvalue, $definition['description'], $proprequired,
544
                    $propdefault);
545
            } else {
546
                $returns[$property] = $thisvalue;
547
 
548
                // Magically treat the format properties (not possible for arrays).
549
                if ($formatproperty = self::get_format_field($properties, $property)) {
550
                    if (isset($returns[$formatproperty])) {
551
                        throw new coding_exception('The format for \'' . $property . '\' is already defined.');
552
                    }
553
                    $formatpropertydef = $properties[$formatproperty];
554
                    $formatpropertyrequired = VALUE_REQUIRED;
555
                    if (!empty($formatpropertydef['optional'])) {
556
                        $formatpropertyrequired = VALUE_OPTIONAL;
557
                    }
558
                    $returns[$formatproperty] = self::get_format_structure($property, $formatpropertydef, $formatpropertyrequired);
559
                }
560
            }
561
        }
562
 
563
        return new external_single_structure($returns, '', $required, $default);
564
    }
565
 
566
    /**
567
     * Returns the update structure.
568
     *
569
     * This structure can never be included at the top level for an external function signature
570
     * because it contains optional parameters.
571
     *
572
     * @return external_single_structure
573
     */
574
    final public static function get_update_structure() {
575
        $properties = self::properties_definition();
576
        $returns = array();
577
 
578
        foreach ($properties as $property => $definition) {
579
            if (isset($returns[$property]) && substr($property, -6) === 'format') {
580
                // We've already treated the format.
581
                continue;
582
            }
583
 
584
            $default = null;
585
            $required = VALUE_OPTIONAL;
586
            if ($property == 'id') {
587
                $required = VALUE_REQUIRED;
588
            }
589
 
590
            // Magically treat the contextid fields.
591
            if ($property == 'contextid') {
592
                if (isset($properties['context'])) {
593
                    throw new coding_exception('There cannot be a context and a contextid column');
594
                }
595
                $returns += self::get_context_structure();
596
 
597
            } else {
598
                $returns[$property] = new external_value($definition['type'], $definition['description'], $required, $default,
599
                    $definition['null']);
600
 
601
                // Magically treat the format properties.
602
                if ($formatproperty = self::get_format_field($properties, $property)) {
603
                    if (isset($returns[$formatproperty])) {
604
                        throw new coding_exception('The format for \'' . $property . '\' is already defined.');
605
                    }
606
                    $returns[$formatproperty] = self::get_format_structure($property,
607
                        $properties[$formatproperty], VALUE_OPTIONAL);
608
                }
609
            }
610
        }
611
 
612
        return new external_single_structure($returns);
613
    }
614
 
615
}