Search moodle.org's
Developer Documentation

See Release Notes

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

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