Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 400 and 402] [Versions 401 and 402] [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\report;
  20  
  21  use coding_exception;
  22  use context;
  23  use lang_string;
  24  use core_reportbuilder\local\entities\base as entity_base;
  25  use core_reportbuilder\local\filters\base as filter_base;
  26  use core_reportbuilder\local\helpers\database;
  27  use core_reportbuilder\local\helpers\user_filter_manager;
  28  use core_reportbuilder\local\models\report;
  29  
  30  /**
  31   * Base class for all reports
  32   *
  33   * @package     core_reportbuilder
  34   * @copyright   2020 Paul Holden <paulh@moodle.com>
  35   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  abstract class base {
  38  
  39      /** @var int Custom report type value */
  40      public const TYPE_CUSTOM_REPORT = 0;
  41  
  42      /** @var int System report type value */
  43      public const TYPE_SYSTEM_REPORT = 1;
  44  
  45      /** @var int Default paging limit */
  46      public const DEFAULT_PAGESIZE = 30;
  47  
  48      /** @var report $report Report persistent */
  49      private $report;
  50  
  51      /** @var string $maintable */
  52      private $maintable = '';
  53  
  54      /** @var string $maintablealias */
  55      private $maintablealias = '';
  56  
  57      /** @var array $sqljoins */
  58      private $sqljoins = [];
  59  
  60      /** @var array $sqlwheres */
  61      private $sqlwheres = [];
  62  
  63      /** @var array $sqlparams */
  64      private $sqlparams = [];
  65  
  66      /** @var entity_base[] $entities */
  67      private $entities = [];
  68  
  69      /** @var lang_string[] */
  70      private $entitytitles = [];
  71  
  72      /** @var column[] $columns */
  73      private $columns = [];
  74  
  75      /** @var filter[] $conditions */
  76      private $conditions = [];
  77  
  78      /** @var filter[] $filters */
  79      private $filters = [];
  80  
  81      /** @var bool $downloadable Set if the report can be downloaded */
  82      private $downloadable = false;
  83  
  84      /** @var string $downloadfilename Name of the downloaded file */
  85      private $downloadfilename = '';
  86  
  87      /** @var int Default paging size */
  88      private $defaultperpage = self::DEFAULT_PAGESIZE;
  89  
  90      /** @var array $attributes */
  91      private $attributes = [];
  92  
  93      /**
  94       * Base report constructor
  95       *
  96       * @param report $report
  97       */
  98      public function __construct(report $report) {
  99          $this->report = $report;
 100  
 101          // Initialise and validate the report.
 102          $this->initialise();
 103          $this->validate();
 104      }
 105  
 106      /**
 107       * Returns persistent class used when initialising this report
 108       *
 109       * @return report
 110       */
 111      final public function get_report_persistent(): report {
 112          return $this->report;
 113      }
 114  
 115      /**
 116       * Initialise report. Specify which columns, filters, etc should be present
 117       *
 118       * To set the base query use:
 119       * - {@see set_main_table}
 120       * - {@see add_base_condition_simple} or {@see add_base_condition_sql}
 121       * - {@see add_join}
 122       *
 123       * To add content to the report use:
 124       * - {@see add_entity}
 125       * - {@see add_column}
 126       * - {@see add_filter}
 127       * - etc
 128       */
 129      abstract protected function initialise(): void;
 130  
 131      /**
 132       * Get the report availability. Sub-classes should override this method to declare themselves unavailable, for example if
 133       * they require classes that aren't present due to missing plugin
 134       *
 135       * @return bool
 136       */
 137      public static function is_available(): bool {
 138          return true;
 139      }
 140  
 141      /**
 142       * Perform some basic validation about expected class properties
 143       *
 144       * @throws coding_exception
 145       */
 146      protected function validate(): void {
 147          if (empty($this->maintable)) {
 148              throw new coding_exception('Report must define main table by calling $this->set_main_table()');
 149          }
 150  
 151          if (empty($this->columns)) {
 152              throw new coding_exception('Report must define at least one column by calling $this->add_column()');
 153          }
 154      }
 155  
 156      /**
 157       * Set the main table and alias for the SQL query
 158       *
 159       * @param string $tablename
 160       * @param string $tablealias
 161       */
 162      final public function set_main_table(string $tablename, string $tablealias = ''): void {
 163          $this->maintable = $tablename;
 164          $this->maintablealias = $tablealias;
 165      }
 166  
 167      /**
 168       * Get the main table name
 169       *
 170       * @return string
 171       */
 172      final public function get_main_table(): string {
 173          return $this->maintable;
 174      }
 175  
 176      /**
 177       * Get the alias for the main table
 178       *
 179       * @return string
 180       */
 181      final public function get_main_table_alias(): string {
 182          return $this->maintablealias;
 183      }
 184  
 185      /**
 186       * Adds report JOIN clause that is always added
 187       *
 188       * @param string $join
 189       * @param array $params
 190       * @param bool $validateparams Some queries might add non-standard params and validation could fail
 191       */
 192      protected function add_join(string $join, array $params = [], bool $validateparams = true): void {
 193          if ($validateparams) {
 194              database::validate_params($params);
 195          }
 196  
 197          $this->sqljoins[trim($join)] = trim($join);
 198          $this->sqlparams += $params;
 199      }
 200  
 201      /**
 202       * Return report JOIN clauses
 203       *
 204       * @return array
 205       */
 206      public function get_joins(): array {
 207          return array_values($this->sqljoins);
 208      }
 209  
 210      /**
 211       * Define simple "field = value" clause to apply to the report query
 212       *
 213       * @param string $fieldname
 214       * @param mixed $fieldvalue
 215       */
 216      final public function add_base_condition_simple(string $fieldname, $fieldvalue): void {
 217          if ($fieldvalue === null) {
 218              $this->add_base_condition_sql("{$fieldname} IS NULL");
 219          } else {
 220              $fieldvalueparam = database::generate_param_name();
 221              $this->add_base_condition_sql("{$fieldname} = :{$fieldvalueparam}", [
 222                  $fieldvalueparam => $fieldvalue,
 223              ]);
 224          }
 225      }
 226  
 227      /**
 228       * Define more complex clause that will always be applied to the report query
 229       *
 230       * @param string $where
 231       * @param array $params Note that the param names should be generated by {@see database::generate_param_name}
 232       */
 233      final public function add_base_condition_sql(string $where, array $params = []): void {
 234          database::validate_params($params);
 235  
 236          $this->sqlwheres[] = trim($where);
 237          $this->sqlparams = $params + $this->sqlparams;
 238      }
 239  
 240      /**
 241       * Return base select/params for the report query
 242       *
 243       * @return array [string $select, array $params]
 244       */
 245      final public function get_base_condition(): array {
 246          return [
 247              implode(' AND ', $this->sqlwheres),
 248              $this->sqlparams,
 249          ];
 250      }
 251  
 252      /**
 253       * Adds given entity, along with it's columns and filters, to the report
 254       *
 255       * @param entity_base $entity
 256       */
 257      final protected function add_entity(entity_base $entity): void {
 258          $entityname = $entity->get_entity_name();
 259          $this->annotate_entity($entityname, $entity->get_entity_title());
 260          $this->entities[$entityname] = $entity->initialise();
 261      }
 262  
 263      /**
 264       * Returns the entity added to the report from the given entity name
 265       *
 266       * @param string $name
 267       * @return entity_base
 268       * @throws coding_exception
 269       */
 270      final protected function get_entity(string $name): entity_base {
 271          if (!array_key_exists($name, $this->entities)) {
 272              throw new coding_exception('Invalid entity name', $name);
 273          }
 274  
 275          return $this->entities[$name];
 276      }
 277  
 278      /**
 279       * Returns the list of all the entities added to the report
 280       *
 281       * @return entity_base[]
 282       */
 283      final protected function get_entities(): array {
 284          return $this->entities;
 285      }
 286  
 287      /**
 288       * Define a new entity for the report
 289       *
 290       * @param string $name
 291       * @param lang_string $title
 292       * @throws coding_exception
 293       */
 294      final protected function annotate_entity(string $name, lang_string $title): void {
 295          if (empty($name) || $name !== clean_param($name, PARAM_ALPHANUMEXT)) {
 296              throw new coding_exception('Entity name must be comprised of alphanumeric character, underscore or dash');
 297          }
 298  
 299          $this->entitytitles[$name] = $title;
 300      }
 301  
 302      /**
 303       * Returns title of given report entity
 304       *
 305       * @param string $name
 306       * @return lang_string
 307       * @throws coding_exception
 308       */
 309      final public function get_entity_title(string $name): lang_string {
 310          if (!array_key_exists($name, $this->entitytitles)) {
 311              throw new coding_exception('Invalid entity name', $name);
 312          }
 313  
 314          return $this->entitytitles[$name];
 315      }
 316  
 317      /**
 318       * Adds a column to the report
 319       *
 320       * @param column $column
 321       * @return column
 322       * @throws coding_exception
 323       */
 324      final protected function add_column(column $column): column {
 325          if (!array_key_exists($column->get_entity_name(), $this->entitytitles)) {
 326              throw new coding_exception('Invalid entity name', $column->get_entity_name());
 327          }
 328  
 329          $name = $column->get_name();
 330          if (empty($name) || $name !== clean_param($name, PARAM_ALPHANUMEXT)) {
 331              throw new coding_exception('Column name must be comprised of alphanumeric character, underscore or dash');
 332          }
 333  
 334          $uniqueidentifier = $column->get_unique_identifier();
 335          if (array_key_exists($uniqueidentifier, $this->columns)) {
 336              throw new coding_exception('Duplicate column identifier', $uniqueidentifier);
 337          }
 338  
 339          $this->columns[$uniqueidentifier] = $column;
 340  
 341          return $column;
 342      }
 343  
 344      /**
 345       * Add given column to the report from an entity
 346       *
 347       * The entity must have already been added to the report before calling this method
 348       *
 349       * @param string $uniqueidentifier
 350       * @return column
 351       */
 352      final protected function add_column_from_entity(string $uniqueidentifier): column {
 353          [$entityname, $columnname] = explode(':', $uniqueidentifier, 2);
 354  
 355          return $this->add_column($this->get_entity($entityname)->get_column($columnname));
 356      }
 357  
 358      /**
 359       * Add given columns to the report from one or more entities
 360       *
 361       * Each entity must have already been added to the report before calling this method
 362       *
 363       * @param string[] $columns Unique identifier of each entity column
 364       */
 365      final protected function add_columns_from_entities(array $columns): void {
 366          foreach ($columns as $column) {
 367              $this->add_column_from_entity($column);
 368          }
 369      }
 370  
 371      /**
 372       * Return report column by unique identifier
 373       *
 374       * @param string $uniqueidentifier
 375       * @return column|null
 376       */
 377      final public function get_column(string $uniqueidentifier): ?column {
 378          return $this->columns[$uniqueidentifier] ?? null;
 379      }
 380  
 381      /**
 382       * Return all available report columns
 383       *
 384       * @return column[]
 385       */
 386      final public function get_columns(): array {
 387          return array_filter($this->columns, static function(column $column): bool {
 388              return $column->get_is_available();
 389          });
 390      }
 391  
 392      /**
 393       * Return all active report columns (by default, all available columns)
 394       *
 395       * @return column[]
 396       */
 397      public function get_active_columns(): array {
 398          return $this->get_columns();
 399      }
 400  
 401      /**
 402       * Return all active report columns, keyed by their alias (only active columns in a report would have a valid alias/index)
 403       *
 404       * @return column[]
 405       */
 406      final public function get_active_columns_by_alias(): array {
 407          $columns = [];
 408  
 409          foreach ($this->get_active_columns() as $column) {
 410              $columns[$column->get_column_alias()] = $column;
 411          }
 412  
 413          return $columns;
 414      }
 415  
 416      /**
 417       * Adds a condition to the report
 418       *
 419       * @param filter $condition
 420       * @return filter
 421       * @throws coding_exception
 422       */
 423      final protected function add_condition(filter $condition): filter {
 424          if (!array_key_exists($condition->get_entity_name(), $this->entitytitles)) {
 425              throw new coding_exception('Invalid entity name', $condition->get_entity_name());
 426          }
 427  
 428          $name = $condition->get_name();
 429          if (empty($name) || $name !== clean_param($name, PARAM_ALPHANUMEXT)) {
 430              throw new coding_exception('Condition name must be comprised of alphanumeric character, underscore or dash');
 431          }
 432  
 433          $uniqueidentifier = $condition->get_unique_identifier();
 434          if (array_key_exists($uniqueidentifier, $this->conditions)) {
 435              throw new coding_exception('Duplicate condition identifier', $uniqueidentifier);
 436          }
 437  
 438          $this->conditions[$uniqueidentifier] = $condition;
 439  
 440          return $condition;
 441      }
 442  
 443      /**
 444       * Add given condition to the report from an entity
 445       *
 446       * The entity must have already been added to the report before calling this method
 447       *
 448       * @param string $uniqueidentifier
 449       * @return filter
 450       */
 451      final protected function add_condition_from_entity(string $uniqueidentifier): filter {
 452          [$entityname, $conditionname] = explode(':', $uniqueidentifier, 2);
 453  
 454          return $this->add_condition($this->get_entity($entityname)->get_condition($conditionname));
 455      }
 456  
 457      /**
 458       * Add given conditions to the report from one or more entities
 459       *
 460       * Each entity must have already been added to the report before calling this method
 461       *
 462       * @param string[] $conditions Unique identifier of each entity condition
 463       */
 464      final protected function add_conditions_from_entities(array $conditions): void {
 465          foreach ($conditions as $condition) {
 466              $this->add_condition_from_entity($condition);
 467          }
 468      }
 469  
 470      /**
 471       * Return report condition by unique identifier
 472       *
 473       * @param string $uniqueidentifier
 474       * @return filter|null
 475       */
 476      final public function get_condition(string $uniqueidentifier): ?filter {
 477          return $this->conditions[$uniqueidentifier] ?? null;
 478      }
 479  
 480      /**
 481       * Return all available report conditions
 482       *
 483       * @return filter[]
 484       */
 485      final public function get_conditions(): array {
 486          return array_filter($this->conditions, static function(filter $condition): bool {
 487              return $condition->get_is_available();
 488          });
 489      }
 490  
 491      /**
 492       * Return all active report conditions (by default, all available conditions)
 493       *
 494       * @return filter[]
 495       */
 496      public function get_active_conditions(): array {
 497          return $this->get_conditions();
 498      }
 499  
 500      /**
 501       * Return all active report condition instances
 502       *
 503       * @return filter_base[]
 504       */
 505      final public function get_condition_instances(): array {
 506          return array_map(static function(filter $condition): filter_base {
 507              /** @var filter_base $conditionclass */
 508              $conditionclass = $condition->get_filter_class();
 509  
 510              return $conditionclass::create($condition);
 511          }, $this->get_active_conditions());
 512      }
 513  
 514      /**
 515       * Set the condition values of the report
 516       *
 517       * @param array $values
 518       * @return bool
 519       */
 520      final public function set_condition_values(array $values): bool {
 521          $this->report->set('conditiondata', json_encode($values))
 522              ->save();
 523  
 524          return true;
 525      }
 526  
 527      /**
 528       * Get the condition values of the report
 529       *
 530       * @return array
 531       */
 532      final public function get_condition_values(): array {
 533          $conditions = (string) $this->report->get('conditiondata');
 534  
 535          return (array) json_decode($conditions);
 536      }
 537  
 538      /**
 539       * Set the settings values of the report
 540       *
 541       * @param array $values
 542       * @return bool
 543       */
 544      final public function set_settings_values(array $values): bool {
 545          $currentsettings = $this->get_settings_values();
 546          $settings = array_merge($currentsettings, $values);
 547          $this->report->set('settingsdata', json_encode($settings))
 548              ->save();
 549          return true;
 550      }
 551  
 552      /**
 553       * Get the settings values of the report
 554       *
 555       * @return array
 556       */
 557      final public function get_settings_values(): array {
 558          $settings = (string) $this->report->get('settingsdata');
 559  
 560          return (array) json_decode($settings);
 561      }
 562  
 563      /**
 564       * Adds a filter to the report
 565       *
 566       * @param filter $filter
 567       * @return filter
 568       * @throws coding_exception
 569       */
 570      final protected function add_filter(filter $filter): filter {
 571          if (!array_key_exists($filter->get_entity_name(), $this->entitytitles)) {
 572              throw new coding_exception('Invalid entity name', $filter->get_entity_name());
 573          }
 574  
 575          $name = $filter->get_name();
 576          if (empty($name) || $name !== clean_param($name, PARAM_ALPHANUMEXT)) {
 577              throw new coding_exception('Filter name must be comprised of alphanumeric character, underscore or dash');
 578          }
 579  
 580          $uniqueidentifier = $filter->get_unique_identifier();
 581          if (array_key_exists($uniqueidentifier, $this->filters)) {
 582              throw new coding_exception('Duplicate filter identifier', $uniqueidentifier);
 583          }
 584  
 585          $this->filters[$uniqueidentifier] = $filter;
 586  
 587          return $filter;
 588      }
 589  
 590      /**
 591       * Add given filter to the report from an entity
 592       *
 593       * The entity must have already been added to the report before calling this method
 594       *
 595       * @param string $uniqueidentifier
 596       * @return filter
 597       */
 598      final protected function add_filter_from_entity(string $uniqueidentifier): filter {
 599          [$entityname, $filtername] = explode(':', $uniqueidentifier, 2);
 600  
 601          return $this->add_filter($this->get_entity($entityname)->get_filter($filtername));
 602      }
 603  
 604      /**
 605       * Add given filters to the report from one or more entities
 606       *
 607       * Each entity must have already been added to the report before calling this method
 608       *
 609       * @param string[] $filters Unique identifier of each entity filter
 610       */
 611      final protected function add_filters_from_entities(array $filters): void {
 612          foreach ($filters as $filter) {
 613              $this->add_filter_from_entity($filter);
 614          }
 615      }
 616  
 617      /**
 618       * Return report filter by unique identifier
 619       *
 620       * @param string $uniqueidentifier
 621       * @return filter|null
 622       */
 623      final public function get_filter(string $uniqueidentifier): ?filter {
 624          return $this->filters[$uniqueidentifier] ?? null;
 625      }
 626  
 627      /**
 628       * Return all available report filters
 629       *
 630       * @return filter[]
 631       */
 632      final public function get_filters(): array {
 633          return array_filter($this->filters, static function(filter $filter): bool {
 634              return $filter->get_is_available();
 635          });
 636      }
 637  
 638      /**
 639       * Return all active report filters (by default, all available filters)
 640       *
 641       * @return filter[]
 642       */
 643      public function get_active_filters(): array {
 644          return $this->get_filters();
 645      }
 646  
 647      /**
 648       * Return all active report filter instances
 649       *
 650       * @return filter_base[]
 651       */
 652      final public function get_filter_instances(): array {
 653          return array_map(static function(filter $filter): filter_base {
 654              /** @var filter_base $filterclass */
 655              $filterclass = $filter->get_filter_class();
 656  
 657              return $filterclass::create($filter);
 658          }, $this->get_active_filters());
 659      }
 660  
 661      /**
 662       * Set the filter values of the report
 663       *
 664       * @param array $values
 665       * @return bool
 666       */
 667      final public function set_filter_values(array $values): bool {
 668          return user_filter_manager::set($this->report->get('id'), $values);
 669      }
 670  
 671      /**
 672       * Get the filter values of the report
 673       *
 674       * @return array
 675       */
 676      final public function get_filter_values(): array {
 677          return user_filter_manager::get($this->report->get('id'));
 678      }
 679  
 680      /**
 681       * Return the number of filter instances that are being applied based on the report's filter values (i.e. user has
 682       * configured them from their initial "Any value" state)
 683       *
 684       * @return int
 685       */
 686      final public function get_applied_filter_count(): int {
 687          $values = $this->get_filter_values();
 688          $applied = array_filter($this->get_filter_instances(), static function(filter_base $filter) use ($values): bool {
 689              return $filter->applies_to_values($values);
 690          });
 691  
 692          return count($applied);
 693      }
 694  
 695      /**
 696       * Set if the report can be downloaded.
 697       *
 698       * @param bool $downloadable
 699       * @param string $downloadfilename If the report is downloadable, then a filename should be provided here
 700       */
 701      final public function set_downloadable(bool $downloadable, string $downloadfilename = 'export'): void {
 702          $this->downloadable = $downloadable;
 703          $this->downloadfilename = $downloadfilename;
 704      }
 705  
 706      /**
 707       * Get if the report can be downloaded.
 708       *
 709       * @return bool
 710       */
 711      final public function is_downloadable(): bool {
 712          return $this->downloadable;
 713      }
 714  
 715      /**
 716       * Return the downloadable report filename
 717       *
 718       * @return string
 719       */
 720      final public function get_downloadfilename(): string {
 721          return $this->downloadfilename;
 722      }
 723  
 724      /**
 725       * Returns the report context
 726       *
 727       * @return context
 728       */
 729      public function get_context(): context {
 730          return $this->report->get_context();
 731      }
 732  
 733      /**
 734       * Set the default 'per page' size
 735       *
 736       * @param int $defaultperpage
 737       */
 738      public function set_default_per_page(int $defaultperpage): void {
 739          $this->defaultperpage = $defaultperpage;
 740      }
 741  
 742      /**
 743       * Default 'per page' size
 744       *
 745       * @return int
 746       */
 747      public function get_default_per_page(): int {
 748          return $this->defaultperpage;
 749      }
 750  
 751      /**
 752       * Add report attributes (data-, class, etc.) that will be included in HTML when report is displayed
 753       *
 754       * @param array $attributes
 755       * @return self
 756       */
 757      public function add_attributes(array $attributes): self {
 758          $this->attributes = $attributes + $this->attributes;
 759          return $this;
 760      }
 761  
 762      /**
 763       * Returns the report HTML attributes
 764       *
 765       * @return array
 766       */
 767      public function get_attributes(): array {
 768          return $this->attributes;
 769      }
 770  }