Passed
Push — master ( 3896ee...e50616 )
by Raffael
05:45
created

Xml::getChildNode()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 2
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * tubee.io
7
 *
8
 * @copyright   Copryright (c) 2017-2019 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\Converter;
22
use Tubee\Endpoint\Xml\Exception as XmlException;
23
use Tubee\Endpoint\Xml\QueryTransformer;
24
use Tubee\EndpointObject\EndpointObjectInterface;
25
use Tubee\Storage\StorageInterface;
26
use Tubee\Workflow\Factory as WorkflowFactory;
27
28
class Xml extends AbstractFile
29
{
30
    /**
31
     * Kind.
32
     */
33
    public const KIND = 'XmlEndpoint';
34
35
    /**
36
     * XML root name.
37
     *
38
     * @var string
39
     */
40
    protected $root_name = 'data';
41
42
    /**
43
     * XMl node name.
44
     *
45
     * @var string
46
     */
47
    protected $node_name = 'row';
48
49
    /**
50
     * Pretty output.
51
     *
52
     * @var bool
53
     */
54
    protected $pretty = true;
55
56
    /**
57
     * Preserved whitespace.
58
     *
59
     * @var bool
60
     */
61
    protected $preserve_whitespace = false;
62
63
    /**
64
     * Init endpoint.
65
     */
66 25
    public function __construct(string $name, string $type, string $file, StorageInterface $storage, CollectionInterface $collection, WorkflowFactory $workflow, LoggerInterface $logger, array $resource = [])
67
    {
68 25
        if (isset($resource['data']['resource'])) {
69 2
            $this->setXmlOptions($resource['data']['resource']);
70
        }
71
72 24
        parent::__construct($name, $type, $file, $storage, $collection, $workflow, $logger, $resource);
73 24
    }
74
75
    /**
76
     * {@inheritdoc}
77
     */
78 14
    public function setup(bool $simulate = false): EndpointInterface
79
    {
80 14
        if ($this->type === EndpointInterface::TYPE_DESTINATION) {
81 9
            $streams = [$this->file => $this->storage->openWriteStream($this->file)];
82
        } else {
83 5
            $streams = $this->storage->openReadStreams($this->file);
84
        }
85
86 14
        foreach ($streams as $path => $stream) {
87 14
            $dom = new DOMDocument('1.0', 'UTF-8');
88 14
            $dom->formatOutput = $this->pretty;
89 14
            $dom->preserveWhiteSpace = $this->preserve_whitespace;
90
91
            //read stream into memory since xml operates in-memory
92 14
            $content = stream_get_contents($stream);
93
94 14
            if ($this->type === EndpointInterface::TYPE_DESTINATION && empty($content)) {
95 4
                $xml_root = $dom->createElement($this->root_name);
96 4
                $xml_root = $dom->appendChild($xml_root);
97
            } else {
98 10
                $this->logger->debug('decode xml stream from ['.$path.']', [
99 10
                    'category' => get_class($this),
100
                ]);
101
102 10
                if ($dom->loadXML($content) === false) {
103
                    throw new XmlException\InvalidXml('could not decode xml stream from '.$path.'');
104
                }
105
106 10
                $xml_root = $dom->documentElement;
107
108 10
                if (!$xml_root->hasChildNodes()) {
109 4
                    $level = $this->type === EndpointInterface::TYPE_SOURCE ? 'warning' : 'debug';
110
111 4
                    $this->logger->$level('empty xml file ['.$path.'] given', [
112 4
                        'category' => get_class($this),
113
                    ]);
114
                }
115
            }
116
117 14
            $this->files[] = [
118 14
                'dom' => $dom,
119 14
                'xml_root' => $xml_root,
120 14
                'path' => $path,
121 14
                'stream' => $stream,
122
            ];
123
        }
124
125 14
        return $this;
126
    }
127
128
    /**
129
     * Set options.
130
     */
131 2
    public function setXmlOptions(?array $config = null): EndpointInterface
132
    {
133 2
        if ($config === null) {
134
            return $this;
135
        }
136
137 2
        foreach ($config as $option => $value) {
138 2
            switch ($option) {
139 2
                case 'node_name':
140 2
                case 'root_name':
141 2
                case 'pretty':
142 2
                case 'preserve_whitespace':
143 1
                    $this->{$option} = $value;
144
145 1
                    break;
146
                default:
147 2
                    throw new InvalidArgumentException('unknown xml option '.$option.' given');
148
            }
149
        }
150
151 1
        return $this;
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157 9
    public function shutdown(bool $simulate = false): EndpointInterface
158
    {
159 9
        foreach ($this->files as $resource) {
160 8
            if ($simulate === false && $this->type === EndpointInterface::TYPE_DESTINATION) {
161 8
                $this->flush($simulate);
162 7
                if (fwrite($resource['stream'], $resource['dom']->saveXML()) === false) {
163
                    throw new Exception\WriteOperationFailed('failed create xml file '.$resource['path']);
164
                }
165
166 7
                $this->storage->syncWriteStream($resource['stream'], $resource['path']);
167
            } else {
168 7
                fclose($resource['stream']);
169
            }
170
        }
171
172 8
        $this->files = [];
173
174 8
        return $this;
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180 8
    public function transformQuery(?array $query = null)
181
    {
182 8
        if ($this->filter_all !== null) {
183 2
            $result = $this->filter_all;
184
        }
185
186 8
        if (!empty($query)) {
187 8
            if ($this->filter_all === null) {
188 6
                $result = '//*['.QueryTransformer::transform($query).']';
189
            } else {
190 2
                if (preg_match('#\[([^)]+)\]#', $this->filter_all, $match)) {
191 1
                    $new = '['.$match[1].' and '.QueryTransformer::transform($query).']';
192
193 1
                    return str_replace($match[0], $new, $this->filter_all);
194
                }
195
196 1
                $result = $this->filter_all.'['.QueryTransformer::transform($query).']';
197
            }
198
        }
199
200 7
        return $result;
0 ignored issues
show
Bug introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206
    public function getAll(?array $query = null): Generator
207
    {
208
        $filter = $this->transformQuery($query);
209
        $i = 0;
210
211
        foreach ($this->files as $xml) {
212
            $this->logger->debug('find xml nodes with xpath ['.$filter.'] in ['.$xml['path'].'] on endpoint ['.$this->getIdentifier().']', [
213
                'category' => get_class($this),
214
            ]);
215
216
            $xpath = new \DOMXPath($xml['dom']);
217
            $node = $xpath->query($filter);
218
219
            foreach ($node as $result) {
220
                $result = Converter::xmlToArray($result);
221
                yield $this->build($result);
222
                ++$i;
223
            }
224
        }
225
226
        return $i;
227
    }
228
229
    /**
230
     * {@inheritdoc}
231
     */
232 4
    public function create(AttributeMapInterface $map, array $object, bool $simulate = false): ?string
233
    {
234 4
        $xml = $this->files[0];
235 4
        $current_track = $xml['dom']->createElement($this->node_name);
236 4
        $current_track = $xml['xml_root']->appendChild($current_track);
237
238 4
        foreach ($object as $column => $value) {
239 4
            if (is_array($value)) {
240 1
                $attr_subnode = $current_track->appendChild($xml['dom']->createElement($column));
241 1
                foreach ($value as $val) {
242 1
                    $attr_subnode->appendChild($xml['dom']->createElement($column, $val));
243
                }
244
            } else {
245 4
                $current_track->appendChild($xml['dom']->createElement($column, $value));
246
            }
247
        }
248
249 4
        $this->logger->debug('create new xml object on endpoint ['.$this->name.'] with values [{values}]', [
250 4
            'category' => get_class($this),
251 4
            'values' => $object,
252
        ]);
253
254 4
        return null;
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260 1
    public function getDiff(AttributeMapInterface $map, array $diff): array
261
    {
262 1
        return $diff;
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     */
268 1
    public function change(AttributeMapInterface $map, array $diff, array $object, array $endpoint_object, bool $simulate = false): ?string
269
    {
270 1
        $xml = $this->files[0];
271 1
        $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...
272 1
        $filter = $this->getFilterOne($object);
273 1
        $xpath = new \DOMXPath($xml['dom']);
274 1
        $node = $xpath->query($filter);
275 1
        $node = $node[0];
276
277 1
        foreach ($diff as $attribute => $update) {
278 1
            $child = $this->getChildNode($node, $attribute);
279
280 1
            switch ($update['action']) {
281 1
                case AttributeMapInterface::ACTION_REPLACE:
282 1
                    if (is_array($update['value'])) {
283
                        $new = $xml['dom']->createElement($attribute);
284
                        foreach ($update['value'] as $val) {
285
                            $new->appendChild($xml['dom']->createElement($attribute, $val));
286
                        }
287
                    } else {
288 1
                        $new = $xml['dom']->createElement($attribute, $update['value']);
289
                    }
290
291 1
                    $node->replaceChild($new, $child);
292
293 1
                break;
294 1
                case AttributeMapInterface::ACTION_REMOVE:
295 1
                    $node->removeChild($child);
296
297 1
                break;
298 1
                case AttributeMapInterface::ACTION_ADD:
299 1
                    $node->appendChild($xml['dom']->createElement($attribute, $update['value']));
300
301 1
                break;
302
                default:
303 1
                    throw new InvalidArgumentException('unknown action '.$update['action'].' given');
304
            }
305
        }
306
307 1
        return null;
308
    }
309
310
    /**
311
     * {@inheritdoc}
312
     */
313 1
    public function delete(AttributeMapInterface $map, array $object, array $endpoint_object, bool $simulate = false): bool
314
    {
315 1
        $xml = $this->files[0];
316 1
        $filter = $this->getFilterOne($object);
317 1
        $xpath = new \DOMXPath($xml['dom']);
318 1
        $node = $xpath->query($filter);
319 1
        $node = $node[0];
320 1
        $xml['xml_root']->removeChild($node);
321
322 1
        return true;
323
    }
324
325
    /**
326
     * {@inheritdoc}
327
     */
328 4
    public function getOne(array $object, array $attributes = []): EndpointObjectInterface
329
    {
330 4
        $filter = $this->getFilterOne($object);
331
332 4
        foreach ($this->files as $xml) {
333 4
            $this->logger->debug('find xml node with xpath ['.$filter.'] in ['.$xml['path'].'] on endpoint ['.$this->getIdentifier().']', [
334 4
                'category' => get_class($this),
335
            ]);
336
337 4
            $xpath = new \DOMXPath($xml['dom']);
338 4
            $nodes = $xpath->query($filter);
339
340 4
            $nodes = iterator_to_array($nodes);
341
342 4
            if (count($nodes) > 1) {
343 1
                throw new Exception\ObjectMultipleFound('found more than one object with filter '.$filter);
344
            }
345 3
            if (count($nodes) === 0) {
346 1
                throw new Exception\ObjectNotFound('no object found with filter '.$filter);
347
            }
348
349 2
            $node = Converter::xmlToArray(array_shift($nodes));
350
351 2
            return $this->build($node);
352
        }
353
    }
354
355
    /**
356
     * Get child node by name.
357
     */
358 1
    protected function getChildNode(DOMNode $node, string $name)
359
    {
360 1
        foreach ($node->childNodes as $child) {
361 1
            if ($child->nodeName === $name) {
362 1
                return $child;
363
            }
364
        }
365 1
    }
366
}
367