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 310 and 402] [Versions 39 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  /**
  18   * Class that holds a tree of availability conditions.
  19   *
  20   * @package core_availability
  21   * @copyright 2014 The Open University
  22   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_availability;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * Class that holds a tree of availability conditions.
  31   *
  32   * The structure of this tree in JSON input data is:
  33   *
  34   * { op:'&', c:[] }
  35   *
  36   * where 'op' is one of the OP_xx constants and 'c' is an array of children.
  37   *
  38   * At the root level one of the following additional values must be included:
  39   *
  40   * op '|' or '!&'
  41   *   show:true
  42   *   Boolean value controlling whether a failed match causes the item to
  43   *   display to students with information, or be completely hidden.
  44   * op '&' or '!|'
  45   *   showc:[]
  46   *   Array of same length as c with booleans corresponding to each child; you
  47   *   can make it be hidden or shown depending on which one they fail. (Anything
  48   *   with false takes precedence.)
  49   *
  50   * @package core_availability
  51   * @copyright 2014 The Open University
  52   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  53   */
  54  class tree extends tree_node {
  55      /** @var int Operator: AND */
  56      const OP_AND = '&';
  57      /** @var int Operator: OR */
  58      const OP_OR = '|';
  59      /** @var int Operator: NOT(AND) */
  60      const OP_NOT_AND = '!&';
  61      /** @var int Operator: NOT(OR) */
  62      const OP_NOT_OR = '!|';
  63  
  64      /** @var bool True if this tree is at root level */
  65      protected $root;
  66  
  67      /** @var string Operator type (OP_xx constant) */
  68      protected $op;
  69  
  70      /** @var tree_node[] Children in this branch (may be empty array if needed) */
  71      protected $children;
  72  
  73      /**
  74       * Array of 'show information or hide completely' options for each child.
  75       * This array is only set for the root tree if it is in AND or NOT OR mode,
  76       * otherwise it is null.
  77       *
  78       * @var bool[]
  79       */
  80      protected $showchildren;
  81  
  82      /**
  83       * Single 'show information or hide completely' option for tree. This option
  84       * is only set for the root tree if it is in OR or NOT AND mode, otherwise
  85       * it is true.
  86       *
  87       * @var bool
  88       */
  89      protected $show;
  90  
  91      /**
  92       * Display a representation of this tree (used for debugging).
  93       *
  94       * @return string Text representation of tree
  95       */
  96      public function __toString() {
  97          $result = '';
  98          if ($this->root && is_null($this->showchildren)) {
  99              $result .= $this->show ? '+' : '-';
 100          }
 101          $result .= $this->op . '(';
 102          $first = true;
 103          foreach ($this->children as $index => $child) {
 104              if ($first) {
 105                  $first = false;
 106              } else {
 107                  $result .= ',';
 108              }
 109              if (!is_null($this->showchildren)) {
 110                  $result .= $this->showchildren[$index] ? '+' : '-';
 111              }
 112              $result .= (string)$child;
 113          }
 114          $result .= ')';
 115          return $result;
 116      }
 117  
 118      /**
 119       * Decodes availability structure.
 120       *
 121       * This function also validates the retrieved data as follows:
 122       * 1. Data that does not meet the API-defined structure causes a
 123       *    coding_exception (this should be impossible unless there is
 124       *    a system bug or somebody manually hacks the database).
 125       * 2. Data that meets the structure but cannot be implemented (e.g.
 126       *    reference to missing plugin or to module that doesn't exist) is
 127       *    either silently discarded (if $lax is true) or causes a
 128       *    coding_exception (if $lax is false).
 129       *
 130       * @see decode_availability
 131       * @param \stdClass $structure Structure (decoded from JSON)
 132       * @param boolean $lax If true, throw exceptions only for invalid structure
 133       * @param boolean $root If true, this is the root tree
 134       * @return tree Availability tree
 135       * @throws \coding_exception If data is not valid structure
 136       */
 137      public function __construct($structure, $lax = false, $root = true) {
 138          $this->root = $root;
 139  
 140          // Check object.
 141          if (!is_object($structure)) {
 142              throw new \coding_exception('Invalid availability structure (not object)');
 143          }
 144  
 145          // Extract operator.
 146          if (!isset($structure->op)) {
 147              throw new \coding_exception('Invalid availability structure (missing ->op)');
 148          }
 149          $this->op = $structure->op;
 150          if (!in_array($this->op, array(self::OP_AND, self::OP_OR,
 151                  self::OP_NOT_AND, self::OP_NOT_OR), true)) {
 152              throw new \coding_exception('Invalid availability structure (unknown ->op)');
 153          }
 154  
 155          // For root tree, get show options.
 156          $this->show = true;
 157          $this->showchildren = null;
 158          if ($root) {
 159              if ($this->op === self::OP_AND || $this->op === self::OP_NOT_OR) {
 160                  // Per-child show options.
 161                  if (!isset($structure->showc)) {
 162                      throw new \coding_exception(
 163                              'Invalid availability structure (missing ->showc)');
 164                  }
 165                  if (!is_array($structure->showc)) {
 166                      throw new \coding_exception(
 167                              'Invalid availability structure (->showc not array)');
 168                  }
 169                  foreach ($structure->showc as $value) {
 170                      if (!is_bool($value)) {
 171                          throw new \coding_exception(
 172                                  'Invalid availability structure (->showc value not bool)');
 173                      }
 174                  }
 175                  // Set it empty now - add corresponding ones later.
 176                  $this->showchildren = array();
 177              } else {
 178                  // Entire tree show option. (Note: This is because when you use
 179                  // OR mode, say you have A OR B, the user does not meet conditions
 180                  // for either A or B. A is set to 'show' and B is set to 'hide'.
 181                  // But they don't have either, so how do we know which one to do?
 182                  // There might as well be only one value.)
 183                  if (!isset($structure->show)) {
 184                      throw new \coding_exception(
 185                              'Invalid availability structure (missing ->show)');
 186                  }
 187                  if (!is_bool($structure->show)) {
 188                      throw new \coding_exception(
 189                              'Invalid availability structure (->show not bool)');
 190                  }
 191                  $this->show = $structure->show;
 192              }
 193          }
 194  
 195          // Get list of enabled plugins.
 196          $pluginmanager = \core_plugin_manager::instance();
 197          $enabled = $pluginmanager->get_enabled_plugins('availability');
 198  
 199          // For unit tests, also allow the mock plugin type (even though it
 200          // isn't configured in the code as a proper plugin).
 201          if (PHPUNIT_TEST) {
 202              $enabled['mock'] = true;
 203          }
 204  
 205          // Get children.
 206          if (!isset($structure->c)) {
 207              throw new \coding_exception('Invalid availability structure (missing ->c)');
 208          }
 209          if (!is_array($structure->c)) {
 210              throw new \coding_exception('Invalid availability structure (->c not array)');
 211          }
 212          if (is_array($this->showchildren) && count($structure->showc) != count($structure->c)) {
 213              throw new \coding_exception('Invalid availability structure (->c, ->showc mismatch)');
 214          }
 215          $this->children = array();
 216          foreach ($structure->c as $index => $child) {
 217              if (!is_object($child)) {
 218                  throw new \coding_exception('Invalid availability structure (child not object)');
 219              }
 220  
 221              // First see if it's a condition. These have a defined type.
 222              if (isset($child->type)) {
 223                  // Look for a plugin of this type.
 224                  $classname = '\availability_' . $child->type . '\condition';
 225                  if (!array_key_exists($child->type, $enabled)) {
 226                      if ($lax) {
 227                          // On load of existing settings, ignore if class
 228                          // doesn't exist.
 229                          continue;
 230                      } else {
 231                          throw new \coding_exception('Unknown condition type: ' . $child->type);
 232                      }
 233                  }
 234                  $this->children[] = new $classname($child);
 235              } else {
 236                  // Not a condition. Must be a subtree.
 237                  $this->children[] = new tree($child, $lax, false);
 238              }
 239              if (!is_null($this->showchildren)) {
 240                  $this->showchildren[] = $structure->showc[$index];
 241              }
 242          }
 243      }
 244  
 245      public function check_available($not, info $info, $grabthelot, $userid) {
 246          // If there are no children in this group, we just treat it as available.
 247          $information = '';
 248          if (!$this->children) {
 249              return new result(true);
 250          }
 251  
 252          // Get logic flags from operator.
 253          list($innernot, $andoperator) = $this->get_logic_flags($not);
 254  
 255          if ($andoperator) {
 256              $allow = true;
 257          } else {
 258              $allow = false;
 259          }
 260          $failedchildren = array();
 261          $totallyhide = !$this->show;
 262          foreach ($this->children as $index => $child) {
 263              // Check available and get info.
 264              $childresult = $child->check_available(
 265                      $innernot, $info, $grabthelot, $userid);
 266              $childyes = $childresult->is_available();
 267              if (!$childyes) {
 268                  $failedchildren[] = $childresult;
 269                  if (!is_null($this->showchildren) && !$this->showchildren[$index]) {
 270                      $totallyhide = true;
 271                  }
 272              }
 273  
 274              if ($andoperator && !$childyes) {
 275                  $allow = false;
 276                  // Do not exit loop at this point, as we will still include other info.
 277              } else if (!$andoperator && $childyes) {
 278                  // Exit loop since we are going to allow access (from this tree at least).
 279                  $allow = true;
 280                  break;
 281              }
 282          }
 283  
 284          if ($allow) {
 285              return new result(true);
 286          } else if ($totallyhide) {
 287              return new result(false);
 288          } else {
 289              return new result(false, $this, $failedchildren);
 290          }
 291      }
 292  
 293      public function is_applied_to_user_lists() {
 294          return true;
 295      }
 296  
 297      /**
 298       * Tests against a user list. Users who cannot access the activity due to
 299       * availability restrictions will be removed from the list.
 300       *
 301       * This test ONLY includes conditions which are marked as being applied to
 302       * user lists. For example, group conditions are included but date
 303       * conditions are not included.
 304       *
 305       * The function operates reasonably efficiently i.e. should not do per-user
 306       * database queries. It is however likely to be fairly slow.
 307       *
 308       * @param array $users Array of userid => object
 309       * @param bool $not If tree's parent indicates it's being checked negatively
 310       * @param info $info Info about current context
 311       * @param capability_checker $checker Capability checker
 312       * @return array Filtered version of input array
 313       */
 314      public function filter_user_list(array $users, $not, info $info,
 315              capability_checker $checker) {
 316          // Get logic flags from operator.
 317          list($innernot, $andoperator) = $this->get_logic_flags($not);
 318  
 319          if ($andoperator) {
 320              // For AND, start with the whole result and whittle it down.
 321              $result = $users;
 322          } else {
 323              // For OR, start with nothing.
 324              $result = array();
 325              $anyconditions = false;
 326          }
 327  
 328          // Loop through all valid children.
 329          foreach ($this->children as $index => $child) {
 330              if (!$child->is_applied_to_user_lists()) {
 331                  if ($andoperator) {
 332                      continue;
 333                  } else {
 334                      // OR condition with one option that doesn't restrict user
 335                      // lists = everyone is allowed.
 336                      $anyconditions = false;
 337                      break;
 338                  }
 339              }
 340              $childresult = $child->filter_user_list($users, $innernot, $info, $checker);
 341              if ($andoperator) {
 342                  $result = array_intersect_key($result, $childresult);
 343              } else {
 344                  // Combine results into array.
 345                  foreach ($childresult as $id => $user) {
 346                      $result[$id] = $user;
 347                  }
 348                  $anyconditions = true;
 349              }
 350          }
 351  
 352          // For OR operator, if there were no conditions just return input.
 353          if (!$andoperator && !$anyconditions) {
 354              return $users;
 355          } else {
 356              return $result;
 357          }
 358      }
 359  
 360      public function get_user_list_sql($not, info $info, $onlyactive) {
 361          global $DB;
 362          // Get logic flags from operator.
 363          list($innernot, $andoperator) = $this->get_logic_flags($not);
 364  
 365          // Loop through all valid children, getting SQL for each.
 366          $childresults = array();
 367          foreach ($this->children as $index => $child) {
 368              if (!$child->is_applied_to_user_lists()) {
 369                  if ($andoperator) {
 370                      continue;
 371                  } else {
 372                      // OR condition with one option that doesn't restrict user
 373                      // lists = everyone is allowed.
 374                      $childresults = array();
 375                      break;
 376                  }
 377              }
 378              $childresult = $child->get_user_list_sql($innernot, $info, $onlyactive);
 379              if ($childresult[0]) {
 380                  $childresults[] = $childresult;
 381              } else if (!$andoperator) {
 382                  // When using OR operator, if any part doesn't have restrictions,
 383                  // then nor does the whole thing.
 384                  return array('', array());
 385              }
 386          }
 387  
 388          // If there are no conditions, return null.
 389          if (!$childresults) {
 390              return array('', array());
 391          }
 392          // If there is a single condition, return it.
 393          if (count($childresults) === 1) {
 394              return $childresults[0];
 395          }
 396  
 397          // Combine results using INTERSECT or UNION.
 398          $outsql = null;
 399          $subsql = array();
 400          $outparams = array();
 401          foreach ($childresults as $childresult) {
 402              $subsql[] = $childresult[0];
 403              $outparams = array_merge($outparams, $childresult[1]);
 404          }
 405          if ($andoperator) {
 406              $outsql = $DB->sql_intersect($subsql, 'id');
 407          } else {
 408              $outsql = '(' . join(') UNION (', $subsql) . ')';
 409          }
 410          return array($outsql, $outparams);
 411      }
 412  
 413      public function is_available_for_all($not = false) {
 414          // Get logic flags.
 415          list($innernot, $andoperator) = $this->get_logic_flags($not);
 416  
 417          // No children = always available.
 418          if (!$this->children) {
 419              return true;
 420          }
 421  
 422          // Check children.
 423          foreach ($this->children as $child) {
 424              $innerall = $child->is_available_for_all($innernot);
 425              if ($andoperator) {
 426                  // When there is an AND operator, then any child that results
 427                  // in unavailable status would cause the whole thing to be
 428                  // unavailable.
 429                  if (!$innerall) {
 430                      return false;
 431                  }
 432              } else {
 433                  // When there is an OR operator, then any child which must only
 434                  // be available means the whole thing must be available.
 435                  if ($innerall) {
 436                      return true;
 437                  }
 438              }
 439          }
 440  
 441          // If we get to here then for an AND operator that means everything must
 442          // be available. From OR it means that everything must be possibly
 443          // not available.
 444          return $andoperator;
 445      }
 446  
 447      /**
 448       * Gets full information about this tree (including all children) as HTML
 449       * for display to staff.
 450       *
 451       * @param info $info Information about location of condition tree
 452       * @throws \coding_exception If you call on a non-root tree
 453       * @return string HTML data (empty string if none)
 454       */
 455      public function get_full_information(info $info) {
 456          if (!$this->root) {
 457              throw new \coding_exception('Only supported on root item');
 458          }
 459          return $this->get_full_information_recursive(false, $info, null, true);
 460      }
 461  
 462      /**
 463       * Gets information about this tree corresponding to the given result
 464       * object. (In other words, only conditions which the student actually
 465       * fails will be shown - and nothing if display is turned off.)
 466       *
 467       * @param info $info Information about location of condition tree
 468       * @param result $result Result object
 469       * @throws \coding_exception If you call on a non-root tree
 470       * @return string HTML data (empty string if none)
 471       */
 472      public function get_result_information(info $info, result $result) {
 473          if (!$this->root) {
 474              throw new \coding_exception('Only supported on root item');
 475          }
 476          return $this->get_full_information_recursive(false, $info, $result, true);
 477      }
 478  
 479      /**
 480       * Gets information about this tree (including all or selected children) as
 481       * HTML for display to staff or student.
 482       *
 483       * @param bool $not True if there is a NOT in effect
 484       * @param info $info Information about location of condition tree
 485       * @param result|null $result Result object if this is a student display, else null
 486       * @param bool $root True if this is the root item
 487       * @param bool $hidden Staff display; true if this tree has show=false (from parent)
 488       * @return string|renderable Information to render
 489       */
 490      protected function get_full_information_recursive(
 491              $not, info $info, ?result $result, $root, $hidden = false) {
 492          // Get list of children - either full list, or those which are shown.
 493          $children = $this->children;
 494          $staff = true;
 495          if ($result) {
 496              $children = $result->filter_nodes($children);
 497              $staff = false;
 498          }
 499  
 500          // If no children, return empty string.
 501          if (!$children) {
 502              return '';
 503          }
 504  
 505          list($innernot, $andoperator) = $this->get_logic_flags($not);
 506  
 507          // If there is only one child, don't bother displaying this tree
 508          // (AND and OR makes no difference). Recurse to the child if a tree,
 509          // otherwise display directly.
 510          if (count ($children) === 1) {
 511              $child = reset($children);
 512              if ($this->root && is_null($result)) {
 513                  if (is_null($this->showchildren)) {
 514                      $childhidden = !$this->show;
 515                  } else {
 516                      $childhidden = !$this->showchildren[0];
 517                  }
 518              } else {
 519                  $childhidden = $hidden;
 520              }
 521              if ($child instanceof tree) {
 522                  return $child->get_full_information_recursive(
 523                          $innernot, $info, $result, $root, $childhidden);
 524              } else {
 525                  if ($root) {
 526                      $result = $child->get_standalone_description($staff, $innernot, $info);
 527                  } else {
 528                      $result = $child->get_description($staff, $innernot, $info);
 529                  }
 530                  if ($childhidden) {
 531                      $result .= ' ' . get_string('hidden_marker', 'availability');
 532                  }
 533                  return $result;
 534              }
 535          }
 536  
 537          // Multiple children, so prepare child messages (recursive).
 538          $items = array();
 539          $index = 0;
 540          foreach ($children as $child) {
 541              // Work out if this node is hidden (staff view only).
 542              $childhidden = $this->root && is_null($result) &&
 543                      !is_null($this->showchildren) && !$this->showchildren[$index];
 544              if ($child instanceof tree) {
 545                  $items[] = $child->get_full_information_recursive(
 546                          $innernot, $info, $result, false, $childhidden);
 547              } else {
 548                  $childdescription = $child->get_description($staff, $innernot, $info);
 549                  if ($childhidden) {
 550                      $childdescription .= ' ' . get_string('hidden_marker', 'availability');
 551                  }
 552                  $items[] = $childdescription;
 553              }
 554              $index++;
 555          }
 556  
 557          // If showing output to staff, and root is set to hide completely,
 558          // then include this information in the message.
 559          if ($this->root) {
 560              $treehidden = !$this->show && is_null($result);
 561          } else {
 562              $treehidden = $hidden;
 563          }
 564  
 565          // Format output for display.
 566          return new \core_availability_multiple_messages($root, $andoperator, $treehidden, $items);
 567      }
 568  
 569      /**
 570       * Converts the operator for the tree into two flags used for computing
 571       * the result.
 572       *
 573       * The 2 flags are $innernot (whether to set $not when calling for children)
 574       * and $andoperator (whether to use AND or OR operator to combine children).
 575       *
 576       * @param bool $not Not flag passed to this tree
 577       * @return array Array of the 2 flags ($innernot, $andoperator)
 578       */
 579      public function get_logic_flags($not) {
 580          // Work out which type of logic to use for the group.
 581          switch($this->op) {
 582              case self::OP_AND:
 583              case self::OP_OR:
 584                  $negative = false;
 585                  break;
 586              case self::OP_NOT_AND:
 587              case self::OP_NOT_OR:
 588                  $negative = true;
 589                  break;
 590              default:
 591                  throw new \coding_exception('Unknown operator');
 592          }
 593          switch($this->op) {
 594              case self::OP_AND:
 595              case self::OP_NOT_AND:
 596                  $andoperator = true;
 597                  break;
 598              case self::OP_OR:
 599              case self::OP_NOT_OR:
 600                  $andoperator = false;
 601                  break;
 602              default:
 603                  throw new \coding_exception('Unknown operator');
 604          }
 605  
 606          // Select NOT (or not) for children. It flips if this is a 'not' group.
 607          $innernot = $negative ? !$not : $not;
 608  
 609          // Select operator to use for this group. If flips for negative, because:
 610          // NOT (a AND b) = (NOT a) OR (NOT b)
 611          // NOT (a OR b) = (NOT a) AND (NOT b).
 612          if ($innernot) {
 613              $andoperator = !$andoperator;
 614          }
 615          return array($innernot, $andoperator);
 616      }
 617  
 618      public function save() {
 619          $result = new \stdClass();
 620          $result->op = $this->op;
 621          // Only root tree has the 'show' options.
 622          if ($this->root) {
 623              if ($this->op === self::OP_AND || $this->op === self::OP_NOT_OR) {
 624                  $result->showc = $this->showchildren;
 625              } else {
 626                  $result->show = $this->show;
 627              }
 628          }
 629          $result->c = array();
 630          foreach ($this->children as $child) {
 631              $result->c[] = $child->save();
 632          }
 633          return $result;
 634      }
 635  
 636      /**
 637       * Checks whether this tree is empty (contains no children).
 638       *
 639       * @return boolean True if empty
 640       */
 641      public function is_empty() {
 642          return count($this->children) === 0;
 643      }
 644  
 645      /**
 646       * Recursively gets all children of a particular class (you can use a base
 647       * class to get all conditions, or a specific class).
 648       *
 649       * @param string $classname Full class name e.g. core_availability\condition
 650       * @return array Array of nodes of that type (flattened, not a tree any more)
 651       */
 652      public function get_all_children($classname) {
 653          $result = array();
 654          $this->recursive_get_all_children($classname, $result);
 655          return $result;
 656      }
 657  
 658      /**
 659       * Internal function that implements get_all_children efficiently.
 660       *
 661       * @param string $classname Full class name e.g. core_availability\condition
 662       * @param array $result Output array of nodes
 663       */
 664      protected function recursive_get_all_children($classname, array &$result) {
 665          foreach ($this->children as $child) {
 666              if (is_a($child, $classname)) {
 667                  $result[] = $child;
 668              }
 669              if ($child instanceof tree) {
 670                  $child->recursive_get_all_children($classname, $result);
 671              }
 672          }
 673      }
 674  
 675      public function update_after_restore($restoreid, $courseid,
 676              \base_logger $logger, $name) {
 677          $changed = false;
 678          foreach ($this->children as $index => $child) {
 679              if ($child->include_after_restore($restoreid, $courseid, $logger, $name,
 680                      info::get_restore_task($restoreid))) {
 681                  $thischanged = $child->update_after_restore($restoreid, $courseid,
 682                          $logger, $name);
 683                  $changed = $changed || $thischanged;
 684              } else {
 685                  unset($this->children[$index]);
 686                  unset($this->showchildren[$index]);
 687                  $this->showchildren = !is_null($this->showchildren) ? array_values($this->showchildren) : null;
 688                  $changed = true;
 689              }
 690          }
 691          return $changed;
 692      }
 693  
 694      public function update_dependency_id($table, $oldid, $newid) {
 695          $changed = false;
 696          foreach ($this->children as $child) {
 697              $thischanged = $child->update_dependency_id($table, $oldid, $newid);
 698              $changed = $changed || $thischanged;
 699          }
 700          return $changed;
 701      }
 702  
 703      /**
 704       * Returns a JSON object which corresponds to a tree.
 705       *
 706       * Intended for unit testing, as normally the JSON values are constructed
 707       * by JavaScript code.
 708       *
 709       * This function generates 'nested' (i.e. not root-level) trees.
 710       *
 711       * @param array $children Array of JSON objects from component children
 712       * @param string $op Operator (tree::OP_xx)
 713       * @return stdClass JSON object
 714       * @throws coding_exception If you get parameters wrong
 715       */
 716      public static function get_nested_json(array $children, $op = self::OP_AND) {
 717  
 718          // Check $op and work out its type.
 719          switch($op) {
 720              case self::OP_AND:
 721              case self::OP_NOT_OR:
 722              case self::OP_OR:
 723              case self::OP_NOT_AND:
 724                  break;
 725              default:
 726                  throw new \coding_exception('Invalid $op');
 727          }
 728  
 729          // Do simple tree.
 730          $result = new \stdClass();
 731          $result->op = $op;
 732          $result->c = $children;
 733          return $result;
 734      }
 735  
 736      /**
 737       * Returns a JSON object which corresponds to a tree at root level.
 738       *
 739       * Intended for unit testing, as normally the JSON values are constructed
 740       * by JavaScript code.
 741       *
 742       * The $show parameter can be a boolean for all OP_xx options. For OP_AND
 743       * and OP_NOT_OR where you have individual show options, you can specify
 744       * a boolean (same for all) or an array.
 745       *
 746       * @param array $children Array of JSON objects from component children
 747       * @param string $op Operator (tree::OP_xx)
 748       * @param bool|array $show Whether 'show' option is turned on (see above)
 749       * @return stdClass JSON object ready for encoding
 750       * @throws coding_exception If you get parameters wrong
 751       */
 752      public static function get_root_json(array $children, $op = self::OP_AND, $show = true) {
 753  
 754          // Get the basic object.
 755          $result = self::get_nested_json($children, $op);
 756  
 757          // Check $op type.
 758          switch($op) {
 759              case self::OP_AND:
 760              case self::OP_NOT_OR:
 761                  $multishow = true;
 762                  break;
 763              case self::OP_OR:
 764              case self::OP_NOT_AND:
 765                  $multishow = false;
 766                  break;
 767          }
 768  
 769          // Add show options depending on operator.
 770          if ($multishow) {
 771              if (is_bool($show)) {
 772                  $result->showc = array_pad(array(), count($result->c), $show);
 773              } else if (is_array($show)) {
 774                  // The JSON will break if anything isn't an actual bool, so check.
 775                  foreach ($show as $item) {
 776                      if (!is_bool($item)) {
 777                          throw new \coding_exception('$show array members must be bool');
 778                      }
 779                  }
 780                  // Check the size matches.
 781                  if (count($show) != count($result->c)) {
 782                      throw new \coding_exception('$show array size does not match $children');
 783                  }
 784                  $result->showc = $show;
 785              } else {
 786                  throw new \coding_exception('$show must be bool or array');
 787              }
 788          } else {
 789              if (!is_bool($show)) {
 790                  throw new \coding_exception('For this operator, $show must be bool');
 791              }
 792              $result->show = $show;
 793          }
 794  
 795          return $result;
 796      }
 797  }