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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body