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