AutorÃa | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** Generic exporter to take a stdClass and prepare it for return by webservice.** @package core* @copyright 2015 Damyon Wiese* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/namespace core\external;use stdClass;use renderer_base;use context;use coding_exception;use core_external\external_format_value;use core_external\external_multiple_structure;use core_external\external_single_structure;use core_external\external_value;/*** Generic exporter to take a stdClass and prepare it for return by webservice, or as the context for a template.** templatable classes implementing export_for_template, should always use a standard exporter if it exists.* External functions should always use a standard exporter if it exists.** @copyright 2015 Damyon Wiese* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/abstract class exporter {/** @var array $related List of related objects used to avoid DB queries. */protected $related = array();/** @var stdClass|array The data of this exporter. */protected $data = null;/*** Constructor - saves the persistent object, and the related objects.** @param mixed $data - Either an stdClass or an array of values.* @param array $related - An optional list of pre-loaded objects related to this object.*/public function __construct($data, $related = array()) {$this->data = $data;// Cache the valid related objects.foreach (static::define_related() as $key => $classname) {$isarray = false;$nullallowed = false;// Allow ? to mean null is allowed.if (substr($classname, -1) === '?') {$classname = substr($classname, 0, -1);$nullallowed = true;}// Allow [] to mean an array of values.if (substr($classname, -2) === '[]') {$classname = substr($classname, 0, -2);$isarray = true;}$missingdataerr = 'Exporter class is missing required related data: (' . get_called_class() . ') ';$scalartypes = ['string', 'int', 'bool', 'float'];$scalarcheck = 'is_' . $classname;if ($nullallowed && (!array_key_exists($key, $related) || $related[$key] === null)) {$this->related[$key] = null;} else if ($isarray) {if (array_key_exists($key, $related) && is_array($related[$key])) {foreach ($related[$key] as $index => $value) {if (!$value instanceof $classname && !$scalarcheck($value)) {throw new coding_exception($missingdataerr . $key . ' => ' . $classname . '[]');}}$this->related[$key] = $related[$key];} else {throw new coding_exception($missingdataerr . $key . ' => ' . $classname . '[]');}} else {if (array_key_exists($key, $related) &&((in_array($classname, $scalartypes) && $scalarcheck($related[$key])) ||($related[$key] instanceof $classname))) {$this->related[$key] = $related[$key];} else {throw new coding_exception($missingdataerr . $key . ' => ' . $classname);}}}}/*** Function to export the renderer data in a format that is suitable for a* mustache template. This means raw records are generated as in to_record,* but all strings are correctly passed through \core_external\util::format_text (or \core_external\util::format_string).** @param renderer_base $output Used to do a final render of any components that need to be rendered for export.* @return stdClass*/final public function export(renderer_base $output) {$data = new stdClass();$properties = self::read_properties_definition();$values = (array) $this->data;$othervalues = $this->get_other_values($output);if (array_intersect_key($values, $othervalues)) {// Attempt to replace a standard property.throw new coding_exception('Cannot override a standard property value.');}$values += $othervalues;$record = (object) $values;foreach ($properties as $property => $definition) {if (isset($data->$property)) {// This happens when we have already defined the format properties.continue;} else if (!property_exists($record, $property) && array_key_exists('default', $definition)) {// We have a default value for this property.$record->$property = $definition['default'];} else if (!property_exists($record, $property) && !empty($definition['optional'])) {// Fine, this property can be omitted.continue;} else if (!property_exists($record, $property)) {// Whoops, we got something that wasn't defined.throw new coding_exception('Unexpected property ' . $property);}$data->$property = $record->$property;// If the field is PARAM_RAW and has a format field.if ($propertyformat = self::get_format_field($properties, $property)) {$formatdefinition = $properties[$propertyformat];if (!property_exists($record, $propertyformat) && !array_key_exists('default', $formatdefinition)) {// Whoops, we got something that wasn't defined.throw new coding_exception('Unexpected property ' . $propertyformat);}$formatparams = $this->get_format_parameters($property);$format = $record->$propertyformat ?? $formatdefinition['default'];list($text, $format) = \core_external\util::format_text($data->$property, $format, $formatparams['context'],$formatparams['component'], $formatparams['filearea'], $formatparams['itemid'], $formatparams['options']);$data->$property = $text;$data->$propertyformat = $format;} else if ($definition['type'] === PARAM_TEXT) {$formatparams = $this->get_format_parameters($property);if (!empty($definition['multiple'])) {foreach ($data->$property as $key => $value) {$data->{$property}[$key] = \core_external\util::format_string($value, $formatparams['context'],$formatparams['striplinks'], $formatparams['options']);}} else {$data->$property = \core_external\util::format_string($data->$property, $formatparams['context'],$formatparams['striplinks'], $formatparams['options']);}}}return $data;}/*** Get the format parameters.** This method returns the parameters to use with the functions \core_external\util::format_text(), and* \core_external\util::format_string(). To override the default parameters, you can define a protected method* called 'get_format_parameters_for_<propertyName>'. For example, 'get_format_parameters_for_description',* if your property is 'description'.** Your method must return an array containing any of the following keys:* - context: The context to use. Defaults to $this->related['context'] if defined, else throws an exception.* - component: The component to use with \core_external\util::format_text(). Defaults to null.* - filearea: The filearea to use with \core_external\util::format_text(). Defaults to null.* - itemid: The itemid to use with \core_external\util::format_text(). Defaults to null.* - options: An array of options accepted by \core_external\util::format_text()* or \core_external\util::format_string().* Defaults to [].* - striplinks: Whether to strip the links with \core_external\util::format_string(). Defaults to true.** @param string $property The property to get the parameters for.* @return array*/final protected function get_format_parameters($property) {$parameters = ['component' => null,'filearea' => null,'itemid' => null,'options' => [],'striplinks' => true,];$candidate = 'get_format_parameters_for_' . $property;if (method_exists($this, $candidate)) {$parameters = array_merge($parameters, $this->{$candidate}());}if (!isset($parameters['context'])) {if (!isset($this->related['context']) || !($this->related['context'] instanceof context)) {throw new coding_exception("Unknown context to use for formatting the property '$property' in the " ."exporter '" . get_class($this) . "'. You either need to add 'context' to your related objects, " ."or create the method '$candidate' and return the context from there.");}$parameters['context'] = $this->related['context'];} else if (!($parameters['context'] instanceof context)) {throw new coding_exception("The context given to format the property '$property' in the exporter '" .get_class($this) . "' is invalid.");}return $parameters;}/*** Get the additional values to inject while exporting.** These are additional generated values that are not passed in through $data* to the exporter. For a persistent exporter - these are generated values that* do not exist in the persistent class. For your convenience the format_text or* format_string functions do not need to be applied to PARAM_TEXT fields,* it will be done automatically during export.** These values are only used when returning data via {@link self::export()},* they are not used when generating any of the different external structures.** Note: These must be defined in {@link self::define_other_properties()}.** @param renderer_base $output The renderer.* @return array Keys are the property names, values are their values.*/protected function get_other_values(renderer_base $output) {return array();}/*** Get the read properties definition of this exporter. Read properties combines the* default properties from the model (persistent or stdClass) with the properties defined* by {@link self::define_other_properties()}.** @return array Keys are the property names, and value their definition.*/final public static function read_properties_definition() {$properties = static::properties_definition();$customprops = static::define_other_properties();$customprops = static::format_properties($customprops);$properties += $customprops;return $properties;}/*** Recursively formats a given property definition with the default fields required.** @param array $properties List of properties to format* @return array Formatted array*/final public static function format_properties($properties) {foreach ($properties as $property => $definition) {// Ensures that null is set to its default.if (!isset($definition['null'])) {$properties[$property]['null'] = NULL_NOT_ALLOWED;}if (!isset($definition['description'])) {$properties[$property]['description'] = $property;}// If an array is provided, it may be a nested array that is unformatted so rinse and repeat.if (is_array($definition['type'])) {$properties[$property]['type'] = static::format_properties($definition['type']);}}return $properties;}/*** Get the properties definition of this exporter used for create, and update structures.* The read structures are returned by: {@link self::read_properties_definition()}.** @return array Keys are the property names, and value their definition.*/final public static function properties_definition() {$properties = static::define_properties();foreach ($properties as $property => $definition) {// Ensures that null is set to its default.if (!isset($definition['null'])) {$properties[$property]['null'] = NULL_NOT_ALLOWED;}if (!isset($definition['description'])) {$properties[$property]['description'] = $property;}}return $properties;}/*** Return the list of additional properties used only for display.** Additional properties are only ever used for the read structure, and during* export of the persistent data.** The format of the array returned by this method has to match the structure* defined in {@link \core\persistent::define_properties()}. The display properties* can however do some more fancy things. They can define 'multiple' => true to wrap* values in an external_multiple_structure automatically - or they can define the* type as a nested array of more properties in order to generate a nested* external_single_structure.** You can specify an array of values by including a 'multiple' => true array value. This* will result in a nested external_multiple_structure.* E.g.** 'arrayofbools' => array(* 'type' => PARAM_BOOL,* 'multiple' => true* ),** You can return a nested array in the type field, which will result in a nested external_single_structure.* E.g.* 'competency' => array(* 'type' => competency_exporter::read_properties_definition()* ),** Other properties can be specifically marked as optional, in which case they do not need* to be included in the export in {@link self::get_other_values()}. This is useful when exporting* a substructure which cannot be set as null due to webservices protocol constraints.* E.g.* 'competency' => array(* 'type' => competency_exporter::read_properties_definition(),* 'optional' => true* ),** @return array*/protected static function define_other_properties() {return array();}/*** Return the list of properties.** The format of the array returned by this method has to match the structure* defined in {@link \core\persistent::define_properties()}. Howewer you can* add a new attribute "description" to describe the parameter for documenting the API.** Note that the type PARAM_TEXT should ONLY be used for strings which need to* go through filters (multilang, etc...) and do not have a FORMAT_* associated* to them. Typically strings passed through to format_string().** Other filtered strings which use a FORMAT_* constant (hear used with format_text)* must be defined as PARAM_RAW.** @return array*/protected static function define_properties() {return array();}/*** Returns a list of objects that are related to this persistent.** Only objects listed here can be cached in this object.** The class name can be suffixed:* - with [] to indicate an array of values.* - with ? to indicate that 'null' is allowed.** @return array of 'propertyname' => array('type' => classname, 'required' => true)*/protected static function define_related() {return array();}/*** Get the context structure.** @return array*/final protected static function get_context_structure() {return array('contextid' => new external_value(PARAM_INT, 'The context id', VALUE_OPTIONAL),'contextlevel' => new external_value(PARAM_ALPHA, 'The context level', VALUE_OPTIONAL),'instanceid' => new external_value(PARAM_INT, 'The Instance id', VALUE_OPTIONAL),);}/*** Get the format field name.** @param array $definitions List of properties definitions.* @param string $property The name of the property that may have a format field.* @return bool|string False, or the name of the format property.*/final protected static function get_format_field($definitions, $property) {$formatproperty = $property . 'format';if (($definitions[$property]['type'] == PARAM_RAW || $definitions[$property]['type'] == PARAM_CLEANHTML)&& isset($definitions[$formatproperty])&& $definitions[$formatproperty]['type'] == PARAM_INT) {return $formatproperty;}return false;}/*** Get the format structure.** @param string $property The name of the property on which the format applies.* @param array $definition The definition of the format property.* @param int $required Constant VALUE_*.* @return external_format_value*/final protected static function get_format_structure($property, $definition, $required = VALUE_REQUIRED) {$default = null;if (array_key_exists('default', $definition)) {$required = VALUE_DEFAULT;$default = $definition['default'];}return new external_format_value($property, $required, $default);}/*** Returns the create structure.** @return external_single_structure*/final public static function get_create_structure() {$properties = self::properties_definition();$returns = array();foreach ($properties as $property => $definition) {if ($property == 'id') {// The can not be set on create.continue;} else if (isset($returns[$property]) && substr($property, -6) === 'format') {// We've already treated the format.continue;}$required = VALUE_REQUIRED;$default = null;// We cannot use isset here because we want to detect nulls.if (array_key_exists('default', $definition)) {$required = VALUE_DEFAULT;$default = $definition['default'];}// Magically treat the contextid fields.if ($property == 'contextid') {if (isset($properties['context'])) {throw new coding_exception('There cannot be a context and a contextid column');}$returns += self::get_context_structure();} else {$returns[$property] = new external_value($definition['type'], $definition['description'], $required, $default,$definition['null']);// Magically treat the format properties.if ($formatproperty = self::get_format_field($properties, $property)) {if (isset($returns[$formatproperty])) {throw new coding_exception('The format for \'' . $property . '\' is already defined.');}$returns[$formatproperty] = self::get_format_structure($property,$properties[$formatproperty], VALUE_REQUIRED);}}}return new external_single_structure($returns);}/*** Returns the read structure.** @param int $required Whether is required.* @param mixed $default The default value.** @return external_single_structure*/final public static function get_read_structure($required = VALUE_REQUIRED, $default = null) {$properties = self::read_properties_definition();return self::get_read_structure_from_properties($properties, $required, $default);}/*** Returns the read structure from a set of properties (recursive).** @param array $properties The properties.* @param int $required Whether is required.* @param mixed $default The default value.* @return external_single_structure*/final protected static function get_read_structure_from_properties($properties, $required = VALUE_REQUIRED, $default = null) {$returns = array();foreach ($properties as $property => $definition) {if (isset($returns[$property]) && substr($property, -6) === 'format') {// We've already treated the format.continue;}$thisvalue = null;$type = $definition['type'];$proprequired = VALUE_REQUIRED;$propdefault = null;if (array_key_exists('default', $definition)) {$propdefault = $definition['default'];}if (array_key_exists('optional', $definition)) {// Mark as optional. Note that this should only apply to "reading" "other" properties.$proprequired = VALUE_OPTIONAL;}if (is_array($type)) {// This is a nested array of more properties.$thisvalue = self::get_read_structure_from_properties($type, $proprequired, $propdefault);} else {if ($definition['type'] == PARAM_TEXT || $definition['type'] == PARAM_CLEANHTML) {// PARAM_TEXT always becomes PARAM_RAW because filters may be applied.$type = PARAM_RAW;}$thisvalue = new external_value($type, $definition['description'], $proprequired, $propdefault, $definition['null']);}if (!empty($definition['multiple'])) {$returns[$property] = new external_multiple_structure($thisvalue, $definition['description'], $proprequired,$propdefault);} else {$returns[$property] = $thisvalue;// Magically treat the format properties (not possible for arrays).if ($formatproperty = self::get_format_field($properties, $property)) {if (isset($returns[$formatproperty])) {throw new coding_exception('The format for \'' . $property . '\' is already defined.');}$formatpropertydef = $properties[$formatproperty];$formatpropertyrequired = VALUE_REQUIRED;if (!empty($formatpropertydef['optional'])) {$formatpropertyrequired = VALUE_OPTIONAL;}$returns[$formatproperty] = self::get_format_structure($property, $formatpropertydef, $formatpropertyrequired);}}}return new external_single_structure($returns, '', $required, $default);}/*** Returns the update structure.** This structure can never be included at the top level for an external function signature* because it contains optional parameters.** @return external_single_structure*/final public static function get_update_structure() {$properties = self::properties_definition();$returns = array();foreach ($properties as $property => $definition) {if (isset($returns[$property]) && substr($property, -6) === 'format') {// We've already treated the format.continue;}$default = null;$required = VALUE_OPTIONAL;if ($property == 'id') {$required = VALUE_REQUIRED;}// Magically treat the contextid fields.if ($property == 'contextid') {if (isset($properties['context'])) {throw new coding_exception('There cannot be a context and a contextid column');}$returns += self::get_context_structure();} else {$returns[$property] = new external_value($definition['type'], $definition['description'], $required, $default,$definition['null']);// Magically treat the format properties.if ($formatproperty = self::get_format_field($properties, $property)) {if (isset($returns[$formatproperty])) {throw new coding_exception('The format for \'' . $property . '\' is already defined.');}$returns[$formatproperty] = self::get_format_structure($property,$properties[$formatproperty], VALUE_OPTIONAL);}}}return new external_single_structure($returns);}}