Completed
Push — master ( bcd23f...ead358 )
by Askupa
01:57
created

Form::is_disabled()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 1
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
namespace Amarkal\UI;
4
5
/**
6
 * Implements a Form controller.
7
 * 
8
 * The form object is used to encapsulate UI components and the update/validation 
9
 * process into a single entity.
10
 */
11
class Form
12
{
13
    /**
14
     * The list of AbstractComponent type objects to be updated.
15
     * 
16
     * @var AbstractComponent[] objects array.
17
     */
18
    private $components;
19
    
20
    /**
21
     * The old component values array. These values are used if the new values
22
     * are invalid.
23
     * Structure: component_name => component_value
24
     * 
25
     * @var array Old values array.
26
     */
27
    private $old_instance;
28
    
29
    /**
30
     * The new component values array.
31
     * Structure: component_name => component_value
32
     * 
33
     * @var array New values array.
34
     */
35
    private $new_instance;
36
    
37
    /**
38
     * The final values array, after filtering and validation.
39
     * This is returned by the update function.
40
     * Structure: component_name => component_value
41
     * 
42
     * @var array Final values array. 
43
     */
44
    private $final_instance = array();
45
    
46
    /**
47
     * Array of names of components that were invalid, and the error message 
48
     * recieved.
49
     * Structure: component_name => error_message
50
     * 
51
     * @var string[] Array of error messages. 
52
     */
53
    private $errors = array();
54
    
55
    /**
56
     * When instantiating a form, a list of component arguments arrays must be 
57
     * provided. Each arguments array must have a 'type' argument, in addition 
58
     * to the component's original arguments.
59
     * 
60
     * @param array $components An array of arrays of component arguments
61
     */
62
    public function __construct( array $components = array() )
63
    {
64
        foreach( $components as $args )
65
        {
66
            $name = $args['name'];
67
            if(array_key_exists($name, $components))
68
            {
69
                throw new \RuntimeException("A component with the name <b>$name</b> has already been created");
70
            }
71
            $this->components[$name] = ComponentFactory::create($args['type'], $args);
72
        }
73
    }
74
    
75
    /**
76
     * Get the updated component values (validated, filtered or ignored).
77
     * 
78
     * Loops through each component and acts according to its type:
79
     * - Disableable components are ignored if they are disabled.
80
     * - Validatable components are validated using their validation function. 
81
     *   If the new value is invalid, the old value will be used.
82
     * - Filterable components are filtered using their filter function.
83
     * - Non-value components are skipped altogether.
84
     * 
85
     * Each component is also set with its new value.
86
     * 
87
     * @param array $new_instance The new component values array.
88
     * @param array $old_instance The old component values array.
89
     * 
90
     * @return array The updated values array.
91
     */
92
    public function update( array $new_instance, array $old_instance = array() )
93
    {
94
        $this->old_instance   = array_merge($this->get_defaults(),$old_instance);
95
        $this->new_instance   = array_merge($this->old_instance,$new_instance);
96
        $this->final_instance = $this->new_instance;
97
        
98
        foreach ( $this->components as $component ) 
99
        {
100
            // Update individual fields, as well as the composite parent field.
101
            if ( $component instanceof ValueComponentInterface )
102
            {
103
                $this->update_component( $component );
104
            }
105
        }
106
        
107
        return $this->final_instance;
108
    }
109
    
110
    /**
111
     * Reset all fields to their default values.
112
     * 
113
     * @return array The updated values array.
114
     */
115
    public function reset()
116
    {
117
        foreach( $this->components as $c )
118
        {
119
            $c->value = $c->default;
0 ignored issues
show
Documentation introduced by
The property value does not exist on object<Amarkal\UI\AbstractComponent>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Documentation introduced by
The property default does not exist on object<Amarkal\UI\AbstractComponent>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
120
            $this->final_instance[$c->name] = $c->default;
0 ignored issues
show
Documentation introduced by
The property name does not exist on object<Amarkal\UI\AbstractComponent>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Documentation introduced by
The property default does not exist on object<Amarkal\UI\AbstractComponent>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
121
        }
122
        return $this->final_instance;
123
    }
124
    
125
    /**
126
     * Get the list of error messages for components that could not be validated.
127
     * Structure: components_name => error_message
128
     * 
129
     * @return string[] Array of error messages. 
130
     */
131
    public function get_errors()
132
    {
133
        return $this->errors;
134
    }
135
    
136
    /**
137
     * Update the component's value with the new value.
138
     * NOTE: this function also updates the $final_instance
139
     * array.
140
     * 
141
     * @param ValueComponentInterface $component The component to validate.
142
     */
143
    private function update_component( ValueComponentInterface $component )
144
    {
145
        $component->value = $this->final_instance[$component->name];
0 ignored issues
show
Bug introduced by
Accessing value on the interface Amarkal\UI\ValueComponentInterface 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...
Bug introduced by
Accessing name on the interface Amarkal\UI\ValueComponentInterface 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...
146
        
147
        // Skip if this field is disabled
148
        if( $this->is_disabled($component) )
0 ignored issues
show
Documentation introduced by
$component is of type object<Amarkal\UI\ValueComponentInterface>, but the function expects a object<Amarkal\UI\UI\AbstractComponent>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
149
        {
150
            return;
151
        }
152
        
153
        // Apply user-defined filter
154
        $this->filter( $component );
0 ignored issues
show
Documentation introduced by
$component is of type object<Amarkal\UI\ValueComponentInterface>, but the function expects a object<Amarkal\UI\UI\AbstractComponent>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
155
        
156
        // Validate value
157
        $this->validate( $component );
0 ignored issues
show
Documentation introduced by
$component is of type object<Amarkal\UI\ValueComponentInterface>, but the function expects a object<Amarkal\UI\UI\AbstractComponent>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
158
    }
159
    
160
    /**
161
     * Check if the given component is disabled.
162
     * 
163
     * @param UI\AbstractComponent $component
164
     * @return boolean
165
     */
166
    private function is_disabled( $component )
167
    {
168
        return 
169
            $component instanceof DisableableComponentInterface &&
170
            (
171
                true === $component->disabled ||
172
                true === $component->readonly
173
            );
174
    }
175
    
176
    /**
177
     * Filter the component's value using its filter function (if applicable)
178
     * 
179
     * @param UI\AbstractComponent $component
180
     */
181
    private function filter( $component )
182
    {
183
        if( $component instanceof FilterableComponentInterface )
184
        {
185
            $filter = $component->filter;
186
187
            if( is_callable( $filter ) ) 
188
            {
189
                $component->value = $filter( $this->final_instance[$component->name] );
190
                $this->final_instance[$component->name] = $component->value;
191
            }
192
        }
193
    }
194
    
195
    /**
196
     * Validate the component's value using its validation function.
197
     * 
198
     * If the value is invalid, the old value is used, and an error message is
199
     * saved into the errors array as component_name => error_message.
200
     * 
201
     * @param UI\AbstractComponent $component The component to validate.
202
     */
203
    private function validate( $component )
204
    {
205
        if( !($component instanceof ValidatableComponentInterface) ) return;
206
        
207
        $name     = $component->name;
208
        $validate = $component->validation;
209
        $valid    = true;
0 ignored issues
show
Unused Code introduced by
$valid 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...
210
        
211
        $component->validity = $component::VALID;
212
        
213
        if(is_callable($validate))
214
        {
215
            $valid = $validate($this->new_instance[$name]);
216
            
217
            // Invalid input, use old instance or default value
218
            if ( true !== $valid ) 
219
            {
220
                $this->errors[$name]         = $valid;
221
                $component->value            = $this->old_instance[$name];
222
                $component->validity         = $component::INVALID;
223
                $this->final_instance[$name] = $this->old_instance[$name];
224
            }
225
        }
226
    }
227
    
228
    /**
229
     * Get the default values for all form components as an array of name => default_value
230
     * 
231
     * @return array
232
     */
233
    private function get_defaults()
234
    {
235
        $defaults = array();
236
        
237
        foreach( $this->components as $component )
238
        {
239
            $defaults[$component->name] = $component->default;
0 ignored issues
show
Documentation introduced by
The property name does not exist on object<Amarkal\UI\AbstractComponent>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Documentation introduced by
The property default does not exist on object<Amarkal\UI\AbstractComponent>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
240
        }
241
        
242
        return $defaults;
243
    }
244
}