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.

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