Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]

   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   * Api customfield package
  19   *
  20   * @package   core_customfield
  21   * @copyright 2018 David Matamoros <davidmc@moodle.com>
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_customfield;
  26  
  27  use core\output\inplace_editable;
  28  use core_customfield\event\category_created;
  29  use core_customfield\event\category_deleted;
  30  use core_customfield\event\category_updated;
  31  use core_customfield\event\field_created;
  32  use core_customfield\event\field_deleted;
  33  use core_customfield\event\field_updated;
  34  
  35  defined('MOODLE_INTERNAL') || die;
  36  
  37  /**
  38   * Class api
  39   *
  40   * @package core_customfield
  41   * @copyright 2018 David Matamoros <davidmc@moodle.com>
  42   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  43   */
  44  class api {
  45  
  46      /**
  47       * For the given instance and list of fields fields retrieves data associated with them
  48       *
  49       * @param field_controller[] $fields list of fields indexed by field id
  50       * @param int $instanceid
  51       * @param bool $adddefaults
  52       * @return data_controller[] array of data_controller objects indexed by fieldid. All fields are present,
  53       *    some data_controller objects may have 'id', some not
  54       *     If ($adddefaults): All fieldids are present, some data_controller objects may have 'id', some not.
  55       *     If (!$adddefaults): Only fieldids with data are present, all data_controller objects have 'id'.
  56       */
  57      public static function get_instance_fields_data(array $fields, int $instanceid, bool $adddefaults = true) : array {
  58          return self::get_instances_fields_data($fields, [$instanceid], $adddefaults)[$instanceid];
  59      }
  60  
  61      /**
  62       * For given list of instances and fields retrieves data associated with them
  63       *
  64       * @param field_controller[] $fields list of fields indexed by field id
  65       * @param int[] $instanceids
  66       * @param bool $adddefaults
  67       * @return data_controller[][] 2-dimension array, first index is instanceid, second index is fieldid.
  68       *     If ($adddefaults): All instanceids and all fieldids are present, some data_controller objects may have 'id', some not.
  69       *     If (!$adddefaults): All instanceids are present but only fieldids with data are present, all
  70       *         data_controller objects have 'id'.
  71       */
  72      public static function get_instances_fields_data(array $fields, array $instanceids, bool $adddefaults = true) : array {
  73          global $DB;
  74  
  75          // Create the results array where instances and fields order is the same as in the input arrays.
  76          $result = array_fill_keys($instanceids, array_fill_keys(array_keys($fields), null));
  77  
  78          if (empty($instanceids) || empty($fields)) {
  79              return $result;
  80          }
  81  
  82          // Retrieve all existing data.
  83          list($sqlfields, $params) = $DB->get_in_or_equal(array_keys($fields), SQL_PARAMS_NAMED, 'fld');
  84          list($sqlinstances, $iparams) = $DB->get_in_or_equal($instanceids, SQL_PARAMS_NAMED, 'ins');
  85          $sql = "SELECT d.*
  86                    FROM {customfield_field} f
  87                    JOIN {customfield_data} d ON (f.id = d.fieldid AND d.instanceid {$sqlinstances})
  88                   WHERE f.id {$sqlfields}";
  89          $fieldsdata = $DB->get_recordset_sql($sql, $params + $iparams);
  90          foreach ($fieldsdata as $data) {
  91              $result[$data->instanceid][$data->fieldid] = data_controller::create(0, $data, $fields[$data->fieldid]);
  92          }
  93          $fieldsdata->close();
  94  
  95          if ($adddefaults) {
  96              // Add default data where it was not retrieved.
  97              foreach ($instanceids as $instanceid) {
  98                  foreach ($fields as $fieldid => $field) {
  99                      if ($result[$instanceid][$fieldid] === null) {
 100                          $result[$instanceid][$fieldid] =
 101                              data_controller::create(0, (object)['instanceid' => $instanceid], $field);
 102                      }
 103                  }
 104              }
 105          } else {
 106              // Remove null-placeholders for data that was not retrieved.
 107              foreach ($instanceids as $instanceid) {
 108                  $result[$instanceid] = array_filter($result[$instanceid]);
 109              }
 110          }
 111  
 112          return $result;
 113      }
 114  
 115      /**
 116       * Retrieve a list of all available custom field types
 117       *
 118       * @return   array   a list of the fieldtypes suitable to use in a select statement
 119       */
 120      public static function get_available_field_types() {
 121          $fieldtypes = array();
 122  
 123          $plugins = \core\plugininfo\customfield::get_enabled_plugins();
 124          foreach ($plugins as $type => $unused) {
 125              $fieldtypes[$type] = get_string('pluginname', 'customfield_' . $type);
 126          }
 127          asort($fieldtypes);
 128  
 129          return $fieldtypes;
 130      }
 131  
 132      /**
 133       * Updates or creates a field with data that came from a form
 134       *
 135       * @param field_controller $field
 136       * @param \stdClass $formdata
 137       */
 138      public static function save_field_configuration(field_controller $field, \stdClass $formdata) {
 139          foreach ($formdata as $key => $value) {
 140              if ($key === 'configdata' && is_array($formdata->configdata)) {
 141                  $field->set($key, json_encode($value));
 142              } else if ($key === 'id' || ($key === 'type' && $field->get('id'))) {
 143                  continue;
 144              } else if (field::has_property($key)) {
 145                  $field->set($key, $value);
 146              }
 147          }
 148  
 149          $isnewfield = empty($field->get('id'));
 150  
 151          // Process files in description.
 152          if (isset($formdata->description_editor)) {
 153              if (!$field->get('id')) {
 154                  // We need 'id' field to store files used in description.
 155                  $field->save();
 156              }
 157  
 158              $data = (object) ['description_editor' => $formdata->description_editor];
 159              $textoptions = $field->get_handler()->get_description_text_options();
 160              $data = file_postupdate_standard_editor($data, 'description', $textoptions, $textoptions['context'],
 161                  'core_customfield', 'description', $field->get('id'));
 162              $field->set('description', $data->description);
 163              $field->set('descriptionformat', $data->descriptionformat);
 164          }
 165  
 166          // Save the field.
 167          $field->save();
 168  
 169          if ($isnewfield) {
 170              // Move to the end of the category.
 171              self::move_field($field, $field->get('categoryid'));
 172          }
 173  
 174          if ($isnewfield) {
 175              field_created::create_from_object($field)->trigger();
 176          } else {
 177              field_updated::create_from_object($field)->trigger();
 178          }
 179      }
 180  
 181      /**
 182       * Change fields sort order, move field to another category
 183       *
 184       * @param field_controller $field field that needs to be moved
 185       * @param int $categoryid category that needs to be moved
 186       * @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
 187       */
 188      public static function move_field(field_controller $field, int $categoryid, int $beforeid = 0) {
 189          global $DB;
 190  
 191          if ($field->get('categoryid') != $categoryid) {
 192              // Move field to another category. Validate that this category exists and belongs to the same component/area/itemid.
 193              $category = $field->get_category();
 194              $DB->get_record(category::TABLE, [
 195                  'component' => $category->get('component'),
 196                  'area' => $category->get('area'),
 197                  'itemid' => $category->get('itemid'),
 198                  'id' => $categoryid], 'id', MUST_EXIST);
 199              $field->set('categoryid', $categoryid);
 200              $field->save();
 201              field_updated::create_from_object($field)->trigger();
 202          }
 203  
 204          // Reorder fields in the target category.
 205          $records = $DB->get_records(field::TABLE, ['categoryid' => $categoryid], 'sortorder, id', '*');
 206  
 207          $id = $field->get('id');
 208          $fieldsids = array_values(array_diff(array_keys($records), [$id]));
 209          $idx = $beforeid ? array_search($beforeid, $fieldsids) : false;
 210          if ($idx === false) {
 211              // Set as the last field.
 212              $fieldsids = array_merge($fieldsids, [$id]);
 213          } else {
 214              // Set before field with id $beforeid.
 215              $fieldsids = array_merge(array_slice($fieldsids, 0, $idx), [$id], array_slice($fieldsids, $idx));
 216          }
 217  
 218          foreach (array_values($fieldsids) as $idx => $fieldid) {
 219              // Use persistent class to update the sortorder for each field that needs updating.
 220              if ($records[$fieldid]->sortorder != $idx) {
 221                  $f = ($fieldid == $id) ? $field : new field(0, $records[$fieldid]);
 222                  $f->set('sortorder', $idx);
 223                  $f->save();
 224              }
 225          }
 226      }
 227  
 228      /**
 229       * Delete a field
 230       *
 231       * @param field_controller $field
 232       */
 233      public static function delete_field_configuration(field_controller $field) : bool {
 234          $event = field_deleted::create_from_object($field);
 235          get_file_storage()->delete_area_files($field->get_handler()->get_configuration_context()->id, 'core_customfield',
 236              'description', $field->get('id'));
 237          $result = $field->delete();
 238          $event->trigger();
 239          return $result;
 240      }
 241  
 242      /**
 243       * Returns an object for inplace editable
 244       *
 245       * @param category_controller $category category that needs to be moved
 246       * @param bool $editable
 247       * @return inplace_editable
 248       */
 249      public static function get_category_inplace_editable(category_controller $category, bool $editable = true) : inplace_editable {
 250          return new inplace_editable('core_customfield',
 251                                      'category',
 252                                      $category->get('id'),
 253                                      $editable,
 254                                      $category->get_formatted_name(),
 255                                      $category->get('name'),
 256                                      get_string('editcategoryname', 'core_customfield'),
 257                                      get_string('newvaluefor', 'core_form', $category->get_formatted_name())
 258          );
 259      }
 260  
 261      /**
 262       * Reorder categories, move given category before another category
 263       *
 264       * @param category_controller $category category that needs to be moved
 265       * @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
 266       */
 267      public static function move_category(category_controller $category, int $beforeid = 0) {
 268          global $DB;
 269          $records = $DB->get_records(category::TABLE, [
 270              'component' => $category->get('component'),
 271              'area' => $category->get('area'),
 272              'itemid' => $category->get('itemid')
 273          ], 'sortorder, id', '*');
 274  
 275          $id = $category->get('id');
 276          $categoriesids = array_values(array_diff(array_keys($records), [$id]));
 277          $idx = $beforeid ? array_search($beforeid, $categoriesids) : false;
 278          if ($idx === false) {
 279              // Set as the last category.
 280              $categoriesids = array_merge($categoriesids, [$id]);
 281          } else {
 282              // Set before category with id $beforeid.
 283              $categoriesids = array_merge(array_slice($categoriesids, 0, $idx), [$id], array_slice($categoriesids, $idx));
 284          }
 285  
 286          foreach (array_values($categoriesids) as $idx => $categoryid) {
 287              // Use persistent class to update the sortorder for each category that needs updating.
 288              if ($records[$categoryid]->sortorder != $idx) {
 289                  $c = ($categoryid == $id) ? $category : category_controller::create(0, $records[$categoryid]);
 290                  $c->set('sortorder', $idx);
 291                  $c->save();
 292              }
 293          }
 294      }
 295  
 296      /**
 297       * Insert or update custom field category
 298       *
 299       * @param category_controller $category
 300       */
 301      public static function save_category(category_controller $category) {
 302          $isnewcategory = empty($category->get('id'));
 303  
 304          $category->save();
 305  
 306          if ($isnewcategory) {
 307              // Move to the end.
 308              self::move_category($category);
 309              category_created::create_from_object($category)->trigger();
 310          } else {
 311              category_updated::create_from_object($category)->trigger();
 312          }
 313      }
 314  
 315      /**
 316       * Delete a custom field category
 317       *
 318       * @param category_controller $category
 319       * @return bool
 320       */
 321      public static function delete_category(category_controller $category) : bool {
 322          $event = category_deleted::create_from_object($category);
 323  
 324          // Delete all fields.
 325          foreach ($category->get_fields() as $field) {
 326              self::delete_field_configuration($field);
 327          }
 328  
 329          $result = $category->delete();
 330          $event->trigger();
 331          return $result;
 332      }
 333  
 334      /**
 335       * Returns a list of categories with their related fields.
 336       *
 337       * @param string $component
 338       * @param string $area
 339       * @param int $itemid
 340       * @return category_controller[]
 341       */
 342      public static function get_categories_with_fields(string $component, string $area, int $itemid) : array {
 343          global $DB;
 344  
 345          $categories = [];
 346  
 347          $options = [
 348                  'component' => $component,
 349                  'area'      => $area,
 350                  'itemid'    => $itemid
 351          ];
 352  
 353          $plugins = \core\plugininfo\customfield::get_enabled_plugins();
 354          list($sqlfields, $params) = $DB->get_in_or_equal(array_keys($plugins), SQL_PARAMS_NAMED, 'param', true, null);
 355  
 356          $fields = 'f.*, ' . join(', ', array_map(function($field) {
 357                  return "c.$field AS category_$field";
 358          }, array_diff(array_keys(category::properties_definition()), ['usermodified', 'timemodified'])));
 359          $sql = "SELECT $fields
 360                    FROM {customfield_category} c
 361               LEFT JOIN {customfield_field} f ON c.id = f.categoryid AND f.type $sqlfields
 362                   WHERE c.component = :component AND c.area = :area AND c.itemid = :itemid
 363                ORDER BY c.sortorder, f.sortorder";
 364          $fieldsdata = $DB->get_recordset_sql($sql, $options + $params);
 365  
 366          foreach ($fieldsdata as $data) {
 367              if (!array_key_exists($data->category_id, $categories)) {
 368                  $categoryobj = new \stdClass();
 369                  foreach ($data as $key => $value) {
 370                      if (preg_match('/^category_(.*)$/', $key, $matches)) {
 371                          $categoryobj->{$matches[1]} = $value;
 372                      }
 373                  }
 374                  $category = category_controller::create(0, $categoryobj);
 375                  $categories[$categoryobj->id] = $category;
 376              } else {
 377                  $category = $categories[$data->categoryid];
 378              }
 379              if ($data->id) {
 380                  $fieldobj = new \stdClass();
 381                  foreach ($data as $key => $value) {
 382                      if (!preg_match('/^category_/', $key)) {
 383                          $fieldobj->{$key} = $value;
 384                      }
 385                  }
 386                  $field = field_controller::create(0, $fieldobj, $category);
 387              }
 388          }
 389          $fieldsdata->close();
 390  
 391          return $categories;
 392      }
 393  
 394      /**
 395       * Prepares the object to pass to field configuration form set_data() method
 396       *
 397       * @param field_controller $field
 398       * @return \stdClass
 399       */
 400      public static function prepare_field_for_config_form(field_controller $field) : \stdClass {
 401          if ($field->get('id')) {
 402              $formdata = $field->to_record();
 403              $formdata->configdata = $field->get('configdata');
 404              // Preprocess the description.
 405              $textoptions = $field->get_handler()->get_description_text_options();
 406              file_prepare_standard_editor($formdata, 'description', $textoptions, $textoptions['context'], 'core_customfield',
 407                  'description', $formdata->id);
 408          } else {
 409              $formdata = (object)['categoryid' => $field->get('categoryid'), 'type' => $field->get('type'), 'configdata' => []];
 410          }
 411          // Allow field to do more preprocessing (usually for editor or filemanager elements).
 412          $field->prepare_for_config_form($formdata);
 413          return $formdata;
 414      }
 415  
 416      /**
 417       * Get a list of the course custom fields that support course grouping in
 418       * block_myoverview
 419       * @return array $shortname => $name
 420       */
 421      public static function get_fields_supporting_course_grouping() {
 422          global $DB;
 423          $sql = "
 424              SELECT f.*
 425                FROM {customfield_field} f
 426                JOIN {customfield_category} cat ON cat.id = f.categoryid
 427               WHERE cat.component = 'core_course' AND cat.area = 'course'
 428               ORDER BY f.name
 429          ";
 430          $ret = [];
 431          $fields = $DB->get_records_sql($sql);
 432          foreach ($fields as $field) {
 433              $inst = field_controller::create(0, $field);
 434              $isvisible = $inst->get_configdata_property('visibility') == \core_course\customfield\course_handler::VISIBLETOALL;
 435              // Only visible fields to everybody supporting course grouping will be displayed.
 436              if ($inst->supports_course_grouping() && $isvisible) {
 437                  $ret[$inst->get('shortname')] = $inst->get('name');
 438              }
 439          }
 440          return $ret;
 441      }
 442  }