Completed
Push — master ( 0f6fd0...ea8606 )
by Askupa
01:59
created

Form::add_component()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
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 = array();
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
        $this->add_components($components);
65
    }
66
    
67
    /**
68
     * Get the updated component values (validated, filtered or ignored).
69
     * 
70
     * Loops through each component and acts according to its type:
71
     * - Disableable components are ignored if they are disabled.
72
     * - Validatable components are validated using their validation function. 
73
     *   If the new value is invalid, the old value will be used.
74
     * - Filterable components are filtered using their filter function.
75
     * - Non-value components are skipped altogether.
76
     * 
77
     * Each component is also set with its new value.
78
     * 
79
     * @param array $new_instance The new component values array.
80
     * @param array $old_instance The old component values array.
81
     * 
82
     * @return array The updated values array.
83
     */
84
    public function update( array $new_instance = array(), array $old_instance = array() )
85
    {
86
        $this->old_instance   = array_merge($this->get_defaults(),$old_instance);
87
        $this->new_instance   = array_merge($this->old_instance,$new_instance);
88
        $this->final_instance = $this->new_instance;
89
        
90
        foreach ( $this->components as $component ) 
91
        {
92
            // Update individual fields, as well as the composite parent field.
93
            if ( $component instanceof ValueComponentInterface )
94
            {
95
                $this->update_component( $component );
96
            }
97
        }
98
        
99
        return $this->final_instance;
100
    }
101
    
102
    /**
103
     * Reset all fields to their default values.
104
     * 
105
     * @return array The updated values array.
106
     */
107
    public function reset()
108
    {
109
        foreach( $this->components as $c )
110
        {
111
            $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...
112
            $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...
113
        }
114
        return $this->final_instance;
115
    }
116
    
117
    /**
118
     * Get the list of error messages for components that could not be validated.
119
     * Structure: components_name => error_message
120
     * 
121
     * @return string[] Array of error messages. 
122
     */
123
    public function get_errors()
124
    {
125
        return $this->errors;
126
    }
127
    
128
    /**
129
     * Add a component to the form.
130
     * 
131
     * @param array $args The component arguments
132
     * @throws \RuntimeException If a component with the same name has already been registered
133
     */
134
    public function add_component( array $args )
135
    {
136
        $name = $args['name'];
137
        if(array_key_exists($name, $this->components))
138
        {
139
            throw new \RuntimeException("A component with the name <b>$name</b> has already been created");
140
        }
141
        $this->components[$name] = ComponentFactory::create($args['type'], $args);
142
    }
143
    
144
    /**
145
     * Add multiple components to the form.
146
     * 
147
     * @param array $components
148
     */
149
    public function add_components( array $components )
150
    {
151
        foreach( $components as $component )
152
        {
153
            $this->add_component($component);
154
        }
155
    }
156
    
157
    /**
158
     * Get a component by its name.
159
     * 
160
     * @param string $name
161
     * @return UI\AbstractComponent
162
     */
163
    public function get_component( $name )
164
    {
165
        return $this->components[$name];
166
    }
167
    
168
    /**
169
     * Get all components.
170
     * 
171
     * @return array
172
     */
173
    public function get_components()
174
    {
175
        return $this->components;
176
    }
177
    
178
    /**
179
     * Update the component's value with the new value.
180
     * NOTE: this function also updates the $final_instance
181
     * array.
182
     * 
183
     * @param ValueComponentInterface $component The component to validate.
184
     */
185
    private function update_component( ValueComponentInterface $component )
186
    {
187
        $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...
188
        
189
        // Skip if this field is disabled
190
        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...
191
        {
192
            return;
193
        }
194
        
195
        // Apply user-defined filter
196
        $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...
197
        
198
        // Validate value
199
        $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...
200
    }
201
    
202
    /**
203
     * Check if the given component is disabled.
204
     * 
205
     * @param UI\AbstractComponent $component
206
     * @return boolean
207
     */
208
    private function is_disabled( $component )
209
    {
210
        return 
211
            $component instanceof DisableableComponentInterface &&
212
            (
213
                true === $component->disabled ||
214
                true === $component->readonly
215
            );
216
    }
217
    
218
    /**
219
     * Filter the component's value using its filter function (if applicable)
220
     * 
221
     * @param UI\AbstractComponent $component
222
     */
223
    private function filter( $component )
224
    {
225
        if( $component instanceof FilterableComponentInterface )
226
        {
227
            $filter = $component->filter;
228
229
            if( is_callable( $filter ) ) 
230
            {
231
                $component->value = \call_user_func_array($filter, array($this->final_instance[$component->name]));
232
                $this->final_instance[$component->name] = $component->value;
233
            }
234
        }
235
    }
236
    
237
    /**
238
     * Validate the component's value using its validation function.
239
     * 
240
     * If the value is invalid, the old value is used, and an error message is
241
     * saved into the errors array as component_name => error_message.
242
     * 
243
     * @param UI\AbstractComponent $component The component to validate.
244
     */
245
    private function validate( $component )
246
    {
247
        if( !($component instanceof ValidatableComponentInterface) ) return;
248
        
249
        $name     = $component->name;
250
        $validate = $component->validation;
251
        
252
        $component->validity = $component::VALID;
253
        
254
        if(is_callable($validate))
255
        {
256
            $error = '';
257
            $valid = \call_user_func_array($validate, array($this->final_instance[$name], &$error));
258
            
259
            // Invalid input, use old instance or default value
260
            if ( true !== $valid ) 
261
            {
262
                $this->errors[$name]         = $error ? $error : ValidatableComponentInterface::DEFAULT_MESSAGE;
263
                $component->value            = $this->old_instance[$name];
264
                $component->validity         = $component::INVALID;
265
                $this->final_instance[$name] = $this->old_instance[$name];
266
            }
267
        }
268
    }
269
    
270
    /**
271
     * Get the default values for all form components as an array of name => default_value
272
     * 
273
     * @return array
274
     */
275
    private function get_defaults()
276
    {
277
        $defaults = array();
278
        
279
        foreach( $this->components as $component )
280
        {
281
            $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...
282
        }
283
        
284
        return $defaults;
285
    }
286
}