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