Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 400 and 401] [Versions 401 and 402] [Versions 401 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       * @param int $columntype
 111       * @return mixed
 112       */
 113      public static function format_value($value, array $values, array $callbacks, int $columntype) {
 114          $firstvalue = reset($values);
 115          if ($firstvalue === null) {
 116              return '';
 117          }
 118  
 119          $formattedvalues = [];
 120  
 121          // Store original names of all values that would be present without aggregation.
 122          $valuenames = array_keys($values);
 123          $valuenamescount = count($valuenames);
 124  
 125          // Loop over each extracted value from the concatenated string.
 126          $values = explode(self::FIELD_VALUE_DELIMETER, (string)$firstvalue);
 127          foreach ($values as $value) {
 128  
 129              // Ensure we have equal number of value names/data, account for truncation by DB.
 130              $valuedata = explode(self::COLUMN_FIELD_DELIMETER, $value);
 131              if ($valuenamescount !== count($valuedata)) {
 132                  continue;
 133              }
 134  
 135              // Re-construct original values, also ensuring any nulls contained within are restored.
 136              $originalvalues = array_map(static function(string $value): ?string {
 137                  return $value === self::COLUMN_NULL_COALESCE ? null : $value;
 138              }, array_combine($valuenames, $valuedata));
 139  
 140              $originalvalue = column::get_default_value($originalvalues, $columntype);
 141  
 142              // Once we've re-constructed each value, we can apply callbacks to it.
 143              $formattedvalues[] = parent::format_value($originalvalue, $originalvalues, $callbacks, $columntype);
 144          }
 145  
 146          $listseparator = get_string('listsep', 'langconfig') . ' ';
 147          return implode($listseparator, $formattedvalues);
 148      }
 149  }