Form::reset()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 6
rs 9.4285
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 $component_list;
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 ComponentList $component_list
61
     */
62
    public function __construct( ComponentList $component_list = null )
63
    {
64
        if(null === $component_list)
65
        {
66
            $component_list = new ComponentList();
67
        }
68
        $this->component_list = $component_list;
0 ignored issues
show
Documentation Bug introduced by
It seems like $component_list of type object<Amarkal\UI\ComponentList> is incompatible with the declared type array<integer,object<Ama...\UI\AbstractComponent>> of property $component_list.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
69
    }
70
    
71
    /**
72
     * Get the updated component values (validated, filtered or ignored).
73
     * 
74
     * Loops through each component and acts according to its type:
75
     * - Disableable components are ignored if they are disabled.
76
     * - Validatable components are validated using their validation function. 
77
     *   If the new value is invalid, the old value will be used.
78
     * - Filterable components are filtered using their filter function.
79
     * - Non-value components are skipped altogether.
80
     * 
81
     * Each component is also set with its new value.
82
     * 
83
     * @param array $new_instance The new component values array.
84
     * @param array $old_instance The old component values array.
85
     * 
86
     * @return array The updated values array.
87
     */
88
    public function update( array $new_instance = array(), array $old_instance = array() )
89
    {
90
        $this->old_instance   = array_merge($this->get_defaults(),$old_instance);
91
        $this->new_instance   = array_merge($this->old_instance,$new_instance);
92
        $this->final_instance = $this->new_instance;
93
        
94
        foreach ( $this->component_list->get_value_components() as $component ) 
0 ignored issues
show
Bug introduced by
The method get_value_components cannot be called on $this->component_list (of type array<integer,object<Ama...\UI\AbstractComponent>>).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
95
        {
96
            // Update individual fields, as well as the composite parent field.
97
            $this->update_component( $component );
98
        }
99
        
100
        return $this->final_instance;
101
    }
102
    
103
    /**
104
     * Reset all fields to their default values.
105
     * 
106
     * @return array The updated values array.
107
     */
108
    public function reset()
109
    {
110
        return $this->reset_components(
111
            $this->component_list->get_value_components()
0 ignored issues
show
Bug introduced by
The method get_value_components cannot be called on $this->component_list (of type array<integer,object<Ama...\UI\AbstractComponent>>).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
112
        );
113
    }
114
115
    /**
116
     * Reset the given list of components to their default values.
117
     *
118
     * @param array $components
119
     * @return void
120
     */
121
    public function reset_components( array $components )
122
    {
123
        foreach( $components as $component )
124
        {
125
            $this->reset_component($component);
126
        }
127
        return $this->final_instance;
128
    }
129
130
    /**
131
     * Reset the given component to its default value.
132
     *
133
     * @param AbstractComponent $component
134
     * @return void
135
     */
136
    public function reset_component( $component )
137
    {
138
        $component->value = $component->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...
139
        $this->final_instance[$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...
140
        return $this->final_instance;
141
    }
142
    
143
    /**
144
     * Get the list of error messages for components that could not be validated.
145
     * Structure: components_name => error_message
146
     * 
147
     * @return string[] Array of error messages. 
148
     */
149
    public function get_errors()
150
    {
151
        return $this->errors;
152
    }
153
    
154
    /**
155
     * Get all components.
156
     * 
157
     * @return array
158
     */
159
    public function get_component_list()
160
    {
161
        return $this->component_list;
162
    }
163
    
164
    /**
165
     * Update the component's value with the new value.
166
     * NOTE: this function also updates the $final_instance
167
     * array.
168
     * 
169
     * @param ValueComponentInterface $component The component to validate.
170
     */
171
    private function update_component( ValueComponentInterface $component )
172
    {
173
        $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...
174
        
175
        // Skip if this field is disabled
176
        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...
177
        {
178
            return;
179
        }
180
181
        // Apply user-defined filter
182
        $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...
183
        
184
        // Validate value
185
        $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...
186
    }
187
    
188
    /**
189
     * Check if the given component is disabled.
190
     * 
191
     * @param UI\AbstractComponent $component
192
     * @return boolean
193
     */
194
    private function is_disabled( $component )
195
    {
196
        return 
197
            $component instanceof DisableableComponentInterface &&
198
            (
199
                true === $component->disabled ||
200
                true === $component->readonly
201
            );
202
    }
203
204
    /**
205
     * Filter the component's value using its filter function (if applicable)
206
     * 
207
     * @param UI\AbstractComponent $component
208
     */
209
    private function filter( $component )
210
    {
211
        if( $component instanceof FilterableComponentInterface )
212
        {
213
            $filter = $component->filter;
214
215
            if( is_callable( $filter ) ) 
216
            {
217
                $component->value = \call_user_func_array($filter, array($this->final_instance[$component->name]));
218
                $this->final_instance[$component->name] = $component->value;
219
            }
220
        }
221
    }
222
    
223
    /**
224
     * Validate the component's value using its validation function.
225
     * 
226
     * If the value is invalid, the old value is used, and an error message is
227
     * saved into the errors array as component_name => error_message.
228
     * 
229
     * @param UI\AbstractComponent $component The component to validate.
230
     */
231
    private function validate( $component )
232
    {
233
        if( !($component instanceof ValidatableComponentInterface) ) return;
234
        
235
        $name     = $component->name;
236
        $validate = $component->validation;
237
        
238
        $component->validity = $component::VALID;
239
        
240
        if(is_callable($validate))
241
        {
242
            $error = '';
243
            $valid = \call_user_func_array($validate, array($this->final_instance[$name], &$error));
244
            
245
            // Invalid input, use old instance or default value
246
            if ( true !== $valid ) 
247
            {
248
                $this->errors[$name]         = $error ? $error : ValidatableComponentInterface::DEFAULT_MESSAGE;
249
                $component->value            = $this->old_instance[$name];
250
                $this->final_instance[$name] = $this->old_instance[$name];
251
                $component->set_validity($component::INVALID);
252
            }
253
        }
254
    }
255
    
256
    /**
257
     * Get the default values for all form components as an array of name => default_value
258
     * 
259
     * @return array
260
     */
261
    private function get_defaults()
262
    {
263
        $defaults = array();
264
        
265
        foreach( $this->component_list->get_value_components() as $component )
0 ignored issues
show
Bug introduced by
The method get_value_components cannot be called on $this->component_list (of type array<integer,object<Ama...\UI\AbstractComponent>>).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
266
        {
267
            $defaults[$component->name] = $component->default;
268
        }
269
        
270
        return $defaults;
271
    }
272
}