Completed
Push — master ( 73d5d0...a19231 )
by Adrien
02:53
created

XMLProcessor::invokeCallback()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 2
crap 1
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 174
    public function __construct($xmlReader)
39
    {
40 174
        $this->xmlReader = $xmlReader;
41 174
    }
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 174
    public function registerCallback($nodeName, $nodeType, $callback)
50
    {
51 174
        $callbackKey = $this->getCallbackKey($nodeName, $nodeType);
52 174
        $this->callbacks[$callbackKey] = $this->getInvokableCallbackData($callback);
53
54 174
        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 174
    private function getCallbackKey($nodeName, $nodeType)
63
    {
64 174
        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 174
    private function getInvokableCallbackData($callback)
77
    {
78 174
        $callbackObject = $callback[0];
79 174
        $callbackMethodName = $callback[1];
80 174
        $reflectionMethod = new \ReflectionMethod(get_class($callbackObject), $callbackMethodName);
81 174
        $reflectionMethod->setAccessible(true);
82
83
        return [
84 174
            self::CALLBACK_REFLECTION_METHOD => $reflectionMethod,
85 174
            self::CALLBACK_REFLECTION_OBJECT => $callbackObject,
86 174
        ];
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 168
    public function readUntilStopped()
97
    {
98 168
        while ($this->xmlReader->read()) {
99 168
            $nodeType = $this->xmlReader->nodeType;
100 168
            $nodeNamePossiblyWithPrefix = $this->xmlReader->name;
101 168
            $nodeNameWithoutPrefix = $this->xmlReader->localName;
102
103 168
            $callbackData = $this->getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType);
104
105 168
            if ($callbackData !== null) {
106 168
                $callbackResponse = $this->invokeCallback($callbackData, [$this->xmlReader]);
107
108 168
                if ($callbackResponse === self::PROCESSING_STOP) {
109
                    // stop reading
110 168
                    break;
111
                }
112 168
            }
113 168
        }
114 168
    }
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 168
    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 168
        $callbackKeyForPossiblyPrefixedName = $this->getCallbackKey($nodeNamePossiblyWithPrefix, $nodeType);
128 168
        $callbackKeyForUnPrefixedName = $this->getCallbackKey($nodeNameWithoutPrefix, $nodeType);
129 168
        $hasPrefix = ($nodeNamePossiblyWithPrefix !== $nodeNameWithoutPrefix);
130
131 168
        $callbackKeyToUse = $callbackKeyForUnPrefixedName;
132 168
        if ($hasPrefix && isset($this->callbacks[$callbackKeyForPossiblyPrefixedName])) {
133 78
            $callbackKeyToUse = $callbackKeyForPossiblyPrefixedName;
134 78
        }
135
136
        // Using isset here because it is way faster than array_key_exists...
137 168
        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 168
    private function invokeCallback($callbackData, $args)
146
    {
147 168
        $reflectionMethod = $callbackData[self::CALLBACK_REFLECTION_METHOD];
148 168
        $callbackObject = $callbackData[self::CALLBACK_REFLECTION_OBJECT];
149
150 168
        return $reflectionMethod->invokeArgs($callbackObject, $args);
151
    }
152
}
153