Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [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 mixed $options */
  61      protected $options;
  62  
  63      /** @var array $limitoperators */
  64      protected $limitoperators = [];
  65  
  66      /** @var filter_model $persistent */
  67      protected $persistent;
  68  
  69      /**
  70       * Filter constructor
  71       *
  72       * @param string $filterclass Filter type class to use, must extend {@see base} filter class
  73       * @param string $name Internal name of the filter
  74       * @param lang_string $header Title of the filter used in reports
  75       * @param string $entityname Name of the entity this filter belongs to. Typically when creating filters within entities
  76       *      this value should be the result of calling {@see get_entity_name}, however if creating filters inside reports directly
  77       *      it should be the name of the entity as passed to {@see \core_reportbuilder\local\report\base::annotate_entity}
  78       * @param string $fieldsql SQL clause to use for filtering, {@see set_field_sql}
  79       * @param array $fieldparams
  80       * @throws moodle_exception For invalid filter class
  81       */
  82      public function __construct(
  83          string $filterclass,
  84          string $name,
  85          lang_string $header,
  86          string $entityname,
  87          string $fieldsql = '',
  88          array $fieldparams = []
  89      ) {
  90          if (!class_exists($filterclass) || !is_subclass_of($filterclass, base::class)) {
  91              throw new moodle_exception('filterinvalid', 'reportbuilder', '', null, $filterclass);
  92          }
  93  
  94          $this->filterclass = $filterclass;
  95          $this->name = $name;
  96          $this->header = $header;
  97          $this->entityname = $entityname;
  98  
  99          if ($fieldsql !== '') {
 100              $this->set_field_sql($fieldsql, $fieldparams);
 101          }
 102      }
 103  
 104      /**
 105       * Get filter class path
 106       *
 107       * @return string
 108       */
 109      public function get_filter_class(): string {
 110          return $this->filterclass;
 111      }
 112  
 113      /**
 114       * Get filter name
 115       *
 116       * @return string
 117       */
 118      public function get_name(): string {
 119          return $this->name;
 120      }
 121  
 122      /**
 123       * Return header
 124       *
 125       * @return string
 126       */
 127      public function get_header(): string {
 128          return $this->header->out();
 129      }
 130  
 131      /**
 132       * Set header
 133       *
 134       * @param lang_string $header
 135       * @return self
 136       */
 137      public function set_header(lang_string $header): self {
 138          $this->header = $header;
 139          return $this;
 140      }
 141  
 142      /**
 143       * Return filter entity name
 144       *
 145       * @return string
 146       */
 147      public function get_entity_name(): string {
 148          return $this->entityname;
 149      }
 150  
 151      /**
 152       * Return unique identifier for this filter
 153       *
 154       * @return string
 155       */
 156      public function get_unique_identifier(): string {
 157          return $this->get_entity_name() . ':' . $this->get_name();
 158      }
 159  
 160      /**
 161       * Return joins
 162       *
 163       * @return string[]
 164       */
 165      public function get_joins(): array {
 166          return array_values($this->joins);
 167      }
 168  
 169      /**
 170       * Add join clause required for this filter to join to existing tables/entities
 171       *
 172       * This is necessary in the case where {@see set_field_sql} is selecting data from a table that isn't otherwise queried
 173       *
 174       * @param string $join
 175       * @return self
 176       */
 177      public function add_join(string $join): self {
 178          $this->joins[trim($join)] = trim($join);
 179          return $this;
 180      }
 181  
 182      /**
 183       * Add multiple join clauses required for this filter, passing each to {@see add_join}
 184       *
 185       * Typically when defining filters in entities, you should pass {@see \core_reportbuilder\local\report\base::get_joins} to
 186       * this method, so that all entity joins are included in the report when your filter is used in it
 187       *
 188       * @param string[] $joins
 189       * @return self
 190       */
 191      public function add_joins(array $joins): self {
 192          foreach ($joins as $join) {
 193              $this->add_join($join);
 194          }
 195          return $this;
 196      }
 197  
 198      /**
 199       * Get SQL expression for the field
 200       *
 201       * @return string
 202       */
 203      public function get_field_sql(): string {
 204          return $this->fieldsql;
 205      }
 206  
 207      /**
 208       * Get the SQL params for the field being filtered
 209       *
 210       * @return array
 211       */
 212      public function get_field_params(): array {
 213          return $this->fieldparams;
 214      }
 215  
 216      /**
 217       * Retrieve SQL expression and parameters for the field
 218       *
 219       * @param int $index
 220       * @return array [$sql, [...$params]]
 221       */
 222      public function get_field_sql_and_params(int $index = 0): array {
 223          $fieldsql = $this->get_field_sql();
 224          $fieldparams = $this->get_field_params();
 225  
 226          // Shortcut if there aren't any parameters.
 227          if (empty($fieldparams)) {
 228              return [$fieldsql, $fieldparams];
 229          }
 230  
 231          // Simple callback for replacement of parameter names within filter SQL.
 232          $transform = function(string $param) use ($index): string {
 233              return "{$param}_{$index}";
 234          };
 235  
 236          $paramnames = array_keys($fieldparams);
 237          $sql = database::sql_replace_parameter_names($fieldsql, $paramnames, $transform);
 238  
 239          $params = [];
 240          foreach ($paramnames as $paramname) {
 241              $paramnametransform = $transform($paramname);
 242              $params[$paramnametransform] = $fieldparams[$paramname];
 243          }
 244  
 245          return [$sql, $params];
 246      }
 247  
 248      /**
 249       * Set the SQL expression for the field that is being filtered. It will be passed to the filter class
 250       *
 251       * @param string $sql
 252       * @param array $params
 253       * @return self
 254       */
 255      public function set_field_sql(string $sql, array $params = []): self {
 256          $this->fieldsql = $sql;
 257          $this->fieldparams = $params;
 258          return $this;
 259      }
 260  
 261      /**
 262       * Return available state of the filter for the current user
 263       *
 264       * @return bool
 265       */
 266      public function get_is_available(): bool {
 267          return $this->available;
 268      }
 269  
 270      /**
 271       * Conditionally set whether the filter is available. For instance the filter may be added to a report with the
 272       * expectation that only some users are able to see it
 273       *
 274       * @param bool $available
 275       * @return self
 276       */
 277      public function set_is_available(bool $available): self {
 278          $this->available = $available;
 279          return $this;
 280      }
 281  
 282      /**
 283       * Set the options for the filter in the format that the filter class expected (e.g. the "select" filter expects an array)
 284       *
 285       * This method should only be used if the options do not require any calculations/queries, in which
 286       * case {@see set_options_callback} should be used. For performance, {@see get_string} shouldn't be used either, use of
 287       * {@see lang_string} is instead encouraged
 288       *
 289       * @param mixed $options
 290       * @return self
 291       */
 292      public function set_options($options): self {
 293          $this->options = $options;
 294          return $this;
 295      }
 296  
 297      /**
 298       * Set the options for the filter to be returned by a callback (that receives no arguments) in the format that the filter
 299       * class expects
 300       *
 301       * @param callable $callback
 302       * @return self
 303       */
 304      public function set_options_callback(callable $callback): self {
 305          $this->options = $callback;
 306          return $this;
 307      }
 308  
 309      /**
 310       * Get the options for the filter, returning via the the previously set options or generated via defined options callback
 311       *
 312       * @return mixed
 313       */
 314      public function get_options() {
 315          if (is_callable($this->options)) {
 316              $callable = $this->options;
 317              $this->options = ($callable)();
 318          }
 319          return $this->options;
 320      }
 321  
 322      /**
 323       * Set a limited subset of operators that should be used for the filter, refer to each filter class to find defined
 324       * operator constants
 325       *
 326       * @param array $limitoperators Simple array of operator values
 327       * @return self
 328       */
 329      public function set_limited_operators(array $limitoperators): self {
 330          $this->limitoperators = $limitoperators;
 331          return $this;
 332      }
 333  
 334      /**
 335       * Filter given operators to include only those previously defined by {@see set_limited_operators}
 336       *
 337       * @param array $operators All operators as defined by the filter class
 338       * @return array
 339       */
 340      public function restrict_limited_operators(array $operators): array {
 341          if (empty($this->limitoperators)) {
 342              return $operators;
 343          }
 344  
 345          return array_intersect_key($operators, array_flip($this->limitoperators));
 346      }
 347  
 348      /**
 349       * Set filter persistent
 350       *
 351       * @param filter_model $persistent
 352       * @return self
 353       */
 354      public function set_persistent(filter_model $persistent): self {
 355          $this->persistent = $persistent;
 356          return $this;
 357      }
 358  
 359      /**
 360       * Return filter persistent
 361       *
 362       * @return filter_model|null
 363       */
 364      public function get_persistent(): ?filter_model {
 365          return $this->persistent ?? null;
 366      }
 367  }