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.
   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  namespace core\progress;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  
  21  /**
  22   * Base class for handling progress information.
  23   *
  24   * Subclasses should generally override the {@link current_progress} function which
  25   * summarises all progress information.
  26   *
  27   * @package core_progress
  28   * @copyright 2013 The Open University
  29   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   */
  31  abstract class base {
  32      /**
  33       * @var int Constant indicating that the number of progress calls is unknown.
  34       */
  35      const INDETERMINATE = -1;
  36  
  37      /**
  38       * This value is set rather high to ensure there are no regressions from
  39       * previous behaviour. For testing, it may be useful to set the
  40       * frontendservertimeout config option to a lower value, such as 180
  41       * seconds (default for some commercial products).
  42       *
  43       * @var int The number of seconds that can pass without {@link progress()} calls.
  44       */
  45      const TIME_LIMIT_WITHOUT_PROGRESS = 3600;
  46  
  47      /**
  48       * @var int Time of last progress call.
  49       */
  50      protected $lastprogresstime;
  51  
  52      /**
  53       * @var int Number of progress calls (restricted to ~ 1/second).
  54       */
  55      protected $count;
  56  
  57      /**
  58       * @var array Array of progress descriptions for each stack level.
  59       */
  60      protected $descriptions = array();
  61  
  62      /**
  63       * @var array Array of maximum progress values for each stack level.
  64       */
  65      protected $maxes = array();
  66  
  67      /**
  68       * @var array Array of current progress values.
  69       */
  70      protected $currents = array();
  71  
  72      /**
  73       * @var int[] Array of counts within parent progress entry (ignored for first)
  74       */
  75      protected $parentcounts = array();
  76  
  77      /**
  78       * Marks the start of an operation that will display progress.
  79       *
  80       * This can be called multiple times for nested progress sections. It must
  81       * be paired with calls to end_progress.
  82       *
  83       * The progress maximum may be {@link self::INDETERMINATE} if the current operation has
  84       * an unknown number of steps. (This is default.)
  85       *
  86       * Calling this function will always result in a new display, so this
  87       * should not be called exceedingly frequently.
  88       *
  89       * When it is complete by calling {@link end_progress()}, each {@link start_progress} section
  90       * automatically adds progress to its parent, as defined by $parentcount.
  91       *
  92       * @param string $description Description to display
  93       * @param int $max Maximum value of progress for this section
  94       * @param int $parentcount How many progress points this section counts for
  95       * @throws \coding_exception If max is invalid
  96       */
  97      public function start_progress($description, $max = self::INDETERMINATE,
  98              $parentcount = 1) {
  99          if ($max != self::INDETERMINATE && $max < 0) {
 100              throw new \coding_exception(
 101                      'start_progress() max value cannot be negative');
 102          }
 103          if ($parentcount < 1) {
 104              throw new \coding_exception(
 105                      'start_progress() parent progress count must be at least 1');
 106          }
 107          if (!empty($this->descriptions)) {
 108              $prevmax = end($this->maxes);
 109              if ($prevmax !== self::INDETERMINATE) {
 110                  $prevcurrent = end($this->currents);
 111                  if ($prevcurrent + $parentcount > $prevmax) {
 112                      throw new \coding_exception(
 113                              'start_progress() parent progress would exceed max');
 114                  }
 115              }
 116          } else {
 117              if ($parentcount != 1) {
 118                  throw new \coding_exception(
 119                          'start_progress() progress count must be 1 when no parent');
 120              }
 121          }
 122          $this->descriptions[] = $description;
 123          $this->maxes[] = $max;
 124          $this->currents[] = 0;
 125          $this->parentcounts[] = $parentcount;
 126          $this->update_progress();
 127      }
 128  
 129      /**
 130       * Marks the end of an operation that will display progress.
 131       *
 132       * This must be paired with each {@link start_progress} call.
 133       *
 134       * If there is a parent progress section, its progress will be increased
 135       * automatically to reflect the end of the child section.
 136       *
 137       * @throws \coding_exception If progress hasn't been started
 138       */
 139      public function end_progress() {
 140          if (!count($this->descriptions)) {
 141              throw new \coding_exception('end_progress() without start_progress()');
 142          }
 143          array_pop($this->descriptions);
 144          array_pop($this->maxes);
 145          array_pop($this->currents);
 146          $parentcount = array_pop($this->parentcounts);
 147          if (!empty($this->descriptions)) {
 148              $lastmax = end($this->maxes);
 149              if ($lastmax != self::INDETERMINATE) {
 150                  $lastvalue = end($this->currents);
 151                  $this->currents[key($this->currents)] = $lastvalue + $parentcount;
 152              }
 153          }
 154          $this->update_progress();
 155      }
 156  
 157      /**
 158       * Indicates that progress has occurred.
 159       *
 160       * The progress value should indicate the total progress so far, from 0
 161       * to the value supplied for $max (inclusive) in {@link start_progress}.
 162       *
 163       * You do not need to call this function for every value. It is OK to skip
 164       * values. It is also OK to call this function as often as desired; it
 165       * doesn't update the display if called more than once per second.
 166       *
 167       * It must be INDETERMINATE if {@link start_progress} was called with $max set to
 168       * INDETERMINATE. Otherwise it must not be indeterminate.
 169       *
 170       * @param int $progress Progress so far
 171       * @throws \coding_exception If progress value is invalid
 172       */
 173      public function progress($progress = self::INDETERMINATE) {
 174          // Check we are inside a progress section.
 175          $max = end($this->maxes);
 176          if ($max === false) {
 177              throw new \coding_exception(
 178                      'progress() without start_progress');
 179          }
 180  
 181          // Check and apply new progress.
 182          if ($progress === self::INDETERMINATE) {
 183              // Indeterminate progress.
 184              if ($max !== self::INDETERMINATE) {
 185                  throw new \coding_exception(
 186                          'progress() INDETERMINATE, expecting value');
 187              }
 188          } else {
 189              // Determinate progress.
 190              $current = end($this->currents);
 191              if ($max === self::INDETERMINATE) {
 192                  throw new \coding_exception(
 193                          'progress() with value, expecting INDETERMINATE');
 194              } else if ($progress < 0 || $progress > $max) {
 195                  throw new \coding_exception(
 196                          'progress() value out of range');
 197              } else if ($progress < $current) {
 198                  throw new \coding_exception(
 199                          'progress() value may not go backwards');
 200              }
 201              $this->currents[key($this->currents)] = $progress;
 202          }
 203  
 204          // Don't update progress bar too frequently (more than once per second).
 205          $now = $this->get_time();
 206          if ($now === $this->lastprogresstime) {
 207              return;
 208          }
 209  
 210          // Update progress.
 211          $this->count++;
 212          $this->lastprogresstime = $now;
 213  
 214          // Update time limit before next progress display.
 215          \core_php_time_limit::raise(self::TIME_LIMIT_WITHOUT_PROGRESS);
 216          $this->update_progress();
 217      }
 218  
 219      /**
 220       * An alternative to calling progress. This keeps track of the number of items done internally. Call this method
 221       * with no parameters to increment the internal counter by one or you can use the $incby parameter to specify a positive
 222       * change in progress. The internal progress counter should not exceed $max as passed to {@link start_progress} for this
 223       * section.
 224       *
 225       * If you called {@link start_progress} with parameter INDETERMINATE then you cannot call this method.
 226       *
 227       * @var int $incby The positive change to apply to the internal progress counter. Defaults to 1.
 228       */
 229      public function increment_progress($incby = 1) {
 230          $current = end($this->currents);
 231          $this->progress($current + $incby);
 232      }
 233  
 234      /**
 235       * Gets time (this is provided so that unit tests can override it).
 236       *
 237       * @return int Current system time
 238       */
 239      protected function get_time() {
 240          return time();
 241      }
 242  
 243      /**
 244       * Called whenever new progress should be displayed.
 245       */
 246      protected abstract function update_progress();
 247  
 248      /**
 249       * @return bool True if currently inside a progress section
 250       */
 251      public function is_in_progress_section() {
 252          return !empty($this->descriptions);
 253      }
 254  
 255      /**
 256       * Checks max value of current progress section.
 257       *
 258       * @return int Current max value - may be {@link \core\progress\base::INDETERMINATE}.
 259       * @throws \coding_exception If not in a progress section
 260       */
 261      public function get_current_max() {
 262          $max = end($this->maxes);
 263          if ($max === false) {
 264              throw new \coding_exception('Not inside progress section');
 265          }
 266          return $max;
 267      }
 268  
 269      /**
 270       * @throws \coding_exception
 271       * @return string Current progress section description
 272       */
 273      public function get_current_description() {
 274          $description = end($this->descriptions);
 275          if ($description === false) {
 276              throw new \coding_exception('Not inside progress section');
 277          }
 278          return $description;
 279      }
 280  
 281      /**
 282       * Obtains current progress in a way suitable for drawing a progress bar.
 283       *
 284       * Progress is returned as a minimum and maximum value. If there is no
 285       * indeterminate progress, these values will be identical. If there is
 286       * intermediate progress, these values can be different. (For example, if
 287       * the top level progress sections is indeterminate, then the values will
 288       * always be 0.0 and 1.0.)
 289       *
 290       * @return array Minimum and maximum possible progress proportions
 291       */
 292      public function get_progress_proportion_range() {
 293          // If there is no progress underway, we must have finished.
 294          if (empty($this->currents)) {
 295              return array(1.0, 1.0);
 296          }
 297          $count = count($this->currents);
 298          $min = 0.0;
 299          $max = 1.0;
 300          for ($i = 0; $i < $count; $i++) {
 301              // Get max value at that section - if it's indeterminate we can tell
 302              // no more.
 303              $sectionmax = $this->maxes[$i];
 304              if ($sectionmax === self::INDETERMINATE) {
 305                  return array($min, $max);
 306              }
 307  
 308              // Special case if current value is max (this should only happen
 309              // just before ending a section).
 310              $sectioncurrent = $this->currents[$i];
 311              if ($sectioncurrent === $sectionmax) {
 312                  return array($max, $max);
 313              }
 314  
 315              // Using the current value at that section, we know we are somewhere
 316              // between 'current' and the next 'current' value which depends on
 317              // the parentcount of the nested section (if any).
 318              $newmin = ($sectioncurrent / $sectionmax) * ($max - $min) + $min;
 319              $nextcurrent = $sectioncurrent + 1;
 320              if ($i + 1 < $count) {
 321                  $weight = $this->parentcounts[$i + 1];
 322                  $nextcurrent = $sectioncurrent + $weight;
 323              }
 324              $newmax = ($nextcurrent / $sectionmax) * ($max - $min) + $min;
 325              $min = $newmin;
 326              $max = $newmax;
 327          }
 328  
 329          // If there was nothing indeterminate, we use the min value as current.
 330          return array($min, $min);
 331      }
 332  
 333      /**
 334       * Obtains current indeterminate progress in a way suitable for adding to
 335       * the progress display.
 336       *
 337       * This returns the number of indeterminate calls (at any level) during the
 338       * lifetime of this progress reporter, whether or not there is a current
 339       * indeterminate step. (The number will not be ridiculously high because
 340       * progress calls are limited to one per second.)
 341       *
 342       * @return int Number of indeterminate progress calls
 343       */
 344      public function get_progress_count() {
 345          return $this->count;
 346      }
 347  }