Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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