Completed
Push — master ( ea9cfc...8dc8d9 )
by Raffael
12:43 queued 03:57
created

Xml   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 387
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 60
lcom 1
cbo 7
dl 0
loc 387
ccs 0
cts 167
cp 0
rs 3.6
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
B setup() 0 49 8
B setXmlOptions() 0 22 7
A shutdown() 0 19 5
A transformQuery() 0 19 4
A getAll() 0 22 3
A create() 0 24 4
A getDiff() 0 4 1
B change() 0 41 7
A delete() 0 11 1
A getOne() 0 26 4
B xmlToArray() 0 40 11
A getChildNode() 0 8 3

How to fix   Complexity   

Complex Class

Complex classes like Xml often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Xml, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * tubee.io
7
 *
8
 * @copyright   Copryright (c) 2017-2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Tubee\Endpoint;
13
14
use DOMDocument;
15
use DOMNode;
16
use Generator;
17
use InvalidArgumentException;
18
use Psr\Log\LoggerInterface;
19
use Tubee\AttributeMap\AttributeMapInterface;
20
use Tubee\Collection\CollectionInterface;
21
use Tubee\Endpoint\Xml\Exception as XmlException;
22
use Tubee\Endpoint\Xml\QueryTransformer;
23
use Tubee\EndpointObject\EndpointObjectInterface;
24
use Tubee\Storage\StorageInterface;
25
use Tubee\Workflow\Factory as WorkflowFactory;
26
27
class Xml extends AbstractFile
28
{
29
    /**
30
     * Kind.
31
     */
32
    public const KIND = 'XmlEndpoint';
33
34
    /**
35
     * new XML element.
36
     *
37
     * @var SimpleXMLElement
38
     */
39
    protected $new_xml;
40
41
    /**
42
     * XML root name.
43
     *
44
     * @var string
45
     */
46
    protected $root_name = 'data';
47
48
    /**
49
     * XMl node name.
50
     *
51
     * @var string
52
     */
53
    protected $node_name = 'row';
54
55
    /**
56
     * Pretty output.
57
     *
58
     * @var bool
59
     */
60
    protected $pretty = true;
61
62
    /**
63
     * Preserved whitespace.
64
     *
65
     * @var bool
66
     */
67
    protected $preserve_whitespace = false;
68
69
    /**
70
     * Init endpoint.
71
     */
72
    public function __construct(string $name, string $type, string $file, StorageInterface $storage, CollectionInterface $collection, WorkflowFactory $workflow, LoggerInterface $logger, array $resource = [])
73
    {
74
        if (isset($resource['data']['resource'])) {
75
            $this->setXmlOptions($resource['data']['resource']);
76
        }
77
78
        parent::__construct($name, $type, $file, $storage, $collection, $workflow, $logger, $resource);
79
    }
80
81
    /**
82
     * {@inheritdoc}
83
     */
84
    public function setup(bool $simulate = false): EndpointInterface
85
    {
86
        if ($this->type === EndpointInterface::TYPE_DESTINATION) {
87
            $streams = [$this->file => $this->storage->openWriteStream($this->file)];
88
        } else {
89
            $streams = $this->storage->openReadStreams($this->file);
90
        }
91
92
        foreach ($streams as $path => $stream) {
93
            $dom = new DOMDocument('1.0', 'UTF-8');
94
            $dom->formatOutput = $this->pretty;
95
            $dom->preserveWhiteSpace = $this->preserve_whitespace;
96
97
            //read stream into memory since xml operates in-memory
98
            $content = stream_get_contents($stream);
99
100
            if ($this->type === EndpointInterface::TYPE_DESTINATION && empty($content)) {
101
                $xml_root = $dom->createElement($this->root_name);
102
                $xml_root = $dom->appendChild($xml_root);
103
            } else {
104
                $this->logger->debug('decode xml stream from ['.$path.']', [
105
                    'category' => get_class($this),
106
                ]);
107
108
                if ($dom->loadXML($content) === false) {
109
                    throw new XmlException\InvalidXml('could not decode xml stream from '.$path.'');
110
                }
111
112
                $xml_root = $dom->documentElement;
113
114
                if (!$xml_root->hasChildNodes()) {
115
                    $level = $this->type === EndpointInterface::TYPE_SOURCE ? 'warning' : 'debug';
116
117
                    $this->logger->$level('empty xml file ['.$path.'] given', [
118
                        'category' => get_class($this),
119
                    ]);
120
                }
121
            }
122
123
            $this->files[] = [
124
                'dom' => $dom,
125
                'xml_root' => $xml_root,
126
                'path' => $path,
127
                'stream' => $stream,
128
            ];
129
        }
130
131
        return $this;
132
    }
133
134
    /**
135
     * Set options.
136
     */
137
    public function setXmlOptions(?array $config = null): EndpointInterface
138
    {
139
        if ($config === null) {
140
            return $this;
141
        }
142
143
        foreach ($config as $option => $value) {
144
            switch ($option) {
145
                case 'node_name':
146
                case 'root_name':
147
                case 'pretty':
148
                case 'preserve_whitespace':
149
                    $this->{$option} = $value;
150
151
                    break;
152
                default:
153
                    throw new InvalidArgumentException('unknown xml option '.$option.' given');
154
            }
155
        }
156
157
        return $this;
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     */
163
    public function shutdown(bool $simulate = false): EndpointInterface
164
    {
165
        foreach ($this->files as $resource) {
166
            if ($simulate === false && $this->type === EndpointInterface::TYPE_DESTINATION) {
167
                $this->flush($simulate);
168
                if (fwrite($resource['stream'], $resource['dom']->saveXML()) === false) {
169
                    throw new Exception\WriteOperationFailed('failed create xml file '.$resource['path']);
170
                }
171
172
                $this->storage->syncWriteStream($resource['stream'], $resource['path']);
173
            }
174
175
            fclose($resource['stream']);
176
        }
177
178
        $this->files = [];
179
180
        return $this;
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     */
186
    public function transformQuery(?array $query = null)
187
    {
188
        $pre = '//'.$this->node_name;
189
        $result = $pre;
190
191
        if ($this->filter_all !== null) {
192
            $result = $pre.'['.$this->filter_all.']';
193
        }
194
195
        if (!empty($query)) {
196
            if ($this->filter_all === null) {
197
                $result = $pre.'['.QueryTransformer::transform($query).']';
198
            } else {
199
                $result = $pre.'['.$this->filter_all.' and '.QueryTransformer::transform($query).']';
200
            }
201
        }
202
203
        return $result;
204
    }
205
206
    /**
207
     * {@inheritdoc}
208
     */
209
    public function getAll(?array $query = null): Generator
210
    {
211
        $filter = $this->transformQuery($query);
212
        $i = 0;
213
214
        foreach ($this->files as $xml) {
215
            $this->logger->debug('find xml nodes with xpath ['.$filter.'] in ['.$xml['path'].'] on endpoint ['.$this->getIdentifier().']', [
216
                'category' => get_class($this),
217
            ]);
218
219
            $xpath = new \DOMXPath($xml['dom']);
220
            $node = $xpath->query($filter);
221
222
            foreach ($node as $result) {
223
                $result = $this->xmlToArray($result);
224
                yield $this->build($result);
225
                ++$i;
226
            }
227
        }
228
229
        return $i;
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     */
235
    public function create(AttributeMapInterface $map, array $object, bool $simulate = false): ?string
236
    {
237
        $xml = $this->files[0];
238
        $current_track = $xml['dom']->createElement($this->node_name);
239
        $current_track = $xml['xml_root']->appendChild($current_track);
240
241
        foreach ($object as $column => $value) {
242
            if (is_array($value)) {
243
                $attr_subnode = $current_track->appendChild($xml['dom']->createElement($column));
244
                foreach ($value as $val) {
245
                    $attr_subnode->appendChild($xml['dom']->createElement($column, $val));
246
                }
247
            } else {
248
                $current_track->appendChild($xml['dom']->createElement($column, $value));
249
            }
250
        }
251
252
        $this->logger->debug('create new xml object on endpoint ['.$this->name.'] with values [{values}]', [
253
            'category' => get_class($this),
254
            'values' => $object,
255
        ]);
256
257
        return null;
258
    }
259
260
    /**
261
     * {@inheritdoc}
262
     */
263
    public function getDiff(AttributeMapInterface $map, array $diff): array
264
    {
265
        return $diff;
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271
    public function change(AttributeMapInterface $map, array $diff, array $object, array $endpoint_object, bool $simulate = false): ?string
272
    {
273
        $xml = $this->files[0];
274
        $attrs = [];
0 ignored issues
show
Unused Code introduced by
$attrs is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
275
        $filter = $this->getFilterOne($object);
276
        $xpath = new \DOMXPath($xml['dom']);
277
        $node = $xpath->query($filter);
278
        $node = $node[0];
279
280
        foreach ($diff as $attribute => $update) {
281
            $child = $this->getChildNode($node, $attribute);
282
283
            switch ($update['action']) {
284
                case AttributeMapInterface::ACTION_REPLACE:
285
                    if (is_array($update['value'])) {
286
                        $new = $xml['dom']->createElement($attribute);
287
                        foreach ($update['value'] as $val) {
288
                            $new->appendChild($xml['dom']->createElement($attribute, $val));
289
                        }
290
                    } else {
291
                        $new = $xml['dom']->createElement($attribute, $update['value']);
292
                    }
293
294
                    $node->replaceChild($new, $child);
295
296
                break;
297
                case AttributeMapInterface::ACTION_REMOVE:
298
                    $node->removeChild($child);
299
300
                break;
301
                case AttributeMapInterface::ACTION_ADD:
302
                    $child->appendChild($xml['dom']->createElement($attribute, $update['value']));
303
304
                break;
305
                default:
306
                    throw new InvalidArgumentException('unknown action '.$update['action'].' given');
307
            }
308
        }
309
310
        return null;
311
    }
312
313
    /**
314
     * {@inheritdoc}
315
     */
316
    public function delete(AttributeMapInterface $map, array $object, array $endpoint_object, bool $simulate = false): bool
317
    {
318
        $xml = $this->files[0];
319
        $filter = $this->getFilterOne($object);
320
        $xpath = new \DOMXPath($xml['dom']);
321
        $node = $xpath->query($filter);
322
        $node = $node[0];
323
        $xml['xml_root']->removeChild($node);
324
325
        return true;
326
    }
327
328
    /**
329
     * {@inheritdoc}
330
     */
331
    public function getOne(array $object, array $attributes = []): EndpointObjectInterface
332
    {
333
        $filter = $pre = '//'.$this->node_name.'['.$this->getFilterOne($object).']';
0 ignored issues
show
Unused Code introduced by
$pre is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
334
335
        foreach ($this->files as $xml) {
336
            $this->logger->debug('find xml node with xpath ['.$filter.'] in ['.$xml['path'].'] on endpoint ['.$this->getIdentifier().']', [
337
                'category' => get_class($this),
338
            ]);
339
340
            $xpath = new \DOMXPath($xml['dom']);
341
            $nodes = $xpath->query($filter);
342
343
            $nodes = iterator_to_array($nodes);
344
345
            if (count($nodes) > 1) {
346
                throw new Exception\ObjectMultipleFound('found more than one object with filter '.$filter);
347
            }
348
            if (count($nodes) === 0) {
349
                throw new Exception\ObjectNotFound('no object found with filter '.$filter);
350
            }
351
352
            $node = $this->xmlToArray(array_shift($nodes));
353
354
            return $this->build($node);
355
        }
356
    }
357
358
    /**
359
     * Convert XMLElement into nicly formatted php array.
360
     */
361
    protected function xmlToArray($root)
362
    {
363
        $result = [];
364
365
        if ($root->hasAttributes()) {
366
            $attrs = $root->attributes;
367
            foreach ($attrs as $attr) {
368
                $result['@attributes'][$attr->name] = $attr->value;
369
            }
370
        }
371
372
        if ($root->hasChildNodes()) {
373
            $children = $root->childNodes;
374
375
            if ($children->length == 1) {
376
                $child = $children->item(0);
377
378
                if ($child->nodeType === XML_TEXT_NODE || $child->nodeType === XML_CDATA_SECTION_NODE) {
379
                    $result['_value'] = $child->nodeValue;
380
381
                    return count($result) == 1 ? $result['_value'] : $result;
382
                }
383
            }
384
385
            $groups = [];
386
            foreach ($children as $child) {
387
                if (!isset($result[$child->nodeName])) {
388
                    $result[$child->nodeName] = $this->xmlToArray($child);
389
                } else {
390
                    if (!isset($groups[$child->nodeName])) {
391
                        $result[$child->nodeName] = [$result[$child->nodeName]];
392
                        $groups[$child->nodeName] = 1;
393
                    }
394
                    $result[$child->nodeName][] = $this->xmlToArray($child);
395
                }
396
            }
397
        }
398
399
        return $result;
400
    }
401
402
    /**
403
     * Get child node by name.
404
     */
405
    protected function getChildNode(DOMNode $node, string $name)
406
    {
407
        foreach ($node->childNodes as $child) {
408
            if ($child->nodeName === $name) {
409
                return $child;
410
            }
411
        }
412
    }
413
}
414