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