HasChildrenTrait::cleanUpMissingGroups()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 9
ccs 8
cts 8
cp 1
rs 9.2
cc 4
eloc 5
nc 3
nop 0
crap 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\Specs;
8
9
trait HasChildrenTrait
10
{
11
    /**
12
     * List of the child element
13
     *
14
     * @var array
15
     */
16
    protected $elements = array();
17
18
    /**
19
     * Value to keep track of the order the elements were added
20
     *
21
     * @var int
22
     */
23
    protected $elementsIndex = PHP_INT_MAX;
24
25
    /**
26
     * @var ElementFactory
27
     */
28
    protected $elementFactory;
29
30
    /**
31
     * Generates the actual name that will be used to identify the element in the input object
32
     * For input objects the name of the child is the same as the name provided,
33
     * For field-sets the name of the child is prefixed/name-spaced with the name of the field-set
34
     * For collections the name of the child is prefixed with the name of the collection and an index placeholder
35
     *
36
     * @param string $name
37
     *
38
     * @return string
39
     */
40 12
    protected function getFullChildName($name)
41
    {
42 12
        return $name;
43
    }
44
45
    /**
46
     * Sets the element factory.
47
     * Objects that have children (eg: Fieldset, Collection) must be factory-aware
48
     * This is passed down from form to fieldsets, collections or other elements that have children
49
     *
50
     * @param ElementFactory $elementFactory
51
     * @return $this
52
     */
53 9
    public function setElementFactory(ElementFactory $elementFactory)
54
    {
55 9
        $this->elementFactory = $elementFactory;
56 9
        $this->createChildren();
57
58 9
        return $this;
59
    }
60
61
    /**
62
     * Add an element to the children list
63
     *
64
     * @param string|\Sirius\Input\Element $nameOrElement
65
     * @param array $specs
66
     *
67
     * @throws \RuntimeException
68
     * @return $this
69
     */
70 19
    public function addElement($nameOrElement, $specs = array())
71
    {
72 19
        if (is_string($nameOrElement)) {
73
74 16
            $name = $nameOrElement;
75 16
            $element = $this->elementFactory->createFromOptions($this->getFullChildName($nameOrElement), $specs);
76
77 19
        } elseif ($nameOrElement instanceof Element) {
78
79 4
            $element = $nameOrElement;
80
            // for an element with the name 'address[street]' we get only the 'street'
81
            // because we assume the element has the name constructed using the rule in getFullChildName()
82 4
            $parts = explode('[', str_replace(']', '', $element->getName()));
83 4
            $name = array_pop($parts);
84
85 4
        } else {
86
87 1
            throw new \RuntimeException(sprintf('Variable $nameorElement must be a string or an instance of the Element class'));
88
89
        }
90
91 18
        $this->ensureGroupExists($element);
92
93
        // add the index for sorting
94 18
        if (!isset($element['__index'])) {
95
96 18
            $element['__index'] = ($this->elementsIndex--);
97
98 18
        }
99
100 18
        $this->elements[$name] = $element;
101
102 18
        return $this;
103
    }
104
105
    /**
106
     * Make sure a Group type element is added to the children if the element has a group
107
     *
108
     * @param Element $element
109
     */
110 18
    protected function ensureGroupExists(Element $element) {
111
112 18
        if (!$element->getGroup() || $this->hasElement($element->getGroup())) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $element->getGroup() of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
113 18
            return;
114
        }
115
116 1
        $this->addElement($element->getGroup(), array(
117 1
            Specs::TYPE => 'group',
118 1
            Specs::POSITION => $element->getPosition()
119 1
        ));
120 1
    }
121
122
    /**
123
     * Create the children using the factory.
124
     * This will be called by elements that are factory-aware (eg: Fieldsets)
125
     */
126 9
    protected function createChildren()
127
    {
128
        /* @var $this \ArrayObject */
129 9
        if (isset($this[Specs::CHILDREN])) {
130 1
            foreach ($this[Specs::CHILDREN] as $name => $options) {
131 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...
132 1
            }
133 1
        }
134 9
    }
135
136
    /**
137
     * Retrieve an element by name
138
     *
139
     * @param string $name
140
     *
141
     * @return \Sirius\Input\Element
142
     */
143 10
    public function getElement($name)
144
    {
145 10
        return isset($this->elements[$name]) ? $this->elements[$name] : null;
146
    }
147
148
    /**
149
     * Removes an element from the children list
150
     *
151
     * @param string $name
152
     *
153
     * @throws \RuntimeException
154
     * @return $this
155
     */
156 4
    public function removeElement($name)
157
    {
158 4
        if (array_key_exists($name, $this->elements)) {
159 4
            unset($this->elements[$name]);
160 4
        }
161
162 4
        return $this;
163
    }
164
165
    /**
166
     * Returns whether an element exist in the children list
167
     *
168
     * @param string $name
169
     *
170
     * @return boolean
171
     */
172 4
    public function hasElement($name)
173
    {
174 4
        return null !== $this->getElement($name);
175
    }
176
177
    /**
178
     * Input comparator callback
179
     *
180
     * @param \ArrayObject $childA
181
     * @param \ArrayObject $childB
182
     *
183
     * @return integer
184
     */
185 7
    protected function childComparator($childA, $childB)
186
    {
187 7
        $posA = isset($childA[Specs::POSITION]) ? $childA[Specs::POSITION] : 0;
188 7
        $posB = isset($childB[Specs::POSITION]) ? $childB[Specs::POSITION] : 0;
189
190 7
        if ($posA < $posB) {
191 3
            return -1;
192
        }
193 7
        if ($posA > $posB) {
194 3
            return 1;
195
        }
196
197 7
        if ($childA['__index'] > $childB['__index']) {
198
            return -1;
199
        }
200 7
        if ($childA['__index'] < $childB['__index']) {
201 7
            return 1;
202
        }
203
204
        // if the priority is the same, childB is first
205
        return -1;
206
    }
207
208
    /**
209
     * Returns the list of the elements organized by priority
210
     *
211
     * @return array
212
     */
213 17
    public function getElements()
214
    {
215
        // first sort the children so they are retrieved by priority
216 17
        uasort($this->elements, array($this, 'childComparator'));
217
218 17
        return $this->elements;
219
    }
220
221
222
    /**
223
     * Unset the group property for elements without a valid group (ie: existing group)
224
     */
225 11
    protected function cleanUpMissingGroups()
226
    {
227 11
        foreach ($this->elements as $element) {
228 9
            $group = $element->getGroup();
229 9
            if ($group && !isset($this->elements[$group])) {
230 1
                $element->setGroup(null);
231 1
            }
232 11
        }
233 11
    }
234
235
}
236