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 400 and 402] [Versions 401 and 402]

   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\aggregation;
  20  
  21  use lang_string;
  22  use core_reportbuilder\local\helpers\database;
  23  use core_reportbuilder\local\report\column;
  24  
  25  /**
  26   * Column group concatenation aggregation type
  27   *
  28   * @package     core_reportbuilder
  29   * @copyright   2021 Paul Holden <paulh@moodle.com>
  30   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  31   */
  32  class groupconcat extends base {
  33  
  34      /** @var string Character to use as a delimeter between column fields */
  35      protected const COLUMN_FIELD_DELIMETER = '<|>';
  36  
  37      /** @var string Character to use a null coalesce value */
  38      protected const COLUMN_NULL_COALESCE = '<^>';
  39  
  40      /** @var string Character to use as a delimeter between field values */
  41      protected const FIELD_VALUE_DELIMETER = '<,>';
  42  
  43      /**
  44       * Return aggregation name
  45       *
  46       * @return lang_string
  47       */
  48      public static function get_name(): lang_string {
  49          return new lang_string('aggregationgroupconcat', 'core_reportbuilder');
  50      }
  51  
  52      /**
  53       * This aggregation can be performed on all non-timestamp columns
  54       *
  55       * @param int $columntype
  56       * @return bool
  57       */
  58      public static function compatible(int $columntype): bool {
  59          return !in_array($columntype, [
  60              column::TYPE_TIMESTAMP,
  61          ]);
  62      }
  63  
  64      /**
  65       * Override base method to ensure all SQL fields are concatenated together if there are multiple
  66       *
  67       * @param array $sqlfields
  68       * @return string
  69       */
  70      public static function get_column_field_sql(array $sqlfields): string {
  71          if (count($sqlfields) === 1) {
  72              return parent::get_column_field_sql($sqlfields);
  73          }
  74  
  75          return self::get_column_fields_concat($sqlfields, self::COLUMN_FIELD_DELIMETER, self::COLUMN_NULL_COALESCE);
  76      }
  77  
  78      /**
  79       * Return the aggregated field SQL
  80       *
  81       * @param string $field
  82       * @param int $columntype
  83       * @return string
  84       */
  85      public static function get_field_sql(string $field, int $columntype): string {
  86          global $DB;
  87  
  88          $fieldsort = database::sql_group_concat_sort($field);
  89  
  90          return $DB->sql_group_concat($field, self::FIELD_VALUE_DELIMETER, $fieldsort);
  91      }
  92  
  93      /**
  94       * Return formatted value for column when applying aggregation, note we need to split apart the concatenated string
  95       * and apply callbacks to each concatenated value separately
  96       *
  97       * @param mixed $value
  98       * @param array $values
  99       * @param array $callbacks
 100       * @param int $columntype
 101       * @return mixed
 102       */
 103      public static function format_value($value, array $values, array $callbacks, int $columntype) {
 104          $firstvalue = reset($values);
 105          if ($firstvalue === null) {
 106              return '';
 107          }
 108  
 109          $formattedvalues = [];
 110  
 111          // Store original names of all values that would be present without aggregation.
 112          $valuenames = array_keys($values);
 113          $valuenamescount = count($valuenames);
 114  
 115          // Loop over each extracted value from the concatenated string.
 116          $values = explode(self::FIELD_VALUE_DELIMETER, (string)$firstvalue);
 117          foreach ($values as $value) {
 118  
 119              // Ensure we have equal number of value names/data, account for truncation by DB.
 120              $valuedata = explode(self::COLUMN_FIELD_DELIMETER, $value);
 121              if ($valuenamescount !== count($valuedata)) {
 122                  continue;
 123              }
 124  
 125              // Re-construct original values, also ensuring any nulls contained within are restored.
 126              $originalvalues = array_map(static function(string $value): ?string {
 127                  return $value === self::COLUMN_NULL_COALESCE ? null : $value;
 128              }, array_combine($valuenames, $valuedata));
 129  
 130              $originalvalue = column::get_default_value($originalvalues, $columntype);
 131  
 132              // Once we've re-constructed each value, we can apply callbacks to it.
 133              $formattedvalues[] = parent::format_value($originalvalue, $originalvalues, $callbacks, $columntype);
 134          }
 135  
 136          $listseparator = get_string('listsep', 'langconfig') . ' ';
 137          return implode($listseparator, $formattedvalues);
 138      }
 139  }