Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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       * Get table alias for given custom field
 105       *
 106       * The entity name is used to ensure the alias differs when the entity is used multiple times within the same report, each
 107       * having their own table alias/join
 108       *
 109       * @param field_controller $field
 110       * @return string
 111       */
 112      private function get_table_alias(field_controller $field): string {
 113          static $aliases = [];
 114  
 115          $aliaskey = "{$this->entityname}_{$field->get('id')}";
 116          if (!array_key_exists($aliaskey, $aliases)) {
 117              $aliases[$aliaskey] = database::generate_alias();
 118          }
 119  
 120          return $aliases[$aliaskey];
 121      }
 122  
 123      /**
 124       * Get table join for given custom field
 125       *
 126       * @param field_controller $field
 127       * @return string
 128       */
 129      private function get_table_join(field_controller $field): string {
 130          $customdatatablealias = $this->get_table_alias($field);
 131  
 132          return "LEFT JOIN {customfield_data} {$customdatatablealias}
 133                         ON {$customdatatablealias}.fieldid = {$field->get('id')}
 134                        AND {$customdatatablealias}.instanceid = {$this->tablefieldalias}";
 135      }
 136  
 137      /**
 138       * Gets the custom fields columns for the report.
 139       *
 140       * Column will be named as 'customfield_' + customfield shortname.
 141       *
 142       * @return column[]
 143       */
 144      public function get_columns(): array {
 145          global $DB;
 146  
 147          $columns = [];
 148  
 149          $categorieswithfields = $this->handler->get_categories_with_fields();
 150          foreach ($categorieswithfields as $fieldcategory) {
 151              $categoryfields = $fieldcategory->get_fields();
 152              foreach ($categoryfields as $field) {
 153                  $customdatatablealias = $this->get_table_alias($field);
 154  
 155                  $datacontroller = data_controller::create(0, null, $field);
 156  
 157                  $datafield = $datacontroller->datafield();
 158                  $datafieldsql = "{$customdatatablealias}.{$datafield}";
 159  
 160                  // Long text fields should be cast for Oracle, for aggregation support.
 161                  $columntype = $this->get_column_type($field, $datafield);
 162                  if ($columntype === column::TYPE_LONGTEXT && $DB->get_dbfamily() === 'oracle') {
 163                      $datafieldsql = $DB->sql_order_by_text($datafieldsql, 1024);
 164                  }
 165  
 166                  // Select enough fields to re-create and format each custom field instance value.
 167                  $selectfields = "{$customdatatablealias}.id, {$customdatatablealias}.contextid";
 168                  if ($datafield === 'value') {
 169                      // We will take the format into account when displaying the individual values.
 170                      $selectfields .= ", {$customdatatablealias}.valueformat";
 171                  }
 172  
 173                  $columns[] = (new column(
 174                      'customfield_' . $field->get('shortname'),
 175                      new lang_string('customfieldcolumn', 'core_reportbuilder', $field->get_formatted_name()),
 176                      $this->entityname
 177                  ))
 178                      ->add_joins($this->get_joins())
 179                      ->add_join($this->get_table_join($field))
 180                      ->add_field($datafieldsql, $datafield)
 181                      ->add_fields($selectfields)
 182                      ->set_type($columntype)
 183                      ->set_is_sortable($columntype !== column::TYPE_LONGTEXT)
 184                      ->add_callback(static function($value, stdClass $row, field_controller $field): string {
 185                          return (string) data_controller::create(0, $row, $field)->export_value();
 186                      }, $field)
 187                      // Important. If the handler implements can_view() function, it will be called with parameter $instanceid=0.
 188                      // This means that per-instance access validation will be ignored.
 189                      ->set_is_available($this->handler->can_view($field, 0));
 190              }
 191          }
 192          return $columns;
 193      }
 194  
 195      /**
 196       * Returns the column type
 197       *
 198       * @param field_controller $field
 199       * @param string $datafield
 200       * @return int
 201       */
 202      private function get_column_type(field_controller $field, string $datafield): int {
 203          if ($field->get('type') === 'checkbox') {
 204              return column::TYPE_BOOLEAN;
 205          }
 206  
 207          if ($field->get('type') === 'date') {
 208              return column::TYPE_TIMESTAMP;
 209          }
 210  
 211          if ($datafield === 'intvalue') {
 212              return column::TYPE_INTEGER;
 213          }
 214  
 215          if ($datafield === 'decvalue') {
 216              return column::TYPE_FLOAT;
 217          }
 218  
 219          if ($datafield === 'value') {
 220              return column::TYPE_LONGTEXT;
 221          }
 222  
 223          return column::TYPE_TEXT;
 224      }
 225  
 226      /**
 227       * Returns all available filters on custom fields.
 228       *
 229       * Filter will be named as 'customfield_' + customfield shortname.
 230       *
 231       * @return filter[]
 232       */
 233      public function get_filters(): array {
 234          global $DB;
 235  
 236          $filters = [];
 237  
 238          $categorieswithfields = $this->handler->get_categories_with_fields();
 239          foreach ($categorieswithfields as $fieldcategory) {
 240              $categoryfields = $fieldcategory->get_fields();
 241              foreach ($categoryfields as $field) {
 242                  $customdatatablealias = $this->get_table_alias($field);
 243  
 244                  $datacontroller = data_controller::create(0, null, $field);
 245  
 246                  $datafield = $datacontroller->datafield();
 247                  $datafieldsql = "{$customdatatablealias}.{$datafield}";
 248                  if ($datafield === 'value') {
 249                      $datafieldsql = $DB->sql_cast_to_char($datafieldsql);
 250                  }
 251  
 252                  $typeclass = $this->get_filter_class_type($datacontroller);
 253                  $filter = (new filter(
 254                      $typeclass,
 255                      'customfield_' . $field->get('shortname'),
 256                      new lang_string('customfieldcolumn', 'core_reportbuilder', $field->get_formatted_name()),
 257                      $this->entityname,
 258                      $datafieldsql
 259                  ))
 260                      ->add_joins($this->get_joins())
 261                      ->add_join($this->get_table_join($field));
 262  
 263                  // Options are stored inside configdata json string and we need to convert it to array.
 264                  if ($field->get('type') === 'select') {
 265                      $filter->set_options_callback(static function() use ($field): array {
 266                          $options = explode("\r\n", $field->get_configdata_property('options'));
 267                          // Method set_options starts using array at index 1. we shift one position on this array.
 268                          // In course settings this menu has an empty option and we need to respect that.
 269                          array_unshift($options, " ");
 270                          unset($options[0]);
 271                          return $options;
 272                      });
 273                  }
 274  
 275                  $filters[] = $filter;
 276              }
 277          }
 278          return $filters;
 279      }
 280  
 281      /**
 282       * Returns class for the filter element that should be used for the field
 283       *
 284       * In some situation we can assume what kind of data is stored in the customfield plugin and we can
 285       * display appropriate filter form element. For all others assume text filter.
 286       *
 287       * @param data_controller $datacontroller
 288       * @return string
 289       */
 290      private function get_filter_class_type(data_controller $datacontroller): string {
 291          $type = $datacontroller->get_field()->get('type');
 292  
 293          switch ($type) {
 294              case 'checkbox':
 295                  $classtype = boolean_select::class;
 296                  break;
 297              case 'date':
 298                  $classtype = date::class;
 299                  break;
 300              case 'select':
 301                  $classtype = select::class;
 302                  break;
 303              default:
 304                  // To support third party field type we need to account for stored numbers.
 305                  $datafield = $datacontroller->datafield();
 306                  if ($datafield === 'intvalue' || $datafield === 'decvalue') {
 307                      $classtype = number::class;
 308                  } else {
 309                      $classtype = text::class;
 310                  }
 311                  break;
 312          }
 313  
 314          return $classtype;
 315      }
 316  }