Completed
Push — master ( 7a8290...bcd23f )
by Askupa
03:29
created

Form::update()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 2
dl 0
loc 17
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 $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 (retrieved from the POST request).
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
     * 
41
     * @var array Final values array. 
42
     */
43
    private $final_instance = array();
44
    
45
    /**
46
     * Array of names of components that were invalid,
47
     * and the error message recieved.
48
     * Structure: component_name => error_message
49
     * 
50
     * @var string[] Array of error messages. 
51
     */
52
    private $errors = array();
53
    
54
    /**
55
     * When instantiating a form, a list of component argument arrays must be provided.
56
     * Each argument array must have the following arguments, in addition to the
57
     * component's original arguments:
58
     * 
59
     * type (string) - the component's type
60
     * default (mix) - the component's default value
61
     * filter (callable) - (optional) the component's value filter callback
62
     * validation (callable) - (optional) the component's value validation callback
63
     * 
64
     * @param array $components An array of arrays of component arguments
65
     */
66
    public function __construct( array $components = array() )
67
    {
68
        foreach( $components as $args )
69
        {
70
            $name = $args['name'];
71
            if(array_key_exists($name, $components))
72
            {
73
                throw new \RuntimeException("A component with the name <b>$name</b> has already been created");
74
            }
75
            $this->components[$name] = ComponentFactory::create($args['type'], $args);
76
        }
77
    }
78
    
79
    /**
80
     * Get the updated component values (validated, filtered or ignored).
81
     * 
82
     * Loops through each component and acts according to its type:
83
     * - Disableable components are ignored if they are disabled.
84
     * - Validatable components are validated using their validation function. 
85
     *   If the new value is invalid, the old value will be used.
86
     * - Filterable components are filtered using their filter function.
87
     * - Non-value components are skipped altogether.
88
     * 
89
     * Each component is also set with its new value.
90
     * 
91
     * @param array $new_instance The new component values array.
92
     * @param array $old_instance The old component values array.
93
     * 
94
     * @return array The updated values array.
95
     */
96
    public function update( array $new_instance, array $old_instance = array() )
97
    {
98
        $this->old_instance   = array_merge($this->get_defaults(),$old_instance);
99
        $this->new_instance   = array_merge($this->old_instance,$new_instance);
100
        $this->final_instance = $this->new_instance;
101
        
102
        foreach ( $this->components as $component ) 
103
        {
104
            // Update individual fields, as well as the composite parent field.
105
            if ( $component instanceof ValueComponentInterface )
106
            {
107
                $this->update_component( $component );
108
            }
109
        }
110
        
111
        return $this->final_instance;
112
    }
113
    
114
    /**
115
     * Reset all fields to their default values.
116
     * 
117
     * @param array $names List of component names to be set to their defaults. If no names are specified, all components will be reset
118
     * @return array The updated values array.
119
     */
120
    public function reset( array $names = array() )
121
    {
122
        if( array() === $names )
123
        {
124
            // Unset new instance to force reset
125
            $this->new_instance = array();
126
            return $this->update();
0 ignored issues
show
Bug introduced by
The call to update() misses a required argument $new_instance.

This check looks for function calls that miss required arguments.

Loading history...
127
        }
128
        else
129
        {
130
            foreach( $this->components as $c )
131
            {
132
                if( in_array($c->name, $names) )
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...
133
                {
134
                    $this->new_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...
135
                }
136
            }
137
            return $this->update();
0 ignored issues
show
Bug introduced by
The call to update() misses a required argument $new_instance.

This check looks for function calls that miss required arguments.

Loading history...
138
        }
139
    }
140
    
141
    /**
142
     * Get the list of error messages for components that could not be validated.
143
     * Structure: components_name => error_message
144
     * 
145
     * @return string[] Array of error messages. 
146
     */
147
    public function get_errors()
148
    {
149
        return $this->errors;
150
    }
151
    
152
    /**
153
     * Update the component's value with the new value.
154
     * NOTE: this function also updates the $final_instance
155
     * array.
156
     * 
157
     * @param ValueComponentInterface $component The component to validate.
158
     */
159
    private function update_component( ValueComponentInterface $component )
160
    {
161
        $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...
162
        
163
        // Skip if this field is disabled
164
        if($component instanceof DisableableComponentInterface &&
165
           true === $component->disabled) {
166
            return;
167
        }
168
        
169
        // Apply user-defined filter
170
        if( $component instanceof FilterableComponentInterface )
171
        {
172
            $this->filter( $component );
173
        }
174
        
175
        // Validate value
176
        if( $component instanceof ValidatableComponentInterface )
177
        {
178
            $this->validate( $component );
179
        }
180
    }
181
    
182
    /**
183
     * Filter the component's value using its filter function (if applicable)
184
     * 
185
     * @param UI\FilterableComponentInterface $component
186
     */
187
    private function filter( FilterableComponentInterface $component )
188
    {
189
        $filter = $component->filter;
0 ignored issues
show
Bug introduced by
Accessing filter on the interface Amarkal\UI\FilterableComponentInterface 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...
190
        
191
        if( is_callable( $filter ) ) 
192
        {
193
            $component->value = $filter( $this->final_instance[$component->name] );
0 ignored issues
show
Bug introduced by
Accessing value on the interface Amarkal\UI\FilterableComponentInterface 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\FilterableComponentInterface 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...
194
            $this->final_instance[$component->name] = $component->value;
0 ignored issues
show
Bug introduced by
Accessing name on the interface Amarkal\UI\FilterableComponentInterface 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 value on the interface Amarkal\UI\FilterableComponentInterface 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...
195
        }
196
    }
197
    
198
    /**
199
     * Validate the component's value using its validation function.
200
     * 
201
     * If the value is invalid, the old value is used, and an error message is
202
     * saved into the errors array as component_name => error_message.
203
     * 
204
     * @param ValidatableComponentInterface $component The component to validate.
205
     */
206
    private function validate( ValidatableComponentInterface $component )
207
    {
208
        $name     = $component->name;
0 ignored issues
show
Bug introduced by
Accessing name on the interface Amarkal\UI\ValidatableComponentInterface 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...
209
        $validate = $component->validation;
0 ignored issues
show
Bug introduced by
Accessing validation on the interface Amarkal\UI\ValidatableComponentInterface 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...
210
        $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...
211
        
212
        $component->validity = $component::VALID;
0 ignored issues
show
Bug introduced by
Accessing validity on the interface Amarkal\UI\ValidatableComponentInterface 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...
213
        
214
        if(is_callable($validate))
215
        {
216
            $valid = $validate($this->new_instance[$name]);
217
            
218
            // Invalid input, use old instance or default value
219
            if ( true !== $valid ) 
220
            {
221
                $this->errors[$name]         = $valid;
222
                $component->value            = $this->old_instance[$name];
0 ignored issues
show
Bug introduced by
Accessing value on the interface Amarkal\UI\ValidatableComponentInterface 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...
223
                $component->validity         = $component::INVALID;
0 ignored issues
show
Bug introduced by
Accessing validity on the interface Amarkal\UI\ValidatableComponentInterface 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...
224
                $this->final_instance[$name] = $this->old_instance[$name];
225
            }
226
        }
227
    }
228
    
229
    private function get_defaults()
230
    {
231
        $defaults = array();
232
        
233
        foreach( $this->components as $component )
234
        {
235
            $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...
236
        }
237
        
238
        return $defaults;
239
    }
240
}