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