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 400 and 401] [Versions 401 and 403]

   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  declare(strict_types=1);
  18  
  19  namespace core_reportbuilder\local\helpers;
  20  
  21  use core_reportbuilder\local\filters\boolean_select;
  22  use core_reportbuilder\local\filters\date;
  23  use core_reportbuilder\local\filters\number;
  24  use core_reportbuilder\local\filters\select;
  25  use core_reportbuilder\local\filters\text;
  26  use core_reportbuilder\local\report\column;
  27  use core_reportbuilder\local\report\filter;
  28  use lang_string;
  29  use stdClass;
  30  use core_customfield\data_controller;
  31  use core_customfield\field_controller;
  32  use core_customfield\handler;
  33  
  34  /**
  35   * Helper class for course custom fields.
  36   *
  37   * @package   core_reportbuilder
  38   * @copyright 2021 Sara Arjona <sara@moodle.com> based on David Matamoros <davidmc@moodle.com> code.
  39   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  40   */
  41  class custom_fields {
  42  
  43      /** @var string $entityname Name of the entity */
  44      private $entityname;
  45  
  46      /** @var handler $handler The handler for the customfields */
  47      private $handler;
  48  
  49      /** @var int $tablefieldalias The table alias and the field name (table.field) that matches the customfield instanceid. */
  50      private $tablefieldalias;
  51  
  52      /** @var array additional joins */
  53      private $joins = [];
  54  
  55      /**
  56       * Class customfields constructor.
  57       *
  58       * @param string $tablefieldalias table alias and the field name (table.field) that matches the customfield instanceid.
  59       * @param string $entityname name of the entity in the report where we add custom fields.
  60       * @param string $component component name of full frankenstyle plugin name.
  61       * @param string $area name of the area (each component/plugin may define handlers for multiple areas).
  62       * @param int $itemid item id if the area uses them (usually not used).
  63       */
  64      public function __construct(string $tablefieldalias, string $entityname, string $component, string $area, int $itemid = 0) {
  65          $this->tablefieldalias = $tablefieldalias;
  66          $this->entityname = $entityname;
  67          $this->handler = handler::get_handler($component, $area, $itemid);
  68      }
  69  
  70      /**
  71       * Additional join that is needed.
  72       *
  73       * @param string $join
  74       * @return self
  75       */
  76      public function add_join(string $join): self {
  77          $this->joins[trim($join)] = trim($join);
  78          return $this;
  79      }
  80  
  81      /**
  82       * Additional joins that are needed.
  83       *
  84       * @param array $joins
  85       * @return self
  86       */
  87      public function add_joins(array $joins): self {
  88          foreach ($joins as $join) {
  89              $this->add_join($join);
  90          }
  91          return $this;
  92      }
  93  
  94      /**
  95       * Return joins
  96       *
  97       * @return string[]
  98       */
  99      private function get_joins(): array {
 100          return array_values($this->joins);
 101      }
 102  
 103      /**
 104       * Gets the custom fields columns for the report.
 105       *
 106       * Column will be named as 'customfield_' + customfield shortname.
 107       *
 108       * @return column[]
 109       */
 110      public function get_columns(): array {
 111          global $DB;
 112  
 113          $columns = [];
 114  
 115          $categorieswithfields = $this->handler->get_categories_with_fields();
 116          foreach ($categorieswithfields as $fieldcategory) {
 117              $categoryfields = $fieldcategory->get_fields();
 118              foreach ($categoryfields as $field) {
 119                  $customdatatablealias = database::generate_alias();
 120  
 121                  $datacontroller = data_controller::create(0, null, $field);
 122  
 123                  $datafield = $datacontroller->datafield();
 124                  $datafieldsql = "{$customdatatablealias}.{$datafield}";
 125  
 126                  // Long text fields should be cast for Oracle, for aggregation support.
 127                  $columntype = $this->get_column_type($field, $datafield);
 128                  if ($columntype === column::TYPE_LONGTEXT && $DB->get_dbfamily() === 'oracle') {
 129                      $datafieldsql = $DB->sql_order_by_text($datafieldsql, 1024);
 130                  }
 131  
 132                  // Select enough fields to re-create and format each custom field instance value.
 133                  $selectfields = "{$customdatatablealias}.id, {$customdatatablealias}.contextid";
 134                  if ($datafield === 'value') {
 135                      // We will take the format into account when displaying the individual values.
 136                      $selectfields .= ", {$customdatatablealias}.valueformat";
 137                  }
 138  
 139                  $columns[] = (new column(
 140                      'customfield_' . $field->get('shortname'),
 141                      new lang_string('customfieldcolumn', 'core_reportbuilder', $field->get_formatted_name()),
 142                      $this->entityname
 143                  ))
 144                      ->add_joins($this->get_joins())
 145                      ->add_join("LEFT JOIN {customfield_data} {$customdatatablealias} " .
 146                          "ON {$customdatatablealias}.fieldid = " . $field->get('id') . " " .
 147                          "AND {$customdatatablealias}.instanceid = {$this->tablefieldalias}")
 148                      ->add_field($datafieldsql, $datafield)
 149                      ->add_fields($selectfields)
 150                      ->set_type($columntype)
 151                      ->set_is_sortable($columntype !== column::TYPE_LONGTEXT)
 152                      ->add_callback(static function($value, stdClass $row, field_controller $field): string {
 153                          return (string) data_controller::create(0, $row, $field)->export_value();
 154                      }, $field)
 155                      // Important. If the handler implements can_view() function, it will be called with parameter $instanceid=0.
 156                      // This means that per-instance access validation will be ignored.
 157                      ->set_is_available($this->handler->can_view($field, 0));
 158              }
 159          }
 160          return $columns;
 161      }
 162  
 163      /**
 164       * Returns the column type
 165       *
 166       * @param field_controller $field
 167       * @param string $datafield
 168       * @return int
 169       */
 170      private function get_column_type(field_controller $field, string $datafield): int {
 171          if ($field->get('type') === 'checkbox') {
 172              return column::TYPE_BOOLEAN;
 173          }
 174  
 175          if ($field->get('type') === 'date') {
 176              return column::TYPE_TIMESTAMP;
 177          }
 178  
 179          if ($datafield === 'intvalue') {
 180              return column::TYPE_INTEGER;
 181          }
 182  
 183          if ($datafield === 'decvalue') {
 184              return column::TYPE_FLOAT;
 185          }
 186  
 187          if ($datafield === 'value') {
 188              return column::TYPE_LONGTEXT;
 189          }
 190  
 191          return column::TYPE_TEXT;
 192      }
 193  
 194      /**
 195       * Returns all available filters on custom fields.
 196       *
 197       * Filter will be named as 'customfield_' + customfield shortname.
 198       *
 199       * @return filter[]
 200       */
 201      public function get_filters(): array {
 202          global $DB;
 203  
 204          $filters = [];
 205  
 206          $categorieswithfields = $this->handler->get_categories_with_fields();
 207          foreach ($categorieswithfields as $fieldcategory) {
 208              $categoryfields = $fieldcategory->get_fields();
 209              foreach ($categoryfields as $field) {
 210                  $customdatatablealias = database::generate_alias();
 211  
 212                  $datacontroller = data_controller::create(0, null, $field);
 213  
 214                  $datafield = $datacontroller->datafield();
 215                  $datafieldsql = "{$customdatatablealias}.{$datafield}";
 216                  if ($datafield === 'value') {
 217                      $datafieldsql = $DB->sql_cast_to_char($datafieldsql);
 218                  }
 219  
 220                  $typeclass = $this->get_filter_class_type($datacontroller);
 221                  $filter = (new filter(
 222                      $typeclass,
 223                      'customfield_' . $field->get('shortname'),
 224                      new lang_string('customfieldcolumn', 'core_reportbuilder', $field->get_formatted_name()),
 225                      $this->entityname,
 226                      $datafieldsql
 227                  ))
 228                      ->add_joins($this->get_joins())
 229                      ->add_join("LEFT JOIN {customfield_data} {$customdatatablealias} " .
 230                          "ON {$customdatatablealias}.fieldid = " . $field->get('id') . " " .
 231                          "AND {$customdatatablealias}.instanceid = {$this->tablefieldalias}");
 232  
 233                  // Options are stored inside configdata json string and we need to convert it to array.
 234                  if ($field->get('type') === 'select') {
 235                      $filter->set_options_callback(static function() use ($field): array {
 236                          $options = explode("\r\n", $field->get_configdata_property('options'));
 237                          // Method set_options starts using array at index 1. we shift one position on this array.
 238                          // In course settings this menu has an empty option and we need to respect that.
 239                          array_unshift($options, " ");
 240                          unset($options[0]);
 241                          return $options;
 242                      });
 243                  }
 244  
 245                  $filters[] = $filter;
 246              }
 247          }
 248          return $filters;
 249      }
 250  
 251      /**
 252       * Returns class for the filter element that should be used for the field
 253       *
 254       * In some situation we can assume what kind of data is stored in the customfield plugin and we can
 255       * display appropriate filter form element. For all others assume text filter.
 256       *
 257       * @param data_controller $datacontroller
 258       * @return string
 259       */
 260      private function get_filter_class_type(data_controller $datacontroller): string {
 261          $type = $datacontroller->get_field()->get('type');
 262  
 263          switch ($type) {
 264              case 'checkbox':
 265                  $classtype = boolean_select::class;
 266                  break;
 267              case 'date':
 268                  $classtype = date::class;
 269                  break;
 270              case 'select':
 271                  $classtype = select::class;
 272                  break;
 273              default:
 274                  // To support third party field type we need to account for stored numbers.
 275                  $datafield = $datacontroller->datafield();
 276                  if ($datafield === 'intvalue' || $datafield === 'decvalue') {
 277                      $classtype = number::class;
 278                  } else {
 279                      $classtype = text::class;
 280                  }
 281                  break;
 282          }
 283  
 284          return $classtype;
 285      }
 286  }