Completed
Push — master ( b8da3f...f66325 )
by Emily
10s
created

PropertyAccessor::setScalarValue()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 5
cts 5
cp 1
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 16
nc 3
nop 4
crap 4
1
<?php
2
/**
3
 * This file is part of the Composite Utils package.
4
 *
5
 * (c) Emily Shepherd <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the
8
 * LICENSE.md file that was distributed with this source code.
9
 *
10
 * @package spaark/composite-utils
11
 * @author Emily Shepherd <[email protected]>
12
 * @license MIT
13
 */
14
15
namespace Spaark\CompositeUtils\Service;
16
17
use Spaark\CompositeUtils\Model\Reflection\ReflectionComposite;
18
use Spaark\CompositeUtils\Model\Reflection\ReflectionProperty;
19
use Spaark\CompositeUtils\Model\Reflection\Type\ObjectType;
20
use Spaark\CompositeUtils\Model\Reflection\Type\MixedType;
21
use Spaark\CompositeUtils\Model\Reflection\Type\StringType;
22
use Spaark\CompositeUtils\Model\Reflection\Type\IntegerType;
23
use Spaark\CompositeUtils\Model\Reflection\Type\BooleanType;
24
use Spaark\CompositeUtils\Model\Reflection\Type\CollectionType;
25
use Spaark\CompositeUtils\Exception\CannotWritePropertyException;
26
use Spaark\CompositeUtils\Exception\IllegalPropertyTypeException;
27
use Spaark\CompositeUtils\Exception\MissingRequiredParameterException;
28
29
/**
30
 * This class is used to access properties of a composite and enforce
31
 * data type requirements
32
 */
33
class PropertyAccessor extends RawPropertyAccessor
34
{
35
    /**
36
     * Reflection information about the composite being accessed
37
     *
38
     * @var ReflectionComposite
39
     */
40
    protected $reflect;
41
42
    /**
43
     * Creates the PropertyAccessor for the given object, using the
44
     * given reflection information
45
     *
46
     * @param object $object The object to access
47
     * @param ReflectionComposite $reflect Reflection information about
48
     *     the composite
49
     */
50 22
    public function __construct($object, ReflectionComposite $reflect)
51
    {
52 22
        parent::__construct($object);
53
54 22
        $this->reflect = $reflect;
55 22
    }
56
57
    /**
58
     * Initializes the given object with the given parameters, enforcing
59
     * the constructor requirements and auto building any left overs
60
     */
61 6
    public function constructObject(...$args)
62
    {
63 6
        $i = 0;
64 6
        foreach ($this->reflect->requiredProperties as $property)
65
        {
66 6
            if (!isset($args[$i]))
67
            {
68 2
                throw new MissingRequiredParameterException
69
                (
70 2
                    get_class($this->object),
71 2
                    $property->name
72
                );
73
            }
74
75 4
            $this->setAnyValue($property, $args[$i]);
76
77 4
            $i++;
78
        }
79
80 4
        $building = false;
81 4
        foreach ($this->reflect->optionalProperties as $property)
82
        {
83 4
            if ($building)
84
            {
85 2
                $this->buildProperty($property);
86
            }
87
            else
88
            {
89 4
                if (isset($args[$i]))
90
                {
91 2
                    $this->setAnyValue($property, $args[$i]);
92 2
                    $i++;
93
                }
94
                else
95
                {
96 2
                    $building = true;
97 2
                    $this->buildProperty($property);
98
                }
99
            }
100
        }
101
102 4
        foreach ($this->reflect->builtProperties as $property)
103
        {
104 4
            $this->buildProperty($property);
105
        }
106 4
    }
107
108
    /**
109
     * Builds a property automatically
110
     *
111
     * @param ReflectionProperty $property The property to build
112
     */
113 4
    protected function buildProperty(ReflectionProperty $property)
114
    {
115 4
        if ($property->type instanceof ObjectType)
116
        {
117 3
            $class = $property->type->classname;
118 3
            $this->setRawValue($property->name, new $class());
119
        }
120
        else
121
        {
122 3
            $this->setAnyValue($property, 0);
123
        }
124 4
    }
125
126
    /**
127
     * Returns the value of the property
128
     *
129
     * @param string $property The name of the property to get
130
     * @return mixed The value of the property
131
     */
132 6
    public function getValue(string $property)
133
    {
134 6
        return $this->getRawValue($property);
135
    }
136
137
    /**
138
     * Sets the value of a property, enforcing datatype requirements
139
     *
140
     * @param string $property The name of the property to set
141
     * @param mixed $value The value to set
142
     */
143 9
    public function setValue(string $property, $value)
144
    {
145 9
        if (!$this->reflect->properties->containsKey($property))
0 ignored issues
show
Documentation introduced by
$property is of type string, but the function expects a object<Spaark\CompositeU...del\Collection\KeyType>.

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...
146
        {
147
            throw new CannotWritePropertyException
148
            (
149
                get_class($this->object), $property
150
            );
151
        }
152
153 9
        $this->setAnyValue
154
        (
155 9
            $this->reflect->properties[$property],
156
            $value
157
        );
158 5
    }
159
160
    /**
161
     * Sets the value of a property, enforcing datatype requirements
162
     *
163
     * @param ReflectionProperty $property The property to set
164
     * @param mixed $value The value to set
165
     */
166 10
    protected function setAnyValue(ReflectionProperty $property, $value)
167
    {
168 10
        if (is_null($value))
169
        {
170 2
            $this->setNullValue($property);
171
        }
172
        else
173
        {
174 8
            $this->setNonNullValue($property, $value);
175
        }
176 9
    }
177
178
    /**
179
     * Attempts to set a property with a null value
180
     *
181
     * @param ReflectionProperty $property The property to set
182
     */
183 2
    private function setNullValue(ReflectionProperty $property)
184
    {
185 2
        if ($property->type->nullable)
186
        {
187 1
            $this->setRawValue($property->name, null);
188
        }
189
        else
190
        {
191 1
            $this->throwError($property, 'NonNull', null);
192
        }
193 1
    }
194
195
    /**
196
     * Attempts to set a property with a non null value
197
     *
198
     * @param ReflectionProperty $property The property to set
199
     * @param mixed $value The value to set
200
     */
201 8
    private function setNonNullValue
202
    (
203
        ReflectionProperty $property,
204
        $value
205
    )
206
    {
207 8
        switch (get_class($property->type))
208
        {
209 8
            case MixedType::class:
210
                $this->setRawValue($property, $value);
0 ignored issues
show
Documentation introduced by
$property is of type object<Spaark\CompositeU...ion\ReflectionProperty>, but the function expects a string.

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...
211
                break;
212 8
            case IntegerType::class:
213 1
                $this->setScalarValue($property, $value, 'Integer',
214
                    function($value)
215
                    {
216 1
                        return (integer)$value;
217 1
                    });
218 1
                break;
219 8
            case StringType::class:
220 3
                $this->setScalarValue($property, $value, 'String',
221
                    function($value)
222
                    {
223 3
                        return (string)$value;
224 3
                    });
225 3
                break;
226 7
            case BooleanType::class:
227 5
                $this->setScalarValue($property, $value, 'Boolean',
228 5
                    function($value)
229
                    {
230 5
                        return (boolean)$value;
231 5
                    });
232 5
                break;
233 5
            case CollectionType::class:
234
                $this->setCollectionValue($property, $value);
235
                break;
236 5
            case ObjectType::class:
237 5
                $this->setObjectValue($property, $value);
238 5
                break;
239
        }
240 8
    }
241
242
    /**
243
     * Attempts to set a property which expects a scalar value
244
     *
245
     * @param ReflectionProperty $property The property to set
246
     * @param mixed $value The value to set
247
     * @param string $name The name of the scalar type
248
     * @param callable $cast Method to cast a value to the scalar data
249
     *     type
250
     */
251 6
    private function setScalarValue
252
    (
253
        ReflectionProperty $property,
254
        $value,
255
        string $name,
256
        callable $cast
257
    )
258
    {
259 6
        $method = '__to' . $name;
260
261 6
        if (is_scalar($value))
262
        {
263 6
            $this->setRawValue($property->name, $cast($value));
264
        }
265
        elseif (is_object($value) && method_exists([$value, $method]))
266
        {
267
            $this->setScalarValue
268
            (
269
                $property,
270
                $value->$method(),
271
                $method,
272
                $cast
273
            );
274
        }
275
        else
276
        {
277
            $this->throwError($property, $name, $value);
0 ignored issues
show
Documentation introduced by
$value is of type array|null|object, but the function expects a string.

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...
278
        }
279 6
    }
280
281
    /**
282
     * Attempts to set a property which expects an object value
283
     *
284
     * @param ReflectionProperty $property The property to set
285
     * @param mixed $value The value to set
286
     */
287 5
    private function setObjectValue
288
    (
289
        ReflectionProperty $property,
290
        $value
291
    )
292
    {
293 5
        if (is_a($value, $property->type->classname))
0 ignored issues
show
Documentation introduced by
The property classname does not exist on object<Spaark\CompositeU...tion\Type\AbstractType>. 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...
294
        {
295 5
            $this->setRawValue($property->name, $value);
296
        }
297
        else
298
        {
299
            $this->throwError
300
            (
301
                $property,
302
                $property->type->classname,
0 ignored issues
show
Documentation introduced by
The property classname does not exist on object<Spaark\CompositeU...tion\Type\AbstractType>. 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...
303
                $value
304
            );
305
        }
306 5
    }
307
308
    /**
309
     * Attempts to set a property which expects a collection value
310
     *
311
     * @param ReflectionProperty $property The property to set
312
     * @param mixed The value to set
313
     */
314
    private function setCollectionValue
315
    (
316
        ReflectionProperty $property,
317
        $value
318
    )
319
    {
320
        if (is_a($value, \ArrayAccess::class) || is_array($value))
321
        {
322
            $this->setRawValue($property->name, $value);
323
        }
324
        else
325
        {
326
            $this->throwError($property, 'Collection', $value);
327
        }
328
    }
329
330
    /**
331
     * Throws an IlleglPropertyTypeException
332
     *
333
     * @param ReflectionProperty $property The property being set
334
     * @param string $expected The expected datatype
335
     * @param string $value The value being set
336
     */
337 1
    private function throwError
338
    (
339
        ReflectionProperty $property,
340
        string $expected,
341
        $value
342
    )
343
    {
344 1
        throw new IllegalPropertyTypeException
345
        (
346 1
            get_class($this->object),
347 1
            $property->name,
348
            $expected,
349 1
            is_object($value) ? get_class($value) : gettype($value)
350
        );
351
    }
352
}
353