Completed
Push — master ( e5332e...a89400 )
by Adrian
02:09
created

HasChildrenTrait   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 208
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 96.67%

Importance

Changes 8
Bugs 1 Features 1
Metric Value
wmc 26
c 8
b 1
f 1
lcom 1
cbo 1
dl 0
loc 208
ccs 58
cts 60
cp 0.9667
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getFullChildName() 0 4 1
A setElementFactory() 0 7 1
B addElement() 0 32 4
A createChildren() 0 9 3
A getElement() 0 4 2
A removeElement() 0 8 2
A hasElement() 0 4 1
C childComparator() 0 22 7
A getElements() 0 7 1
A cleanUpMissingGroups() 0 9 4
1
<?php
2
3
namespace Sirius\Input\Traits;
4
5
use Sirius\Input\Element;
6
use Sirius\Input\Element\Factory as ElementFactory;
7
use Sirius\Input\Element\FactoryAwareInterface;
8
use Sirius\Input\Specs;
9
10
trait HasChildrenTrait
11
{
12
    /**
13
     * List of the child element
14
     *
15
     * @var array
16
     */
17
    protected $elements = array();
18
19
    /**
20
     * Value to keep track of the order the elements were added
21
     *
22
     * @var int
23
     */
24
    protected $elementsIndex = PHP_INT_MAX;
25
26
    /**
27
     * @var ElementFactory
28
     */
29
    protected $elementFactory;
30
31
    /**
32
     * Generates the actual name that will be used to identify the element in the input object
33
     * For input objects the name of the child is the same as the name provided,
34
     * For field-sets the name of the child is prefixed/name-spaced with the name of the field-set
35
     * For collections the name of the child is prefixed with the name of the collection and an index placeholder
36
     *
37
     * @param string $name
38
     *
39
     * @return string
40
     */
41 12
    protected function getFullChildName($name)
42
    {
43 12
        return $name;
44
    }
45
46
    /**
47
     * Sets the element factory.
48
     * Objects that have children (eg: Fieldset, Collection) must be factory-aware
49
     * This is passed down from form to fieldsets, collections or other elements that have children
50
     *
51
     * @param ElementFactory $elementFactory
52
     * @return $this
53
     */
54 9
    public function setElementFactory(ElementFactory $elementFactory)
55
    {
56 9
        $this->elementFactory = $elementFactory;
57 9
        $this->createChildren();
58
59 9
        return $this;
60
    }
61
62
    /**
63
     * Add an element to the children list
64
     *
65
     * @param string|\Sirius\Input\Element $nameOrElement
66
     * @param array $specs
67
     *
68
     * @throws \RuntimeException
69
     * @return $this
70
     */
71 19
    public function addElement($nameOrElement, $specs = array())
72
    {
73 19
        if (is_string($nameOrElement)) {
74
75 16
            $name = $nameOrElement;
76 16
            $element = $this->elementFactory->createFromOptions($this->getFullChildName($nameOrElement), $specs);
77
78 19
        } elseif ($nameOrElement instanceof Element) {
79
80 4
            $element = $nameOrElement;
81
            // for an element with the name 'address[street]' we get only the 'street'
82
            // because we assume the element has the name constructed using the rule in getFullChildName()
83 4
            $parts = explode('[', str_replace(']', '', $element->getName()));
84 4
            $name = array_pop($parts);
85
86 4
        } else {
87
88 1
            throw new \RuntimeException(sprintf('Variable $nameorElement must be a string or an instance of the Element class'));
89
90
        }
91
92
        // add the index for sorting
93 18
        if (!isset($element['__index'])) {
94
95 18
            $element['__index'] = ($this->elementsIndex--);
96
97 18
        }
98
99 18
        $this->elements[$name] = $element;
100
101 18
        return $this;
102
    }
103
104
    /**
105
     * Create the children using the factory.
106
     * This will be called by elements that are factory-aware (eg: Fieldsets)
107
     */
108 9
    protected function createChildren()
109
    {
110
        /* @var $this \ArrayObject */
111 9
        if (isset($this[Specs::CHILDREN])) {
112 1
            foreach ($this[Specs::CHILDREN] as $name => $options) {
113 1
                $this->addElement($name, $options);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ArrayObject as the method addElement() does only exist in the following sub-classes of ArrayObject: Sirius\Input\Element\Collection, Sirius\Input\Element\Fieldset, Sirius\Input\InputFilter. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
114 1
            }
115 1
        }
116 9
    }
117
118
    /**
119
     * Retrieve an element by name
120
     *
121
     * @param string $name
122
     *
123
     * @return \Sirius\Input\Element
124
     */
125 9
    public function getElement($name)
126
    {
127 9
        return isset($this->elements[$name]) ? $this->elements[$name] : null;
128
    }
129
130
    /**
131
     * Removes an element from the children list
132
     *
133
     * @param string $name
134
     *
135
     * @throws \RuntimeException
136
     * @return $this
137
     */
138 3
    public function removeElement($name)
139
    {
140 3
        if (array_key_exists($name, $this->elements)) {
141 3
            unset($this->elements[$name]);
142 3
        }
143
144 3
        return $this;
145
    }
146
147
    /**
148
     * Returns whether an element exist in the children list
149
     *
150
     * @param string $name
151
     *
152
     * @return boolean
153
     */
154 3
    public function hasElement($name)
155
    {
156 3
        return null !== $this->getElement($name);
157
    }
158
159
    /**
160
     * Input comparator callback
161
     *
162
     * @param \ArrayObject $childA
163
     * @param \ArrayObject $childB
164
     *
165
     * @return integer
166
     */
167 7
    protected function childComparator($childA, $childB)
168
    {
169 7
        $posA = isset($childA[Specs::POSITION]) ? $childA[Specs::POSITION] : 0;
170 7
        $posB = isset($childB[Specs::POSITION]) ? $childB[Specs::POSITION] : 0;
171
172 7
        if ($posA < $posB) {
173 3
            return -1;
174
        }
175 7
        if ($posA > $posB) {
176 3
            return 1;
177
        }
178
179 7
        if ($childA['__index'] > $childB['__index']) {
180
            return -1;
181
        }
182 7
        if ($childA['__index'] < $childB['__index']) {
183 7
            return 1;
184
        }
185
186
        // if the priority is the same, childB is first
187
        return -1;
188
    }
189
190
    /**
191
     * Returns the list of the elements organized by priority
192
     *
193
     * @return array
194
     */
195 17
    public function getElements()
196
    {
197
        // first sort the children so they are retrieved by priority
198 17
        uasort($this->elements, array($this, 'childComparator'));
199
200 17
        return $this->elements;
201
    }
202
203
204
    /**
205
     * Unset the group property for elements without a valid group (ie: existing group)
206
     */
207 11
    protected function cleanUpMissingGroups()
208
    {
209 11
        foreach ($this->elements as $element) {
210 9
            $group = $element->getGroup();
211 9
            if ($group && !isset($this->elements[$group])) {
212 1
                $element->setGroup(null);
213 1
            }
214 11
        }
215 11
    }
216
217
}
218