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