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.
<?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/>.

declare(strict_types=1);

namespace core_reportbuilder\local\helpers;

use core_reportbuilder\local\filters\boolean_select;
use core_reportbuilder\local\filters\date;
use core_reportbuilder\local\filters\number;
use core_reportbuilder\local\filters\select;
use core_reportbuilder\local\filters\text;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
use lang_string;
use stdClass;
use core_customfield\data_controller;
use core_customfield\field_controller;
use core_customfield\handler;

/**
 * Helper class for course custom fields.
 *
 * @package   core_reportbuilder
 * @copyright 2021 Sara Arjona <sara@moodle.com> based on David Matamoros <davidmc@moodle.com> code.
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class custom_fields {

    /** @var string $entityname Name of the entity */
    private $entityname;

    /** @var handler $handler The handler for the customfields */
    private $handler;

    /** @var int $tablefieldalias The table alias and the field name (table.field) that matches the customfield instanceid. */
    private $tablefieldalias;

    /** @var array additional joins */
    private $joins = [];

    /**
     * Class customfields constructor.
     *
     * @param string $tablefieldalias table alias and the field name (table.field) that matches the customfield instanceid.
     * @param string $entityname name of the entity in the report where we add custom fields.
     * @param string $component component name of full frankenstyle plugin name.
     * @param string $area name of the area (each component/plugin may define handlers for multiple areas).
     * @param int $itemid item id if the area uses them (usually not used).
     */
    public function __construct(string $tablefieldalias, string $entityname, string $component, string $area, int $itemid = 0) {
        $this->tablefieldalias = $tablefieldalias;
        $this->entityname = $entityname;
        $this->handler = handler::get_handler($component, $area, $itemid);
    }

    /**
     * Additional join that is needed.
     *
     * @param string $join
     * @return self
     */
    public function add_join(string $join): self {
        $this->joins[trim($join)] = trim($join);
        return $this;
    }

    /**
     * Additional joins that are needed.
     *
     * @param array $joins
     * @return self
     */
    public function add_joins(array $joins): self {
        foreach ($joins as $join) {
            $this->add_join($join);
        }
        return $this;
    }

    /**
     * Return joins
     *
     * @return string[]
     */
    private function get_joins(): array {
        return array_values($this->joins);
    }

    /**
> * Get table alias for given custom field * Gets the custom fields columns for the report. > * * > * The entity name is used to ensure the alias differs when the entity is used multiple times within the same report, each * Column will be named as 'customfield_' + customfield shortname. > * having their own table alias/join * > * * @return column[] > * @param field_controller $field */ > * @return string public function get_columns(): array { > */ $columns = []; > private function get_table_alias(field_controller $field): string { > static $aliases = []; $categorieswithfields = $this->handler->get_categories_with_fields(); > foreach ($categorieswithfields as $fieldcategory) { > $aliaskey = "{$this->entityname}_{$field->get('id')}"; $categoryfields = $fieldcategory->get_fields(); > if (!array_key_exists($aliaskey, $aliases)) { foreach ($categoryfields as $field) { > $aliases[$aliaskey] = database::generate_alias(); $customdatatablealias = database::generate_alias(); > } > $datacontroller = data_controller::create(0, null, $field); > return $aliases[$aliaskey]; $datafield = $datacontroller->datafield(); > } > // Select enough fields to re-create and format each custom field instance value. > /** $selectfields = "{$customdatatablealias}.{$datafield}, {$customdatatablealias}.id, > * Get table join for given custom field {$customdatatablealias}.contextid"; > * if ($datafield === 'value') { > * @param field_controller $field // We will take the format into account when displaying the individual values. > * @return string $selectfields .= ", {$customdatatablealias}.valueformat"; > */ } > private function get_table_join(field_controller $field): string { > $customdatatablealias = $this->get_table_alias($field); $columntype = $this->get_column_type($field, $datafield); > > return "LEFT JOIN {customfield_data} {$customdatatablealias} $newcolumn = (new column( > ON {$customdatatablealias}.fieldid = {$field->get('id')} 'customfield_' . $field->get('shortname'), > AND {$customdatatablealias}.instanceid = {$this->tablefieldalias}"; new lang_string('customfieldcolumn', 'core_reportbuilder', $field->get_formatted_name()), > } $this->entityname > )) > /**
->add_joins($this->get_joins())
> global $DB; ->add_join("LEFT JOIN {customfield_data} {$customdatatablealias} " . >
< $customdatatablealias = database::generate_alias();
> $customdatatablealias = $this->get_table_alias($field);
"AND {$customdatatablealias}.instanceid = {$this->tablefieldalias}")
>
->add_fields($selectfields)
> $datafieldsql = "{$customdatatablealias}.{$datafield}"; ->set_type($columntype) > ->set_is_sortable($columntype !== column::TYPE_LONGTEXT) > // Long text fields should be cast for Oracle, for aggregation support. ->add_callback([$this, 'customfield_value'], $field) > $columntype = $this->get_column_type($field, $datafield); // Important. If the handler implements can_view() function, it will be called with parameter $instanceid=0. > if ($columntype === column::TYPE_LONGTEXT && $DB->get_dbfamily() === 'oracle') { // This means that per-instance access validation will be ignored. > $datafieldsql = $DB->sql_order_by_text($datafieldsql, 1024); ->set_is_available($this->handler->can_view($field, 0)); > }
< $selectfields = "{$customdatatablealias}.{$datafield}, {$customdatatablealias}.id, < {$customdatatablealias}.contextid";
> $selectfields = "{$customdatatablealias}.id, {$customdatatablealias}.contextid";
< $columntype = $this->get_column_type($field, $datafield); < < $newcolumn = (new column(
> $columns[] = (new column(
< ->add_join("LEFT JOIN {customfield_data} {$customdatatablealias} " . < "ON {$customdatatablealias}.fieldid = " . $field->get('id') . " " . < "AND {$customdatatablealias}.instanceid = {$this->tablefieldalias}")
> ->add_join($this->get_table_join($field)) > ->add_field($datafieldsql, $datafield)
< ->add_callback([$this, 'customfield_value'], $field)
> ->add_callback(static function($value, stdClass $row, field_controller $field): string { > return (string) data_controller::create(0, $row, $field)->export_value(); > }, $field)
< < $columns[] = $newcolumn;
* @param string $datafield * @return int */ private function get_column_type(field_controller $field, string $datafield): int { if ($field->get('type') === 'checkbox') { return column::TYPE_BOOLEAN; } if ($field->get('type') === 'date') { return column::TYPE_TIMESTAMP; } if ($datafield === 'intvalue') { return column::TYPE_INTEGER; } if ($datafield === 'decvalue') { return column::TYPE_FLOAT; } if ($datafield === 'value') { return column::TYPE_LONGTEXT; } return column::TYPE_TEXT; } /** * Returns all available filters on custom fields. * * Filter will be named as 'customfield_' + customfield shortname. * * @return filter[] */ public function get_filters(): array {
> global $DB; $filters = []; >
$categorieswithfields = $this->handler->get_categories_with_fields(); foreach ($categorieswithfields as $fieldcategory) { $categoryfields = $fieldcategory->get_fields(); foreach ($categoryfields as $field) {
< $customdatatablealias = database::generate_alias();
> $customdatatablealias = $this->get_table_alias($field);
$datacontroller = data_controller::create(0, null, $field);
>
$datafield = $datacontroller->datafield();
< $typeclass = $this->get_filter_class_type($datacontroller);
> $datafieldsql = "{$customdatatablealias}.{$datafield}"; > if ($datafield === 'value') { > $datafieldsql = $DB->sql_cast_to_char($datafieldsql); > }
> $typeclass = $this->get_filter_class_type($datacontroller);
$filter = (new filter( $typeclass, 'customfield_' . $field->get('shortname'), new lang_string('customfieldcolumn', 'core_reportbuilder', $field->get_formatted_name()), $this->entityname,
< "{$customdatatablealias}.{$datafield}"
> $datafieldsql
)) ->add_joins($this->get_joins())
< ->add_join("LEFT JOIN {customfield_data} {$customdatatablealias} " . < "ON {$customdatatablealias}.fieldid = " . $field->get('id') . " " . < "AND {$customdatatablealias}.instanceid = {$this->tablefieldalias}");
> ->add_join($this->get_table_join($field));
// Options are stored inside configdata json string and we need to convert it to array. if ($field->get('type') === 'select') { $filter->set_options_callback(static function() use ($field): array { $options = explode("\r\n", $field->get_configdata_property('options')); // Method set_options starts using array at index 1. we shift one position on this array. // In course settings this menu has an empty option and we need to respect that. array_unshift($options, " "); unset($options[0]); return $options; }); } $filters[] = $filter; } } return $filters; } /** * Returns class for the filter element that should be used for the field * * In some situation we can assume what kind of data is stored in the customfield plugin and we can * display appropriate filter form element. For all others assume text filter. * * @param data_controller $datacontroller * @return string */ private function get_filter_class_type(data_controller $datacontroller): string { $type = $datacontroller->get_field()->get('type'); switch ($type) { case 'checkbox': $classtype = boolean_select::class; break; case 'date': $classtype = date::class; break; case 'select': $classtype = select::class; break; default: // To support third party field type we need to account for stored numbers. $datafield = $datacontroller->datafield(); if ($datafield === 'intvalue' || $datafield === 'decvalue') { $classtype = number::class; } else { $classtype = text::class; } break; } return $classtype;
< } < < /** < * Format for custom fields value. We get the correct custom field value using export_value method. < * < * @param mixed $value Current value. < * @param stdClass $row Full row. < * @param field_controller $field Field controller object. < * @return mixed|null < */ < public function customfield_value($value, stdClass $row, field_controller $field) { < $data = data_controller::create(0, (object)$row, $field); < return $data->export_value();
} }