Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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