See Release Notes
Long Term Support Release
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body