/lib/spout/src/Spout/Reader/Common/XMLProcessor.php

https://github.com/markn86/moodle · PHP · 148 lines · 71 code · 22 blank · 55 comment · 5 complexity · 618be7033676fd5eb089cfb88096aebb MD5 · raw file

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