Completed
Push — master ( c87337...703ea3 )
by
unknown
01:28
created

AbstractFrame::get()   B

Complexity

Conditions 8
Paths 3

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 8.4444
c 0
b 0
f 0
cc 8
nc 3
nop 1
1
<?php
2
3
/**
4
 * This file is part of the php-epp2 library.
5
 *
6
 * (c) Gunter Grodotzki <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE file
9
 * that was distributed with this source code.
10
 */
11
12
namespace AfriCC\EPP;
13
14
use DOMDocument;
15
use DOMXPath;
16
use Exception;
17
18
/**
19
 * This is a Frame implementation using DOMDocument that other Frames can
20
 * inherit from.
21
 */
22
abstract class AbstractFrame extends DOMDocument implements FrameInterface
23
{
24
    protected $xpath;
25
    /**
26
     * @var \DOMElement[]
27
     */
28
    protected $nodes;
29
    protected $format;
30
    protected $command;
31
    protected $mapping;
32
33
    /**
34
     * Extension name
35
     *
36
     * Redefine in case it's different than class name
37
     *
38
     * @var string
39
     */
40
    protected $extension;
41
42
    /**
43
     * Extension namespace
44
     *
45
     * Make sure to define it in class implementing ExtensionInterface
46
     *
47
     * @var string
48
     */
49
    protected $extension_xmlns;
50
    /**
51
     * @var bool whether to ignore command part when building realxpath
52
     */
53
    protected $ignore_command = false;
54
55
    /**
56
     * @var ObjectSpec custom objectspec used to create XML
57
     */
58
    protected $objectSpec;
59
60
    /**
61
     * Construct (with import if specified) frame
62
     *
63
     * Pass a DOMDocument instance to have it imported as a frame.
64
     * Pass an ObjectSpec instance to have it set as used ObjectSpec
65
     * More arguments will be ignored and only the last one will be used.
66
     */
67
    public function __construct()
68
    {
69
        parent::__construct('1.0', 'UTF-8');
70
        $this->xmlStandalone = false;
71
        $this->formatOutput = true;
72
73
        $import = null;
74
75
        $args = \func_get_args();
76
        foreach ($args as $arg) {
77
            if ($arg instanceof DOMDocument) {
78
                $import = $arg;
79
            }
80
            if ($arg instanceof ObjectSpec) {
81
                $this->objectSpec = $arg;
82
            }
83
        }
84
85
        if (\is_null($this->objectSpec)) {
86
            $this->objectSpec = new ObjectSpec();
87
        }
88
89
        $this->import($import);
90
91
        $this->registerXpath();
92
93
        $this->registerNodeClass('\DOMElement', '\AfriCC\EPP\DOM\DOMElement');
94
95
        $this->getStructure();
96
    }
97
98
    /**
99
     * Import frame data
100
     *
101
     * @param \DOMDocument $import
102
     */
103
    private function import(DOMDocument $import = null)
104
    {
105
        if (is_null($import)) {
106
            return;
107
        }
108
        $node = $this->importNode($import->documentElement, true);
109
        $this->appendChild($node);
110
    }
111
112
    private function registerXpath()
113
    {
114
        // register namespaces
115
        $this->xpath = new DOMXPath($this);
116
        foreach ($this->objectSpec->specs as $prefix => $spec) {
117
            $this->xpath->registerNamespace($prefix, $spec['xmlns']);
118
        }
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     *
124
     * @see \AfriCC\EPP\FrameInterface::set()
125
     */
126
    public function set($path = null, $value = null)
127
    {
128
        $path = $this->realxpath($path);
129
130
        if (!isset($this->nodes[$path])) {
131
            $path = $this->createNodes($path);
132
        }
133
134
        if ($value !== null) {
135
            $this->nodes[$path]->nodeValue = htmlspecialchars($value, ENT_XML1, 'UTF-8');
136
        }
137
138
        return $this->nodes[$path];
139
    }
140
141
    /**
142
     * {@inheritdoc}
143
     *
144
     * @see \AfriCC\EPP\FrameInterface::get()
145
     */
146
    public function get($query)
147
    {
148
        $nodes = $this->xpath->query($query);
149
        if ($nodes === null || $nodes->length === 0) {
150
            return false;
151
        }
152
153
        // try to figure out what type is being requested
154
        $last_bit = substr(strrchr($query, '/'), 1);
155
156
        // @see http://stackoverflow.com/a/24730245/567193
157
        if ($nodes->length === 1 && (
158
                ($last_bit[0] === '@' && $nodes->item(0)->nodeType === XML_ATTRIBUTE_NODE) ||
159
                (stripos($last_bit, 'text()') === 0 && $nodes->item(0)->nodeType === XML_TEXT_NODE)
160
            )) {
161
            return $nodes->item(0)->nodeValue;
162
        } else {
163
            return $nodes;
164
        }
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     *
170
     * @see \AfriCC\EPP\FrameInterface::__toString()
171
     */
172
    public function __toString()
173
    {
174
        return $this->saveXML();
175
    }
176
177
    /**
178
     * Create nodes specified by path
179
     *
180
     * @param string $path
181
     *
182
     * @throws Exception
183
     *
184
     * @return null|string
185
     */
186
    protected function createNodes($path)
187
    {
188
        $path_parts = explode('/', $path);
189
        $node_path = null;
190
191
        for ($i = 0, $limit = count($path_parts); $i < $limit; ++$i) {
192
            $attr_name = $attr_value = $matches = null;
193
194
            // if no namespace given, use root-namespace
195
            if (strpos($path_parts[$i], ':') === false) {
196
                $node_ns = 'epp';
197
                $node_name = $path_parts[$i];
198
                $path_parts[$i] = $node_ns . ':' . $node_name;
199
            } else {
200
                list($node_ns, $node_name) = explode(':', $path_parts[$i], 2);
201
            }
202
203
            // check for node-array
204
            if (substr($node_name, -2) === '[]') {
205
                $node_name = substr($node_name, 0, -2);
206
                // get next key
207
                $next_key = -1;
208
                foreach (array_keys($this->nodes) as $each) {
209
                    if (preg_match('/' . preg_quote($node_ns . ':' . $node_name, '/') . '\[(\d+)\]$/', $each, $matches)) {
210
                        if ($matches[1] > $next_key) {
211
                            $next_key = (int) $matches[1];
212
                        }
213
                    }
214
                }
215
                ++$next_key;
216
                $path_parts[$i] = sprintf('%s:%s[%d]', $node_ns, $node_name, $next_key);
217
            }
218
219
            if (preg_match('/^(.*)\[(\d+)\]$/', $node_name, $matches)) {
220
                // direct node-array access
221
                $node_name = $matches[1];
222
            } elseif (preg_match('/^(.*)\[@([a-z0-9]+)=\'([a-z0-9_]+)\'\]$/i', $node_name, $matches)) {
223
                // check if attribute needs to be set
224
                $node_name = $matches[1];
225
                $attr_name = $matches[2];
226
                $attr_value = $matches[3];
227
            }
228
229
            $node_path = implode('/', array_slice($path_parts, 0, $i + 1));
230
231
            if (isset($this->nodes[$node_path])) {
232
                continue;
233
            }
234
235
            // resolve node namespace
236
            $node_xmlns = $this->objectSpec->xmlns($node_ns);
237
            if ($node_xmlns === false) {
238
                throw new Exception(sprintf('unknown namespace: %s', $node_ns));
239
            }
240
241
            // create node (but don't explicitly define root-node)
242
            if ($node_ns === 'epp') {
243
                $this->nodes[$node_path] = $this->createElementNS($node_xmlns, $node_name);
244
            } else {
245
                $this->nodes[$node_path] = $this->createElementNS($node_xmlns, $node_ns . ':' . $node_name);
246
            }
247
248
            // set attribute
249
            if ($attr_name !== null && $attr_value !== null) {
250
                $this->nodes[$node_path]->setAttribute($attr_name, $attr_value);
251
            }
252
253
            // now append node to parent
254
            if ($i === 0) {
255
                $parent = $this;
256
            } else {
257
                $parent = $this->nodes[implode('/', array_slice($path_parts, 0, $i))];
258
            }
259
            $parent->appendChild($this->nodes[$node_path]);
260
        }
261
262
        return $node_path;
263
    }
264
265
    /**
266
     * Get Real XPath for provided path
267
     *
268
     * @param string $path
269
     *
270
     * @return string
271
     */
272
    protected function realxpath($path)
273
    {
274
        if ($path === null) {
275
            $path_parts = [];
276
        } elseif (isset($path[1]) && $path[0] === '/' && $path[1] === '/') {
277
            // absolute path
278
            return substr($path, 2);
279
        } else {
280
            $path_parts = explode('/', $path);
281
        }
282
283
        if (!empty($this->mapping) && !empty($this->command)) {
284
            array_unshift($path_parts, $this->mapping . ':' . $this->command);
285
        }
286
287
        if (!empty($this->command) && !$this->ignore_command) {
288
            array_unshift($path_parts, 'epp:' . $this->command);
289
        }
290
291
        if (!empty($this->format)) {
292
            array_unshift($path_parts, 'epp:' . $this->format);
293
        }
294
295
        array_unshift($path_parts, 'epp:epp');
296
297
        return implode('/', $path_parts);
298
    }
299
300
    private function getStructure()
301
    {
302
        // get class structure
303
        $classes = [get_class($this)];
304
        $classes = array_merge($classes, class_parents($this));
305
306
        foreach ($classes as $class) {
307
            $bare_class = $this->className($class);
308
309
            // stop when we reach self
310
            if ($bare_class === $this->className(__CLASS__)) {
311
                break;
312
            }
313
314
            // try to figure out the structure
315
            $parent_class = $this->className(get_parent_class($class));
316
            if ($parent_class === false) {
317
                continue;
318
            } elseif (empty($this->mapping) && in_array(strtolower($parent_class), $this->objectSpec->mappings)) {
319
                $this->mapping = strtolower($bare_class);
320
            } elseif (empty($this->command) && $parent_class === 'Command') {
321
                $this->command = strtolower($bare_class);
322
            } elseif ($parent_class === 'AbstractFrame') {
323
                $this->format = strtolower($bare_class);
324
            }
325
        }
326
327
        if ($this instanceof ExtensionInterface) {
328
            // automatically guess extension according to class name if not defined in class
329
            if (!isset($this->extension)) {
330
                $this->extension = $this->getExtensionName();
331
            }
332
333
            // add to object spec
334
            $this->objectSpec->specs[$this->extension]['xmlns'] = $this->getExtensionNamespace();
335
        }
336
    }
337
338
    /**
339
     * Get Class name from full class
340
     *
341
     * @param string $class
342
     *
343
     * @return string
344
     */
345
    private function className($class)
346
    {
347
        if (!is_string($class)) {
348
            return $class;
349
        }
350
        if (($pos = strrpos($class, '\\')) === false) {
351
            return $class;
352
        }
353
354
        return substr($class, $pos + 1);
355
    }
356
357
    public function getExtensionName()
358
    {
359
        return strtolower($this->className(get_class($this)));
360
    }
361
362
    public function getExtensionNamespace()
363
    {
364
        if (empty($this->extension_xmlns) && ($this instanceof ExtensionInterface)) {
365
            throw new Exception(sprintf('Extension %s has no defined namespace', get_class($this)));
366
        }
367
368
        return $this->extension_xmlns;
369
    }
370
}
371