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  
   3  namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
   4  
   5  use PhpOffice\PhpSpreadsheet\Calculation\Exception;
   6  
   7  class BranchPruner
   8  {
   9      /**
  10       * @var bool
  11       */
  12      protected $branchPruningEnabled = true;
  13  
  14      /**
  15       * Used to generate unique store keys.
  16       *
  17       * @var int
  18       */
  19      private $branchStoreKeyCounter = 0;
  20  
  21      /**
  22       * currently pending storeKey (last item of the storeKeysStack.
  23       *
  24       * @var ?string
  25       */
  26      protected $pendingStoreKey;
  27  
  28      /**
  29       * @var string[]
  30       */
  31      protected $storeKeysStack = [];
  32  
  33      /**
  34       * @var bool[]
  35       */
  36      protected $conditionMap = [];
  37  
  38      /**
  39       * @var bool[]
  40       */
  41      protected $thenMap = [];
  42  
  43      /**
  44       * @var bool[]
  45       */
  46      protected $elseMap = [];
  47  
  48      /**
  49       * @var int[]
  50       */
  51      protected $braceDepthMap = [];
  52  
  53      /**
  54       * @var null|string
  55       */
  56      protected $currentCondition;
  57  
  58      /**
  59       * @var null|string
  60       */
  61      protected $currentOnlyIf;
  62  
  63      /**
  64       * @var null|string
  65       */
  66      protected $currentOnlyIfNot;
  67  
  68      /**
  69       * @var null|string
  70       */
  71      protected $previousStoreKey;
  72  
  73      public function __construct(bool $branchPruningEnabled)
  74      {
  75          $this->branchPruningEnabled = $branchPruningEnabled;
  76      }
  77  
  78      public function clearBranchStore(): void
  79      {
  80          $this->branchStoreKeyCounter = 0;
  81      }
  82  
  83      public function initialiseForLoop(): void
  84      {
  85          $this->currentCondition = null;
  86          $this->currentOnlyIf = null;
  87          $this->currentOnlyIfNot = null;
  88          $this->previousStoreKey = null;
  89          $this->pendingStoreKey = empty($this->storeKeysStack) ? null : end($this->storeKeysStack);
  90  
  91          if ($this->branchPruningEnabled) {
  92              $this->initialiseCondition();
  93              $this->initialiseThen();
  94              $this->initialiseElse();
  95          }
  96      }
  97  
  98      private function initialiseCondition(): void
  99      {
 100          if (isset($this->conditionMap[$this->pendingStoreKey]) && $this->conditionMap[$this->pendingStoreKey]) {
 101              $this->currentCondition = $this->pendingStoreKey;
 102              $stackDepth = count($this->storeKeysStack);
 103              if ($stackDepth > 1) {
 104                  // nested if
 105                  $this->previousStoreKey = $this->storeKeysStack[$stackDepth - 2];
 106              }
 107          }
 108      }
 109  
 110      private function initialiseThen(): void
 111      {
 112          if (isset($this->thenMap[$this->pendingStoreKey]) && $this->thenMap[$this->pendingStoreKey]) {
 113              $this->currentOnlyIf = $this->pendingStoreKey;
 114          } elseif (
 115              isset($this->previousStoreKey, $this->thenMap[$this->previousStoreKey])
 116              && $this->thenMap[$this->previousStoreKey]
 117          ) {
 118              $this->currentOnlyIf = $this->previousStoreKey;
 119          }
 120      }
 121  
 122      private function initialiseElse(): void
 123      {
 124          if (isset($this->elseMap[$this->pendingStoreKey]) && $this->elseMap[$this->pendingStoreKey]) {
 125              $this->currentOnlyIfNot = $this->pendingStoreKey;
 126          } elseif (
 127              isset($this->previousStoreKey, $this->elseMap[$this->previousStoreKey])
 128              && $this->elseMap[$this->previousStoreKey]
 129          ) {
 130              $this->currentOnlyIfNot = $this->previousStoreKey;
 131          }
 132      }
 133  
 134      public function decrementDepth(): void
 135      {
 136          if (!empty($this->pendingStoreKey)) {
 137              --$this->braceDepthMap[$this->pendingStoreKey];
 138          }
 139      }
 140  
 141      public function incrementDepth(): void
 142      {
 143          if (!empty($this->pendingStoreKey)) {
 144              ++$this->braceDepthMap[$this->pendingStoreKey];
 145          }
 146      }
 147  
 148      public function functionCall(string $functionName): void
 149      {
 150          if ($this->branchPruningEnabled && ($functionName === 'IF(')) {
 151              // we handle a new if
 152              $this->pendingStoreKey = $this->getUnusedBranchStoreKey();
 153              $this->storeKeysStack[] = $this->pendingStoreKey;
 154              $this->conditionMap[$this->pendingStoreKey] = true;
 155              $this->braceDepthMap[$this->pendingStoreKey] = 0;
 156          } elseif (!empty($this->pendingStoreKey) && array_key_exists($this->pendingStoreKey, $this->braceDepthMap)) {
 157              // this is not an if but we go deeper
 158              ++$this->braceDepthMap[$this->pendingStoreKey];
 159          }
 160      }
 161  
 162      public function argumentSeparator(): void
 163      {
 164          if (!empty($this->pendingStoreKey) && $this->braceDepthMap[$this->pendingStoreKey] === 0) {
 165              // We must go to the IF next argument
 166              if ($this->conditionMap[$this->pendingStoreKey]) {
 167                  $this->conditionMap[$this->pendingStoreKey] = false;
 168                  $this->thenMap[$this->pendingStoreKey] = true;
 169              } elseif ($this->thenMap[$this->pendingStoreKey]) {
 170                  $this->thenMap[$this->pendingStoreKey] = false;
 171                  $this->elseMap[$this->pendingStoreKey] = true;
 172              } elseif ($this->elseMap[$this->pendingStoreKey]) {
 173                  throw new Exception('Reaching fourth argument of an IF');
 174              }
 175          }
 176      }
 177  
 178      /**
 179       * @param mixed $value
 180       */
 181      public function closingBrace($value): void
 182      {
 183          if (!empty($this->pendingStoreKey) && $this->braceDepthMap[$this->pendingStoreKey] === -1) {
 184              // we are closing an IF(
 185              if ($value !== 'IF(') {
 186                  throw new Exception('Parser bug we should be in an "IF("');
 187              }
 188  
 189              if ($this->conditionMap[$this->pendingStoreKey]) {
 190                  throw new Exception('We should not be expecting a condition');
 191              }
 192  
 193              $this->thenMap[$this->pendingStoreKey] = false;
 194              $this->elseMap[$this->pendingStoreKey] = false;
 195              --$this->braceDepthMap[$this->pendingStoreKey];
 196              array_pop($this->storeKeysStack);
 197              $this->pendingStoreKey = null;
 198          }
 199      }
 200  
 201      public function currentCondition(): ?string
 202      {
 203          return $this->currentCondition;
 204      }
 205  
 206      public function currentOnlyIf(): ?string
 207      {
 208          return $this->currentOnlyIf;
 209      }
 210  
 211      public function currentOnlyIfNot(): ?string
 212      {
 213          return $this->currentOnlyIfNot;
 214      }
 215  
 216      private function getUnusedBranchStoreKey(): string
 217      {
 218          $storeKeyValue = 'storeKey-' . $this->branchStoreKeyCounter;
 219          ++$this->branchStoreKeyCounter;
 220  
 221          return $storeKeyValue;
 222      }
 223  }