Completed
Push — master ( 519f76...318dd5 )
by Raffael
24:13 queued 15:29
created

Xml   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 348
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 76.47%

Importance

Changes 0
Metric Value
wmc 51
lcom 1
cbo 11
dl 0
loc 348
ccs 117
cts 153
cp 0.7647
rs 7.92
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A getOne() 0 27 4
A getChildNode() 0 8 3
B setup() 0 49 8
B setXmlOptions() 0 22 7
A shutdown() 0 20 5
A transformQuery() 0 20 5
A getAll() 0 32 4
A create() 0 21 4
A getDiff() 0 4 1
B change() 0 41 7
A delete() 0 11 1

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
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
    use LoggerTrait;
31
32
    /**
33
     * Kind.
34
     */
35
    public const KIND = 'XmlEndpoint';
36
37
    /**
38
     * XML root name.
39
     *
40
     * @var string
41
     */
42
    protected $root_name = 'data';
43
44
    /**
45
     * XMl node name.
46
     *
47
     * @var string
48
     */
49
    protected $node_name = 'row';
50
51
    /**
52
     * Pretty output.
53
     *
54
     * @var bool
55
     */
56
    protected $pretty = true;
57
58
    /**
59
     * Preserved whitespace.
60
     *
61
     * @var bool
62
     */
63
    protected $preserve_whitespace = false;
64
65
    /**
66
     * Init endpoint.
67
     */
68 22
    public function __construct(string $name, string $type, string $file, StorageInterface $storage, CollectionInterface $collection, WorkflowFactory $workflow, LoggerInterface $logger, array $resource = [])
69
    {
70 22
        if (isset($resource['data']['resource'])) {
71 1
            $this->setXmlOptions($resource['data']['resource']);
72
        }
73
74 21
        parent::__construct($name, $type, $file, $storage, $collection, $workflow, $logger, $resource);
75 21
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80 11
    public function setup(bool $simulate = false): EndpointInterface
81
    {
82 11
        $streams = $this->storage->openReadStreams($this->file);
83
84 11
        if ($this->type === EndpointInterface::TYPE_DESTINATION) {
85 6
            $this->writable = $this->storage->openWriteStream($this->file);
86
        }
87
88 11
        foreach ($streams as $path => $stream) {
89 11
            $dom = new DOMDocument('1.0', 'UTF-8');
90 11
            $dom->formatOutput = $this->pretty;
91 11
            $dom->preserveWhiteSpace = $this->preserve_whitespace;
92
93
            //read stream into memory since xml operates in-memory
94 11
            $content = stream_get_contents($stream);
95
96 11
            if ($this->type === EndpointInterface::TYPE_DESTINATION && empty($content)) {
97
                $xml_root = $dom->createElement($this->root_name);
98
                $xml_root = $dom->appendChild($xml_root);
99
            } else {
100 11
                $this->logger->debug('decode xml stream from ['.$path.']', [
101 11
                    'category' => get_class($this),
102
                ]);
103
104 11
                if ($dom->loadXML($content) === false) {
105
                    throw new XmlException\InvalidXml('could not decode xml stream from '.$path.'');
106
                }
107
108 11
                $xml_root = $dom->documentElement;
109
110 11
                if (!$xml_root->hasChildNodes()) {
111 5
                    $level = $this->type === EndpointInterface::TYPE_SOURCE ? 'warning' : 'debug';
112
113 5
                    $this->logger->$level('empty xml file ['.$path.'] given', [
114 5
                        'category' => get_class($this),
115
                    ]);
116
                }
117
            }
118
119 11
            $this->files[] = [
120 11
                'dom' => $dom,
121 11
                'xml_root' => $xml_root,
122 11
                'path' => $path,
123 11
                'stream' => $stream,
124
            ];
125
        }
126
127 11
        return $this;
128
    }
129
130
    /**
131
     * Set options.
132
     */
133 1
    public function setXmlOptions(?array $config = null): EndpointInterface
134
    {
135 1
        if ($config === null) {
136
            return $this;
137
        }
138
139 1
        foreach ($config as $option => $value) {
140
            switch ($option) {
141 1
                case 'node_name':
142 1
                case 'root_name':
143 1
                case 'pretty':
144 1
                case 'preserve_whitespace':
145
                    $this->{$option} = $value;
146
147
                    break;
148
                default:
149 1
                    throw new InvalidArgumentException('unknown xml option '.$option.' given');
150
            }
151
        }
152
153
        return $this;
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159 6
    public function shutdown(bool $simulate = false): EndpointInterface
160
    {
161 6
        foreach ($this->files as $resource) {
162 5
            if ($simulate === false && $this->type === EndpointInterface::TYPE_DESTINATION) {
163 5
                $this->flush($simulate);
164 4
                if (fwrite($this->writable, $resource['dom']->saveXML()) === false) {
165
                    throw new Exception\WriteOperationFailed('failed create xml file '.$resource['path']);
166
                }
167
168 4
                $this->storage->syncWriteStream($this->writable, $resource['path']);
169 4
                fclose($resource['stream']);
170
            } else {
171
                fclose($resource['stream']);
172
            }
173
        }
174
175 5
        $this->files = [];
176
177 5
        return $this;
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183 12
    public function transformQuery(?array $query = null)
184
    {
185 12
        if ($this->filter_all !== null && empty($query)) {
186
            return '//*['.QueryTransformer::transform($this->getFilterAll()).']';
187
        }
188 12
        if (!empty($query)) {
189 12
            if ($this->filter_all === null) {
190 10
                return '//*['.QueryTransformer::transform($query).']';
191
            }
192
193 2
            return '//*['.QueryTransformer::transform([
194
                    '$and' => [
195 2
                        $this->getFilterAll(),
196 2
                        $query,
197
                    ],
198 2
                ]).']';
199
        }
200
201
        return '//'.$this->node_name;
202
    }
203
204
    /**
205
     * {@inheritdoc}
206
     */
207
    public function getAll(?array $query = null): Generator
208
    {
209
        $filter = $this->transformQuery($query);
210
        $i = 0;
211
        $this->logGetAll($filter);
212
213
        foreach ($this->files as $xml) {
214
            $this->logger->debug('find xml nodes with xpath ['.$filter.'] in ['.$xml['path'].']', [
215
                'category' => get_class($this),
216
            ]);
217
218
            $xpath = new \DOMXPath($xml['dom']);
219
            $node = $xpath->query($filter);
220
221
            foreach ($node as $result) {
222
                $result = Converter::xmlToArray($result);
223
224
                if (!is_array($result)) {
225
                    $this->logger->error('xml needs to yield objects ['.$xml['path'].'], make sure a propper endpoint filter has been set', [
226
                        'category' => get_class($this),
227
                    ]);
228
229
                    continue;
230
                }
231
232
                yield $this->build($result);
233
                ++$i;
234
            }
235
        }
236
237
        return $i;
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243 1
    public function create(AttributeMapInterface $map, array $object, bool $simulate = false): ?string
244
    {
245 1
        $xml = $this->files[0];
246 1
        $current_track = $xml['dom']->createElement($this->node_name);
247 1
        $current_track = $xml['xml_root']->appendChild($current_track);
248
249 1
        foreach ($object as $column => $value) {
250 1
            if (is_array($value)) {
251
                $attr_subnode = $current_track->appendChild($xml['dom']->createElement($column));
252
                foreach ($value as $val) {
253
                    $attr_subnode->appendChild($xml['dom']->createElement($column, $val));
254
                }
255
            } else {
256 1
                $current_track->appendChild($xml['dom']->createElement($column, $value));
257
            }
258
        }
259
260 1
        $this->logCreate($object);
261
262 1
        return null;
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     */
268 1
    public function getDiff(AttributeMapInterface $map, array $diff): array
269
    {
270 1
        return $diff;
271
    }
272
273
    /**
274
     * {@inheritdoc}
275
     */
276 1
    public function change(AttributeMapInterface $map, array $diff, array $object, EndpointObjectInterface $endpoint_object, bool $simulate = false): ?string
277
    {
278 1
        $xml = $this->files[0];
279 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...
280 1
        $this->logChange($endpoint_object->getFilter(), $diff);
281 1
        $xpath = new \DOMXPath($xml['dom']);
282 1
        $node = $xpath->query($endpoint_object->getFilter());
283 1
        $node = $node[0];
284
285 1
        foreach ($diff as $attribute => $update) {
286 1
            $child = $this->getChildNode($node, $attribute);
287
288 1
            switch ($update['action']) {
289 1
                case AttributeMapInterface::ACTION_REPLACE:
290 1
                    if (is_array($update['value'])) {
291
                        $new = $xml['dom']->createElement($attribute);
292
                        foreach ($update['value'] as $val) {
293
                            $new->appendChild($xml['dom']->createElement($attribute, $val));
294
                        }
295
                    } else {
296 1
                        $new = $xml['dom']->createElement($attribute, $update['value']);
297
                    }
298
299 1
                    $node->replaceChild($new, $child);
300
301 1
                break;
302 1
                case AttributeMapInterface::ACTION_REMOVE:
303 1
                    $node->removeChild($child);
304
305 1
                break;
306 1
                case AttributeMapInterface::ACTION_ADD:
307 1
                    $node->appendChild($xml['dom']->createElement($attribute, $update['value']));
308
309 1
                break;
310
                default:
311 1
                    throw new InvalidArgumentException('unknown action '.$update['action'].' given');
312
            }
313
        }
314
315 1
        return null;
316
    }
317
318
    /**
319
     * {@inheritdoc}
320
     */
321 1
    public function delete(AttributeMapInterface $map, array $object, EndpointObjectInterface $endpoint_object, bool $simulate = false): bool
322
    {
323 1
        $xml = $this->files[0];
324 1
        $this->logDelete($endpoint_object->getFilter());
325 1
        $xpath = new \DOMXPath($xml['dom']);
326 1
        $node = $xpath->query($endpoint_object->getFilter());
327 1
        $node = $node[0];
328 1
        $xml['xml_root']->removeChild($node);
329
330 1
        return true;
331
    }
332
333
    /**
334
     * {@inheritdoc}
335
     */
336 4
    public function getOne(array $object, array $attributes = []): EndpointObjectInterface
337
    {
338 4
        $filter = $this->transformQuery($this->getFilterOne($object));
339 4
        $this->logGetOne($filter);
340
341 4
        foreach ($this->files as $xml) {
342 4
            $this->logger->debug('find xml node with xpath ['.$filter.'] in ['.$xml['path'].']', [
343 4
                'category' => get_class($this),
344
            ]);
345
346 4
            $xpath = new \DOMXPath($xml['dom']);
347 4
            $nodes = $xpath->query($filter);
348
349 4
            $nodes = iterator_to_array($nodes);
350
351 4
            if (count($nodes) > 1) {
352 1
                throw new Exception\ObjectMultipleFound('found more than one object with filter '.$filter);
353
            }
354 3
            if (count($nodes) === 0) {
355 1
                throw new Exception\ObjectNotFound('no object found with filter '.$filter);
356
            }
357
358 2
            $node = Converter::xmlToArray(array_shift($nodes));
359
360 2
            return $this->build($node, $filter);
361
        }
362
    }
363
364
    /**
365
     * Get child node by name.
366
     */
367 1
    protected function getChildNode(DOMNode $node, string $name)
368
    {
369 1
        foreach ($node->childNodes as $child) {
370 1
            if ($child->nodeName === $name) {
371 1
                return $child;
372
            }
373
        }
374 1
    }
375
}
376