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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body