InstructionsRenderer::render()   C
last analyzed

Complexity

Conditions 15
Paths 38

Size

Total Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 48
rs 5.9166
c 0
b 0
f 0
cc 15
nc 38
nop 3

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Webino (http://webino.sk)
4
 *
5
 * @link        https://github.com/webino/WebinoDraw for the canonical source repository
6
 * @copyright   Copyright (c) 2012-2017 Webino, s. r. o. (http://webino.sk)
7
 * @author      Peter Bačinský <[email protected]>
8
 * @license     BSD-3-Clause
9
 */
10
11
namespace WebinoDraw\Instructions;
12
13
use ArrayObject;
14
use DOMNodeList;
15
use WebinoDraw\Dom\Element;
16
use WebinoDraw\Dom\NodeInterface;
17
use WebinoDraw\Dom\Factory\NodeListFactory;
18
use WebinoDraw\Dom\Locator;
19
use WebinoDraw\Dom\NodeList;
20
use WebinoDraw\Dom\Text;
21
use WebinoDraw\Draw\HelperPluginManager;
22
use WebinoDraw\Exception\InvalidArgumentException;
23
use WebinoDraw\Factory\InstructionsFactory;
24
use WebinoDraw\Options\ModuleOptions;
25
use WebinoDraw\VarTranslator\Translation;
26
use Zend\Stdlib\ArrayUtils;
27
28
/**
29
 * Class InstructionsRenderer
30
 */
31
class InstructionsRenderer implements InstructionsRendererInterface
32
{
33
    /**
34
     * Makes the helper setting optional for common use cases
35
     */
36
    const DEFAULT_DRAW_HELPER = 'WebinoDrawElement';
37
38
    /**
39
     * @var HelperPluginManager
40
     */
41
    protected $drawHelpers;
42
43
    /**
44
     * @var Locator
45
     */
46
    protected $locator;
47
48
    /**
49
     * @var NodeListFactory
50
     */
51
    protected $nodeListFactory;
52
53
    /**
54
     * @var InstructionsFactory
55
     */
56
    protected $instructionsFactory;
57
58
    /**
59
     * @var ModuleOptions
60
     */
61
    protected $drawOptions;
62
63
    /**
64
     * @param object|HelperPluginManager $drawHelpers
65
     * @param Locator $locator
66
     * @param NodeListFactory $nodeListFactory
67
     * @param InstructionsFactory $instructionsFactory
68
     * @param object|ModuleOptions $drawOptions
69
     */
70
    public function __construct(
71
        HelperPluginManager $drawHelpers,
72
        Locator $locator,
73
        NodeListFactory $nodeListFactory,
74
        InstructionsFactory $instructionsFactory,
75
        ModuleOptions $drawOptions
76
    ) {
77
        $this->drawHelpers         = $drawHelpers;
78
        $this->locator             = $locator;
79
        $this->nodeListFactory     = $nodeListFactory;
80
        $this->instructionsFactory = $instructionsFactory;
81
        $this->drawOptions         = $drawOptions;
82
    }
83
84
    /**
85
     * Render the DOMElement ownerDocument
86
     *
87
     * {@inheritDocs}
88
     * @throws InvalidArgumentException
89
     */
90
    public function render(NodeInterface $node, $instructions, array $vars)
91
    {
92
        $varTranslation = (new Translation($vars))->makeVarKeys();
93
        $drawInstructions = is_array($instructions)
94
                          ? $this->instructionsFactory->create($instructions)
95
                          : $instructions;
96
97
        if (!($drawInstructions instanceof Instructions)) {
98
            throw new InvalidArgumentException('Expected instructions as array|InstructionsInterface');
99
        }
100
101
        $nodePath = null;
102
        if ($node instanceof Element || $node instanceof Text) {
103
            $node->setOnReplace(function ($newNode) use (&$nodePath) {
104
                if ($newNode instanceof Element || $newNode instanceof Text) {
105
                    $nodePath = $newNode->getNodePath();
106
                }
107
            });
108
        }
109
110
        $_node = $node;
111
        foreach ($drawInstructions->getSortedArrayCopy() as $specs) {
112
            $spec = (array) $this->createNodeSpec($specs);
113
            unset($specs);
114
115
            if ($this->resolveIsNodeDisabled($_node, $spec)) {
116
                continue;
117
            }
118
119
            $varTranslation->translate($spec['locator']);
120
            $nodes = $this->locator->locate($_node, $spec['locator']);
121
            if ($this->resolveIsEmptyNodes($nodes)) {
122
                continue;
123
            }
124
125
            $helper = !empty($spec['helper'])
126
                    ? $varTranslation->translateString($spec['helper'])
127
                    : self::DEFAULT_DRAW_HELPER;
128
129
            $this->drawNodes($nodes, $helper, $spec, $vars);
130
131
            $ownerDocument = $node->getOwnerDocument();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface WebinoDraw\Dom\NodeInterface as the method getOwnerDocument() does only exist in the following implementations of said interface: WebinoDraw\Dom\Element, WebinoDraw\Dom\Text.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
132
            if ($ownerDocument && empty($node->parentNode) && isset($nodePath)) {
0 ignored issues
show
Bug introduced by
Accessing parentNode on the interface WebinoDraw\Dom\NodeInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
133
                $_newNode = $ownerDocument->getXpath()->query($nodePath)->item(0);
134
                $_newNode and $_node = $_newNode;
135
            }
136
        }
137
    }
138
139
    /**
140
     * @param array $specs
141
     * @return array
142
     */
143
    protected function createNodeSpec(array $specs)
144
    {
145
        // one node per stackIndex
146
        $spec = current($specs);
147
        return $spec;
148
    }
149
150
    /**
151
     * @param DOMNodeList $nodes
152
     * @param string $helper
153
     * @param array $spec
154
     * @param array $vars
155
     */
156
    protected function drawNodes(DOMNodeList $nodes, $helper, array $spec, array $vars)
157
    {
158
        $this->drawHelpers->get((string) $helper)
159
            ->setVars($vars)
160
            ->__invoke($this->nodeListFactory->create($nodes), $spec);
161
    }
162
163
    /**
164
     * @param NodeInterface|null $node
165
     * @param array $spec
166
     * @return bool
167
     */
168
    protected function resolveIsNodeDisabled($node, array $spec)
169
    {
170
        // locator not set or node already removed
171
        return empty($spec['locator']) || empty($node) || empty($node->ownerDocument);
0 ignored issues
show
Bug introduced by
Accessing ownerDocument on the interface WebinoDraw\Dom\NodeInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
172
    }
173
174
    /**
175
     * @param DOMNodeList|null $nodes
176
     * @return bool
177
     */
178
    protected function resolveIsEmptyNodes(DOMNodeList $nodes = null)
179
    {
180
        return empty($nodes->length);
181
    }
182
183
    /**
184
     * @param array|NodeList $nodes
185
     * @param array $instructions
186
     * @param ArrayObject $translation
187
     * @return $this
188
     */
189
    public function subInstructions($nodes, array $instructions, ArrayObject $translation)
190
    {
191
        $nodeList = is_array($nodes) ? $this->nodeListFactory->create($nodes) : $nodes;
192
        if (!($nodeList instanceof NodeList)) {
193
            throw new InvalidArgumentException('Expected nodes as array|NodeList');
194
        }
195
196
        $drawInstructions = $this->instructionsFactory->create($instructions);
197
        $vars = $translation->getArrayCopy();
198
199
        foreach ($nodeList as $node) {
200
            $this->render($node, $drawInstructions, $vars);
201
        }
202
203
        return $this;
204
    }
205
206
    /**
207
     * @param array $spec
208
     * @param Translation $translation
209
     * @return $this
210
     */
211
    public function expandInstructions(array &$spec, Translation $translation = null)
212
    {
213
        if (empty($spec['instructionset'])) {
214
            return $this;
215
        }
216
217
        $translation and $translation->makeVarKeys(clone $translation)->translate($spec['instructionset']);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface ArrayAccess as the method translate() does only exist in the following implementations of said interface: WebinoDraw\VarTranslator\Translation.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
218
219
        $instructions = $this->instructionsFactory->create([]);
220
        foreach ($spec['instructionset'] as $instructionset) {
221
            $instructions->merge($this->drawOptions->instructionsFromSet($instructionset));
222
        }
223
224
        unset($spec['instructionset']);
225
        foreach ($instructions->getSortedArrayCopy() as $instruction) {
226
            $key = key($instruction);
227
            if (empty($spec['instructions'][$key])) {
228
                $spec['instructions'][$key] = current($instruction);
229
                continue;
230
            }
231
            $spec['instructions'][$key] = ArrayUtils::merge(current($instruction), $spec['instructions'][$key]);
232
        }
233
234
        return $this;
235
    }
236
}
237