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.
<?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\report;

use coding_exception;
use context;
use lang_string;
use core_reportbuilder\local\entities\base as entity_base;
use core_reportbuilder\local\filters\base as filter_base;
use core_reportbuilder\local\helpers\database;
use core_reportbuilder\local\helpers\user_filter_manager;
use core_reportbuilder\local\models\report;

/**
 * Base class for all reports
 *
 * @package     core_reportbuilder
 * @copyright   2020 Paul Holden <paulh@moodle.com>
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
abstract class base {

    /** @var int Custom report type value */
    public const TYPE_CUSTOM_REPORT = 0;

    /** @var int System report type value */
    public const TYPE_SYSTEM_REPORT = 1;

    /** @var int Default paging limit */
    public const DEFAULT_PAGESIZE = 30;

    /** @var report $report Report persistent */
    private $report;

    /** @var string $maintable */
    private $maintable = '';

    /** @var string $maintablealias */
    private $maintablealias = '';

    /** @var array $sqljoins */
    private $sqljoins = [];

    /** @var array $sqlwheres */
    private $sqlwheres = [];

    /** @var array $sqlparams */
    private $sqlparams = [];

    /** @var entity_base[] $entities */
    private $entities = [];

    /** @var lang_string[] */
    private $entitytitles = [];

    /** @var column[] $columns */
    private $columns = [];

    /** @var filter[] $conditions */
    private $conditions = [];

    /** @var filter[] $filters */
    private $filters = [];

    /** @var bool $downloadable Set if the report can be downloaded */
    private $downloadable = false;

    /** @var string $downloadfilename Name of the downloaded file */
    private $downloadfilename = '';

    /** @var int Default paging size */
    private $defaultperpage = self::DEFAULT_PAGESIZE;

> /** @var array $attributes */ /** > private $attributes = []; * Base report constructor > * > /** @var lang_string $noresultsnotice */ * @param report $report > private $noresultsnotice; */ >
public function __construct(report $report) { $this->report = $report;
> $this->noresultsnotice = new lang_string('nothingtodisplay');
// Initialise and validate the report. $this->initialise(); $this->validate(); } /** * Returns persistent class used when initialising this report * * @return report */ final public function get_report_persistent(): report { return $this->report; } /** * Initialise report. Specify which columns, filters, etc should be present * * To set the base query use: * - {@see set_main_table} * - {@see add_base_condition_simple} or {@see add_base_condition_sql} * - {@see add_join} * * To add content to the report use: * - {@see add_entity} * - {@see add_column} * - {@see add_filter} * - etc */ abstract protected function initialise(): void; /** * Get the report availability. Sub-classes should override this method to declare themselves unavailable, for example if * they require classes that aren't present due to missing plugin * * @return bool */ public static function is_available(): bool { return true; } /** * Perform some basic validation about expected class properties * * @throws coding_exception */ protected function validate(): void { if (empty($this->maintable)) { throw new coding_exception('Report must define main table by calling $this->set_main_table()'); } if (empty($this->columns)) { throw new coding_exception('Report must define at least one column by calling $this->add_column()'); } } /** * Set the main table and alias for the SQL query * * @param string $tablename * @param string $tablealias */ final public function set_main_table(string $tablename, string $tablealias = ''): void { $this->maintable = $tablename; $this->maintablealias = $tablealias; } /** * Get the main table name * * @return string */ final public function get_main_table(): string { return $this->maintable; } /** * Get the alias for the main table * * @return string */ final public function get_main_table_alias(): string { return $this->maintablealias; } /** * Adds report JOIN clause that is always added * * @param string $join * @param array $params * @param bool $validateparams Some queries might add non-standard params and validation could fail */ protected function add_join(string $join, array $params = [], bool $validateparams = true): void { if ($validateparams) { database::validate_params($params); } $this->sqljoins[trim($join)] = trim($join); $this->sqlparams += $params; } /** * Return report JOIN clauses * * @return array */ public function get_joins(): array { return array_values($this->sqljoins); } /** * Define simple "field = value" clause to apply to the report query * * @param string $fieldname * @param mixed $fieldvalue */ final public function add_base_condition_simple(string $fieldname, $fieldvalue): void { if ($fieldvalue === null) { $this->add_base_condition_sql("{$fieldname} IS NULL"); } else { $fieldvalueparam = database::generate_param_name(); $this->add_base_condition_sql("{$fieldname} = :{$fieldvalueparam}", [ $fieldvalueparam => $fieldvalue, ]); } } /**
< * Define more complex clause that will always be applied to the report query
> * Define more complex/non-empty clause to apply to the report query
* * @param string $where * @param array $params Note that the param names should be generated by {@see database::generate_param_name} */ final public function add_base_condition_sql(string $where, array $params = []): void {
> database::validate_params($params); > // Validate parameters always, so that potential errors are caught early.
> if ($where !== '') {
$this->sqlwheres[] = trim($where); $this->sqlparams = $params + $this->sqlparams; }
> }
/** * Return base select/params for the report query * * @return array [string $select, array $params] */ final public function get_base_condition(): array { return [ implode(' AND ', $this->sqlwheres), $this->sqlparams, ]; } /** * Adds given entity, along with it's columns and filters, to the report * * @param entity_base $entity */ final protected function add_entity(entity_base $entity): void { $entityname = $entity->get_entity_name(); $this->annotate_entity($entityname, $entity->get_entity_title()); $this->entities[$entityname] = $entity->initialise(); } /** * Returns the entity added to the report from the given entity name * * @param string $name * @return entity_base * @throws coding_exception */ final protected function get_entity(string $name): entity_base { if (!array_key_exists($name, $this->entities)) { throw new coding_exception('Invalid entity name', $name); } return $this->entities[$name]; } /** * Returns the list of all the entities added to the report * * @return entity_base[] */ final protected function get_entities(): array { return $this->entities; } /** * Define a new entity for the report * * @param string $name * @param lang_string $title * @throws coding_exception */ final protected function annotate_entity(string $name, lang_string $title): void {
< if (empty($name) || $name !== clean_param($name, PARAM_ALPHANUMEXT)) {
> if ($name === '' || $name !== clean_param($name, PARAM_ALPHANUMEXT)) {
throw new coding_exception('Entity name must be comprised of alphanumeric character, underscore or dash'); }
> if (array_key_exists($name, $this->entitytitles)) { $this->entitytitles[$name] = $title; > throw new coding_exception('Duplicate entity name', $name); } > } >
/** * Returns title of given report entity * * @param string $name * @return lang_string * @throws coding_exception */ final public function get_entity_title(string $name): lang_string { if (!array_key_exists($name, $this->entitytitles)) { throw new coding_exception('Invalid entity name', $name); } return $this->entitytitles[$name]; } /** * Adds a column to the report * * @param column $column * @return column * @throws coding_exception */ final protected function add_column(column $column): column { if (!array_key_exists($column->get_entity_name(), $this->entitytitles)) { throw new coding_exception('Invalid entity name', $column->get_entity_name()); } $name = $column->get_name(); if (empty($name) || $name !== clean_param($name, PARAM_ALPHANUMEXT)) { throw new coding_exception('Column name must be comprised of alphanumeric character, underscore or dash'); } $uniqueidentifier = $column->get_unique_identifier(); if (array_key_exists($uniqueidentifier, $this->columns)) { throw new coding_exception('Duplicate column identifier', $uniqueidentifier); } $this->columns[$uniqueidentifier] = $column; return $column; } /** * Add given column to the report from an entity * * The entity must have already been added to the report before calling this method * * @param string $uniqueidentifier * @return column */ final protected function add_column_from_entity(string $uniqueidentifier): column { [$entityname, $columnname] = explode(':', $uniqueidentifier, 2); return $this->add_column($this->get_entity($entityname)->get_column($columnname)); } /** * Add given columns to the report from one or more entities * * Each entity must have already been added to the report before calling this method * * @param string[] $columns Unique identifier of each entity column */ final protected function add_columns_from_entities(array $columns): void { foreach ($columns as $column) { $this->add_column_from_entity($column); } } /** * Return report column by unique identifier * * @param string $uniqueidentifier * @return column|null */ final public function get_column(string $uniqueidentifier): ?column { return $this->columns[$uniqueidentifier] ?? null; } /** * Return all available report columns * * @return column[] */ final public function get_columns(): array { return array_filter($this->columns, static function(column $column): bool { return $column->get_is_available(); }); } /** * Return all active report columns (by default, all available columns) * * @return column[] */ public function get_active_columns(): array {
< return $this->get_columns();
> $columns = $this->get_columns(); > foreach ($columns as $column) { > if ($column->get_is_deprecated()) { > debugging("The column '{$column->get_unique_identifier()}' is deprecated, please do not use it any more." . > " {$column->get_is_deprecated_message()}", DEBUG_DEVELOPER); > } > } > > return $columns;
} /** * Return all active report columns, keyed by their alias (only active columns in a report would have a valid alias/index) * * @return column[] */ final public function get_active_columns_by_alias(): array { $columns = []; foreach ($this->get_active_columns() as $column) { $columns[$column->get_column_alias()] = $column; } return $columns; } /** * Adds a condition to the report * * @param filter $condition * @return filter * @throws coding_exception */ final protected function add_condition(filter $condition): filter { if (!array_key_exists($condition->get_entity_name(), $this->entitytitles)) { throw new coding_exception('Invalid entity name', $condition->get_entity_name()); } $name = $condition->get_name(); if (empty($name) || $name !== clean_param($name, PARAM_ALPHANUMEXT)) { throw new coding_exception('Condition name must be comprised of alphanumeric character, underscore or dash'); } $uniqueidentifier = $condition->get_unique_identifier(); if (array_key_exists($uniqueidentifier, $this->conditions)) { throw new coding_exception('Duplicate condition identifier', $uniqueidentifier); } $this->conditions[$uniqueidentifier] = $condition; return $condition; } /** * Add given condition to the report from an entity * * The entity must have already been added to the report before calling this method * * @param string $uniqueidentifier * @return filter */ final protected function add_condition_from_entity(string $uniqueidentifier): filter { [$entityname, $conditionname] = explode(':', $uniqueidentifier, 2); return $this->add_condition($this->get_entity($entityname)->get_condition($conditionname)); } /** * Add given conditions to the report from one or more entities * * Each entity must have already been added to the report before calling this method * * @param string[] $conditions Unique identifier of each entity condition */ final protected function add_conditions_from_entities(array $conditions): void { foreach ($conditions as $condition) { $this->add_condition_from_entity($condition); } } /** * Return report condition by unique identifier * * @param string $uniqueidentifier * @return filter|null */ final public function get_condition(string $uniqueidentifier): ?filter { return $this->conditions[$uniqueidentifier] ?? null; } /** * Return all available report conditions * * @return filter[] */ final public function get_conditions(): array { return array_filter($this->conditions, static function(filter $condition): bool { return $condition->get_is_available(); }); } /** * Return all active report conditions (by default, all available conditions) * * @return filter[] */ public function get_active_conditions(): array {
< return $this->get_conditions();
> $conditions = $this->get_conditions(); > foreach ($conditions as $condition) { > if ($condition->get_is_deprecated()) { > debugging("The condition '{$condition->get_unique_identifier()}' is deprecated, please do not use it any more." . > " {$condition->get_is_deprecated_message()}", DEBUG_DEVELOPER); > } > } > > return $conditions;
} /** * Return all active report condition instances * * @return filter_base[] */ final public function get_condition_instances(): array { return array_map(static function(filter $condition): filter_base { /** @var filter_base $conditionclass */ $conditionclass = $condition->get_filter_class(); return $conditionclass::create($condition); }, $this->get_active_conditions()); } /** * Set the condition values of the report * * @param array $values * @return bool */ final public function set_condition_values(array $values): bool { $this->report->set('conditiondata', json_encode($values)) ->save(); return true; } /** * Get the condition values of the report * * @return array */ final public function get_condition_values(): array { $conditions = (string) $this->report->get('conditiondata'); return (array) json_decode($conditions); } /** * Set the settings values of the report * * @param array $values * @return bool */ final public function set_settings_values(array $values): bool { $currentsettings = $this->get_settings_values(); $settings = array_merge($currentsettings, $values); $this->report->set('settingsdata', json_encode($settings)) ->save(); return true; } /** * Get the settings values of the report * * @return array */ final public function get_settings_values(): array { $settings = (string) $this->report->get('settingsdata'); return (array) json_decode($settings); } /** * Adds a filter to the report * * @param filter $filter * @return filter * @throws coding_exception */ final protected function add_filter(filter $filter): filter { if (!array_key_exists($filter->get_entity_name(), $this->entitytitles)) { throw new coding_exception('Invalid entity name', $filter->get_entity_name()); } $name = $filter->get_name(); if (empty($name) || $name !== clean_param($name, PARAM_ALPHANUMEXT)) { throw new coding_exception('Filter name must be comprised of alphanumeric character, underscore or dash'); } $uniqueidentifier = $filter->get_unique_identifier(); if (array_key_exists($uniqueidentifier, $this->filters)) { throw new coding_exception('Duplicate filter identifier', $uniqueidentifier); } $this->filters[$uniqueidentifier] = $filter; return $filter; } /** * Add given filter to the report from an entity * * The entity must have already been added to the report before calling this method * * @param string $uniqueidentifier * @return filter */ final protected function add_filter_from_entity(string $uniqueidentifier): filter { [$entityname, $filtername] = explode(':', $uniqueidentifier, 2); return $this->add_filter($this->get_entity($entityname)->get_filter($filtername)); } /** * Add given filters to the report from one or more entities * * Each entity must have already been added to the report before calling this method * * @param string[] $filters Unique identifier of each entity filter */ final protected function add_filters_from_entities(array $filters): void { foreach ($filters as $filter) { $this->add_filter_from_entity($filter); } } /** * Return report filter by unique identifier * * @param string $uniqueidentifier * @return filter|null */ final public function get_filter(string $uniqueidentifier): ?filter { return $this->filters[$uniqueidentifier] ?? null; } /** * Return all available report filters * * @return filter[] */ final public function get_filters(): array { return array_filter($this->filters, static function(filter $filter): bool { return $filter->get_is_available(); }); } /** * Return all active report filters (by default, all available filters) * * @return filter[] */ public function get_active_filters(): array {
< return $this->get_filters();
> $filters = $this->get_filters(); > foreach ($filters as $filter) { > if ($filter->get_is_deprecated()) { > debugging("The filter '{$filter->get_unique_identifier()}' is deprecated, please do not use it any more." . > " {$filter->get_is_deprecated_message()}", DEBUG_DEVELOPER); > } > } > > return $filters;
} /** * Return all active report filter instances * * @return filter_base[] */ final public function get_filter_instances(): array { return array_map(static function(filter $filter): filter_base { /** @var filter_base $filterclass */ $filterclass = $filter->get_filter_class(); return $filterclass::create($filter); }, $this->get_active_filters()); } /** * Set the filter values of the report * * @param array $values * @return bool */ final public function set_filter_values(array $values): bool { return user_filter_manager::set($this->report->get('id'), $values); } /** * Get the filter values of the report * * @return array */ final public function get_filter_values(): array { return user_filter_manager::get($this->report->get('id')); } /** * Return the number of filter instances that are being applied based on the report's filter values (i.e. user has * configured them from their initial "Any value" state) * * @return int */ final public function get_applied_filter_count(): int { $values = $this->get_filter_values(); $applied = array_filter($this->get_filter_instances(), static function(filter_base $filter) use ($values): bool { return $filter->applies_to_values($values); }); return count($applied); } /** * Set if the report can be downloaded. * * @param bool $downloadable * @param string $downloadfilename If the report is downloadable, then a filename should be provided here */ final public function set_downloadable(bool $downloadable, string $downloadfilename = 'export'): void { $this->downloadable = $downloadable; $this->downloadfilename = $downloadfilename; } /** * Get if the report can be downloaded. * * @return bool */ final public function is_downloadable(): bool { return $this->downloadable; } /** * Return the downloadable report filename * * @return string */ final public function get_downloadfilename(): string { return $this->downloadfilename; } /** * Returns the report context * * @return context */ public function get_context(): context { return $this->report->get_context(); } /** * Set the default 'per page' size * * @param int $defaultperpage */ public function set_default_per_page(int $defaultperpage): void { $this->defaultperpage = $defaultperpage; } /**
> * Set the default lang string for the notice used when no results are found. * Default 'per page' size > * * > * @param lang_string|null $notice string, or null to tell the report to omit the notice entirely. * @return int > * @return void */ > */ public function get_default_per_page(): int { > public function set_default_no_results_notice(?lang_string $notice): void { return $this->defaultperpage; > $this->noresultsnotice = $notice; } > } } > > /** > * Get the default lang string for the notice used when no results are found. > * > * @return lang_string|null the lang_string instance or null if the report prefers not to use one. > */ > public function get_default_no_results_notice(): ?lang_string { > return $this->noresultsnotice; > } > > /**
> } > > /** > * Add report attributes (data-, class, etc.) that will be included in HTML when report is displayed > * > * @param array $attributes > * @return self > */ > public function add_attributes(array $attributes): self { > $this->attributes = $attributes + $this->attributes; > return $this; > } > > /** > * Returns the report HTML attributes > * > * @return array > */ > public function get_attributes(): array { > return $this->attributes;