Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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\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       * We cannot sort this aggregation type
  66       *
  67       * @param bool $columnsortable
  68       * @return bool
  69       */
  70      public static function sortable(bool $columnsortable): bool {
  71          return false;
  72      }
  73  
  74      /**
  75       * Override base method to ensure all SQL fields are concatenated together if there are multiple
  76       *
  77       * @param array $sqlfields
  78       * @return string
  79       */
  80      public static function get_column_field_sql(array $sqlfields): string {
  81          if (count($sqlfields) === 1) {
  82              return parent::get_column_field_sql($sqlfields);
  83          }
  84  
  85          return self::get_column_fields_concat($sqlfields, self::COLUMN_FIELD_DELIMETER, self::COLUMN_NULL_COALESCE);
  86      }
  87  
  88      /**
  89       * Return the aggregated field SQL
  90       *
  91       * @param string $field
  92       * @param int $columntype
  93       * @return string
  94       */
  95      public static function get_field_sql(string $field, int $columntype): string {
  96          global $DB;
  97  
  98          $fieldsort = database::sql_group_concat_sort($field);
  99  
 100          return $DB->sql_group_concat($field, self::FIELD_VALUE_DELIMETER, $fieldsort);
 101      }
 102  
 103      /**
 104       * Return formatted value for column when applying aggregation, note we need to split apart the concatenated string
 105       * and apply callbacks to each concatenated value separately
 106       *
 107       * @param mixed $value
 108       * @param array $values
 109       * @param array $callbacks
 110       * @return mixed
 111       */
 112      public static function format_value($value, array $values, array $callbacks) {
 113          $firstvalue = reset($values);
 114          if ($firstvalue === null) {
 115              return '';
 116          }
 117          $formattedvalues = [];
 118  
 119          // Store original names of all values that would be present without aggregation.
 120          $valuenames = array_keys($values);
 121          $valuenamescount = count($valuenames);
 122  
 123          // Loop over each extracted value from the concatenated string.
 124          $values = explode(self::FIELD_VALUE_DELIMETER, (string)$firstvalue);
 125          foreach ($values as $value) {
 126  
 127              // Ensure we have equal number of value names/data, account for truncation by DB.
 128              $valuedata = explode(self::COLUMN_FIELD_DELIMETER, $value);
 129              if ($valuenamescount !== count($valuedata)) {
 130                  continue;
 131              }
 132  
 133              // Re-construct original values, also ensuring any nulls contained within are restored.
 134              $originalvalue = array_map(static function(string $value): ?string {
 135                  return $value === self::COLUMN_NULL_COALESCE ? null : $value;
 136              }, array_combine($valuenames, $valuedata));
 137  
 138              $originalfirstvalue = reset($originalvalue);
 139  
 140              // Once we've re-constructed each value, we can apply callbacks to it.
 141              $formattedvalues[] = parent::format_value($originalfirstvalue, $originalvalue, $callbacks);
 142          }
 143  
 144          $listseparator = get_string('listsep', 'langconfig') . ' ';
 145          return implode($listseparator, $formattedvalues);
 146      }
 147  }