Completed
Pull Request — master (#84)
by
unknown
01:20
created

AbstractFrame::set()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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