Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  declare(strict_types=1);
  18  
  19  namespace core_reportbuilder\local\report;
  20  
  21  use lang_string;
  22  use moodle_exception;
  23  use core_reportbuilder\local\filters\base;
  24  use core_reportbuilder\local\helpers\database;
  25  use core_reportbuilder\local\models\filter as filter_model;
  26  
  27  /**
  28   * Class to represent a report filter
  29   *
  30   * @package     core_reportbuilder
  31   * @copyright   2021 Paul Holden <paulh@moodle.com>
  32   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  final class filter {
  35  
  36      /** @var string $filterclass */
  37      private $filterclass;
  38  
  39      /** @var string $name */
  40      private $name;
  41  
  42      /** @var lang_string $header */
  43      private $header;
  44  
  45      /** @var string $entity */
  46      private $entityname;
  47  
  48      /** @var string $fieldsql */
  49      private $fieldsql = '';
  50  
  51      /** @var array $fieldparams */
  52      private $fieldparams = [];
  53  
  54      /** @var string[] $joins */
  55      protected $joins = [];
  56  
  57      /** @var bool $available */
  58      protected $available = true;
  59  
  60      /** @var bool $deprecated */
  61      protected $deprecated = false;
  62  
  63      /** @var string $deprecatedmessage */
  64      protected $deprecatedmessage;
  65  
  66      /** @var mixed $options */
  67      protected $options;
  68  
  69      /** @var array $limitoperators */
  70      protected $limitoperators = [];
  71  
  72      /** @var filter_model $persistent */
  73      protected $persistent;
  74  
  75      /**
  76       * Filter constructor
  77       *
  78       * @param string $filterclass Filter type class to use, must extend {@see base} filter class
  79       * @param string $name Internal name of the filter
  80       * @param lang_string $header Title of the filter used in reports
  81       * @param string $entityname Name of the entity this filter belongs to. Typically when creating filters within entities
  82       *      this value should be the result of calling {@see get_entity_name}, however if creating filters inside reports directly
  83       *      it should be the name of the entity as passed to {@see \core_reportbuilder\local\report\base::annotate_entity}
  84       * @param string $fieldsql SQL clause to use for filtering, {@see set_field_sql}
  85       * @param array $fieldparams
  86       * @throws moodle_exception For invalid filter class
  87       */
  88      public function __construct(
  89          string $filterclass,
  90          string $name,
  91          lang_string $header,
  92          string $entityname,
  93          string $fieldsql = '',
  94          array $fieldparams = []
  95      ) {
  96          if (!class_exists($filterclass) || !is_subclass_of($filterclass, base::class)) {
  97              throw new moodle_exception('filterinvalid', 'reportbuilder', '', null, $filterclass);
  98          }
  99  
 100          $this->filterclass = $filterclass;
 101          $this->name = $name;
 102          $this->header = $header;
 103          $this->entityname = $entityname;
 104  
 105          if ($fieldsql !== '') {
 106              $this->set_field_sql($fieldsql, $fieldparams);
 107          }
 108      }
 109  
 110      /**
 111       * Get filter class path
 112       *
 113       * @return string
 114       */
 115      public function get_filter_class(): string {
 116          return $this->filterclass;
 117      }
 118  
 119      /**
 120       * Get filter name
 121       *
 122       * @return string
 123       */
 124      public function get_name(): string {
 125          return $this->name;
 126      }
 127  
 128      /**
 129       * Return header
 130       *
 131       * @return string
 132       */
 133      public function get_header(): string {
 134          return $this->header->out();
 135      }
 136  
 137      /**
 138       * Set header
 139       *
 140       * @param lang_string $header
 141       * @return self
 142       */
 143      public function set_header(lang_string $header): self {
 144          $this->header = $header;
 145          return $this;
 146      }
 147  
 148      /**
 149       * Return filter entity name
 150       *
 151       * @return string
 152       */
 153      public function get_entity_name(): string {
 154          return $this->entityname;
 155      }
 156  
 157      /**
 158       * Return unique identifier for this filter
 159       *
 160       * @return string
 161       */
 162      public function get_unique_identifier(): string {
 163          return $this->get_entity_name() . ':' . $this->get_name();
 164      }
 165  
 166      /**
 167       * Return joins
 168       *
 169       * @return string[]
 170       */
 171      public function get_joins(): array {
 172          return array_values($this->joins);
 173      }
 174  
 175      /**
 176       * Add join clause required for this filter to join to existing tables/entities
 177       *
 178       * This is necessary in the case where {@see set_field_sql} is selecting data from a table that isn't otherwise queried
 179       *
 180       * @param string $join
 181       * @return self
 182       */
 183      public function add_join(string $join): self {
 184          $this->joins[trim($join)] = trim($join);
 185          return $this;
 186      }
 187  
 188      /**
 189       * Add multiple join clauses required for this filter, passing each to {@see add_join}
 190       *
 191       * Typically when defining filters in entities, you should pass {@see \core_reportbuilder\local\report\base::get_joins} to
 192       * this method, so that all entity joins are included in the report when your filter is used in it
 193       *
 194       * @param string[] $joins
 195       * @return self
 196       */
 197      public function add_joins(array $joins): self {
 198          foreach ($joins as $join) {
 199              $this->add_join($join);
 200          }
 201          return $this;
 202      }
 203  
 204      /**
 205       * Get SQL expression for the field
 206       *
 207       * @return string
 208       */
 209      public function get_field_sql(): string {
 210          return $this->fieldsql;
 211      }
 212  
 213      /**
 214       * Get the SQL params for the field being filtered
 215       *
 216       * @return array
 217       */
 218      public function get_field_params(): array {
 219          return $this->fieldparams;
 220      }
 221  
 222      /**
 223       * Retrieve SQL expression and parameters for the field
 224       *
 225       * @param int $index
 226       * @return array [$sql, [...$params]]
 227       */
 228      public function get_field_sql_and_params(int $index = 0): array {
 229          $fieldsql = $this->get_field_sql();
 230          $fieldparams = $this->get_field_params();
 231  
 232          // Shortcut if there aren't any parameters.
 233          if (empty($fieldparams)) {
 234              return [$fieldsql, $fieldparams];
 235          }
 236  
 237          // Simple callback for replacement of parameter names within filter SQL.
 238          $transform = function(string $param) use ($index): string {
 239              return "{$param}_{$index}";
 240          };
 241  
 242          $paramnames = array_keys($fieldparams);
 243          $sql = database::sql_replace_parameter_names($fieldsql, $paramnames, $transform);
 244  
 245          $params = [];
 246          foreach ($paramnames as $paramname) {
 247              $paramnametransform = $transform($paramname);
 248              $params[$paramnametransform] = $fieldparams[$paramname];
 249          }
 250  
 251          return [$sql, $params];
 252      }
 253  
 254      /**
 255       * Set the SQL expression for the field that is being filtered. It will be passed to the filter class
 256       *
 257       * @param string $sql
 258       * @param array $params
 259       * @return self
 260       */
 261      public function set_field_sql(string $sql, array $params = []): self {
 262          $this->fieldsql = $sql;
 263          $this->fieldparams = $params;
 264          return $this;
 265      }
 266  
 267      /**
 268       * Return available state of the filter for the current user
 269       *
 270       * @return bool
 271       */
 272      public function get_is_available(): bool {
 273          return $this->available;
 274      }
 275  
 276      /**
 277       * Conditionally set whether the filter is available. For instance the filter may be added to a report with the
 278       * expectation that only some users are able to see it
 279       *
 280       * @param bool $available
 281       * @return self
 282       */
 283      public function set_is_available(bool $available): self {
 284          $this->available = $available;
 285          return $this;
 286      }
 287  
 288      /**
 289       * Set deprecated state of the filter, in which case it will still be shown when already present in existing reports but
 290       * won't be available for selection in the report editor
 291       *
 292       * @param string $deprecatedmessage
 293       * @return self
 294       */
 295      public function set_is_deprecated(string $deprecatedmessage = ''): self {
 296          $this->deprecated = true;
 297          $this->deprecatedmessage = $deprecatedmessage;
 298          return $this;
 299      }
 300  
 301      /**
 302       * Return deprecated state of the filter
 303       *
 304       * @return bool
 305       */
 306      public function get_is_deprecated(): bool {
 307          return $this->deprecated;
 308      }
 309  
 310      /**
 311       * Return deprecated message of the filter
 312       *
 313       * @return string
 314       */
 315      public function get_is_deprecated_message(): string {
 316          return $this->deprecatedmessage;
 317      }
 318  
 319      /**
 320       * Set the options for the filter in the format that the filter class expected (e.g. the "select" filter expects an array)
 321       *
 322       * This method should only be used if the options do not require any calculations/queries, in which
 323       * case {@see set_options_callback} should be used. For performance, {@see get_string} shouldn't be used either, use of
 324       * {@see lang_string} is instead encouraged
 325       *
 326       * @param mixed $options
 327       * @return self
 328       */
 329      public function set_options($options): self {
 330          $this->options = $options;
 331          return $this;
 332      }
 333  
 334      /**
 335       * Set the options for the filter to be returned by a callback (that receives no arguments) in the format that the filter
 336       * class expects
 337       *
 338       * @param callable $callback
 339       * @return self
 340       */
 341      public function set_options_callback(callable $callback): self {
 342          $this->options = $callback;
 343          return $this;
 344      }
 345  
 346      /**
 347       * Get the options for the filter, returning via the the previously set options or generated via defined options callback
 348       *
 349       * @return mixed
 350       */
 351      public function get_options() {
 352          if (is_callable($this->options)) {
 353              $callable = $this->options;
 354              $this->options = ($callable)();
 355          }
 356          return $this->options;
 357      }
 358  
 359      /**
 360       * Set a limited subset of operators that should be used for the filter, refer to each filter class to find defined
 361       * operator constants
 362       *
 363       * @param array $limitoperators Simple array of operator values
 364       * @return self
 365       */
 366      public function set_limited_operators(array $limitoperators): self {
 367          $this->limitoperators = $limitoperators;
 368          return $this;
 369      }
 370  
 371      /**
 372       * Filter given operators to include only those previously defined by {@see set_limited_operators}
 373       *
 374       * @param array $operators All operators as defined by the filter class
 375       * @return array
 376       */
 377      public function restrict_limited_operators(array $operators): array {
 378          if (empty($this->limitoperators)) {
 379              return $operators;
 380          }
 381  
 382          return array_intersect_key($operators, array_flip($this->limitoperators));
 383      }
 384  
 385      /**
 386       * Set filter persistent
 387       *
 388       * @param filter_model $persistent
 389       * @return self
 390       */
 391      public function set_persistent(filter_model $persistent): self {
 392          $this->persistent = $persistent;
 393          return $this;
 394      }
 395  
 396      /**
 397       * Return filter persistent
 398       *
 399       * @return filter_model|null
 400       */
 401      public function get_persistent(): ?filter_model {
 402          return $this->persistent ?? null;
 403      }
 404  }