Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
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
}