PageRenderTime 44ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/classes/external/exporter.php

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