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 310 and 401] [Versions 39 and 401]

   1  <?php
   2  
   3  namespace Box\Spout\Reader\Common;
   4  
   5  use Box\Spout\Reader\Wrapper\XMLReader;
   6  
   7  /**
   8   * Class XMLProcessor
   9   * Helps process XML files
  10   */
  11  class XMLProcessor
  12  {
  13      /* Node types */
  14      const NODE_TYPE_START = XMLReader::ELEMENT;
  15      const NODE_TYPE_END = XMLReader::END_ELEMENT;
  16  
  17      /* Keys associated to reflection attributes to invoke a callback */
  18      const CALLBACK_REFLECTION_METHOD = 'reflectionMethod';
  19      const CALLBACK_REFLECTION_OBJECT = 'reflectionObject';
  20  
  21      /* Values returned by the callbacks to indicate what the processor should do next */
  22      const PROCESSING_CONTINUE = 1;
  23      const PROCESSING_STOP = 2;
  24  
  25      /** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
  26      protected $xmlReader;
  27  
  28      /** @var array Registered callbacks */
  29      private $callbacks = [];
  30  
  31      /**
  32       * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object
  33       */
  34      public function __construct($xmlReader)
  35      {
  36          $this->xmlReader = $xmlReader;
  37      }
  38  
  39      /**
  40       * @param string $nodeName A callback may be triggered when a node with this name is read
  41       * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
  42       * @param callable $callback Callback to execute when the read node has the given name and type
  43       * @return XMLProcessor
  44       */
  45      public function registerCallback($nodeName, $nodeType, $callback)
  46      {
  47          $callbackKey = $this->getCallbackKey($nodeName, $nodeType);
  48          $this->callbacks[$callbackKey] = $this->getInvokableCallbackData($callback);
  49  
  50          return $this;
  51      }
  52  
  53      /**
  54       * @param string $nodeName Name of the node
  55       * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
  56       * @return string Key used to store the associated callback
  57       */
  58      private function getCallbackKey($nodeName, $nodeType)
  59      {
  60          return "$nodeName$nodeType";
  61      }
  62  
  63      /**
  64       * Because the callback can be a "protected" function, we don't want to use call_user_func() directly
  65       * but instead invoke the callback using Reflection. This allows the invocation of "protected" functions.
  66       * Since some functions can be called a lot, we pre-process the callback to only return the elements that
  67       * will be needed to invoke the callback later.
  68       *
  69       * @param callable $callback Array reference to a callback: [OBJECT, METHOD_NAME]
  70       * @return array Associative array containing the elements needed to invoke the callback using Reflection
  71       */
  72      private function getInvokableCallbackData($callback)
  73      {
  74          $callbackObject = $callback[0];
  75          $callbackMethodName = $callback[1];
  76          $reflectionMethod = new \ReflectionMethod(\get_class($callbackObject), $callbackMethodName);
  77          $reflectionMethod->setAccessible(true);
  78  
  79          return [
  80              self::CALLBACK_REFLECTION_METHOD => $reflectionMethod,
  81              self::CALLBACK_REFLECTION_OBJECT => $callbackObject,
  82          ];
  83      }
  84  
  85      /**
  86       * Resumes the reading of the XML file where it was left off.
  87       * Stops whenever a callback indicates that reading should stop or at the end of the file.
  88       *
  89       * @throws \Box\Spout\Reader\Exception\XMLProcessingException
  90       * @return void
  91       */
  92      public function readUntilStopped()
  93      {
  94          while ($this->xmlReader->read()) {
  95              $nodeType = $this->xmlReader->nodeType;
  96              $nodeNamePossiblyWithPrefix = $this->xmlReader->name;
  97              $nodeNameWithoutPrefix = $this->xmlReader->localName;
  98  
  99              $callbackData = $this->getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType);
 100  
 101              if ($callbackData !== null) {
 102                  $callbackResponse = $this->invokeCallback($callbackData, [$this->xmlReader]);
 103  
 104                  if ($callbackResponse === self::PROCESSING_STOP) {
 105                      // stop reading
 106                      break;
 107                  }
 108              }
 109          }
 110      }
 111  
 112      /**
 113       * @param string $nodeNamePossiblyWithPrefix Name of the node, possibly prefixed
 114       * @param string $nodeNameWithoutPrefix Name of the same node, un-prefixed
 115       * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
 116       * @return array|null Callback data to be used for execution when a node of the given name/type is read or NULL if none found
 117       */
 118      private function getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType)
 119      {
 120          // With prefixed nodes, we should match if (by order of preference):
 121          //  1. the callback was registered with the prefixed node name (e.g. "x:worksheet")
 122          //  2. the callback was registered with the un-prefixed node name (e.g. "worksheet")
 123          $callbackKeyForPossiblyPrefixedName = $this->getCallbackKey($nodeNamePossiblyWithPrefix, $nodeType);
 124          $callbackKeyForUnPrefixedName = $this->getCallbackKey($nodeNameWithoutPrefix, $nodeType);
 125          $hasPrefix = ($nodeNamePossiblyWithPrefix !== $nodeNameWithoutPrefix);
 126  
 127          $callbackKeyToUse = $callbackKeyForUnPrefixedName;
 128          if ($hasPrefix && isset($this->callbacks[$callbackKeyForPossiblyPrefixedName])) {
 129              $callbackKeyToUse = $callbackKeyForPossiblyPrefixedName;
 130          }
 131  
 132          // Using isset here because it is way faster than array_key_exists...
 133          return isset($this->callbacks[$callbackKeyToUse]) ? $this->callbacks[$callbackKeyToUse] : null;
 134      }
 135  
 136      /**
 137       * @param array $callbackData Associative array containing data to invoke the callback using Reflection
 138       * @param array $args Arguments to pass to the callback
 139       * @return int Callback response
 140       */
 141      private function invokeCallback($callbackData, $args)
 142      {
 143          $reflectionMethod = $callbackData[self::CALLBACK_REFLECTION_METHOD];
 144          $callbackObject = $callbackData[self::CALLBACK_REFLECTION_OBJECT];
 145  
 146          return $reflectionMethod->invokeArgs($callbackObject, $args);
 147      }
 148  }