Passed
Push — master ( ea4f4f...d6476b )
by Nelson
02:47
created

PropertiesHandler::__set()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.3332

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 2
dl 0
loc 13
rs 9.9666
c 0
b 0
f 0
ccs 6
cts 9
cp 0.6667
crap 3.3332
1
<?php
2
/**
3
 * PHP: Nelson Martell Library file
4
 *
5
 * Copyright © 2015-2019 Nelson Martell (http://nelson6e65.github.io)
6
 *
7
 * Licensed under The MIT License (MIT)
8
 * For full copyright and license information, please see the LICENSE
9
 * Redistributions of files must retain the above copyright notice.
10
 *
11
 * @copyright 2015-2019 Nelson Martell
12
 * @link      http://nelson6e65.github.io/php_nml/
13
 * @since     0.5.0
14
 * @license   http://www.opensource.org/licenses/mit-license.php The MIT License (MIT)
15
 * */
16
17
namespace NelsonMartell;
18
19
use BadMethodCallException;
20
use InvalidArgumentException;
21
22
use NelsonMartell\Extensions\Text;
23
24
/**
25
 * Enables the class to call, implicitly, getter and setters for its properties, allowing to use properties directly.
26
 *
27
 *
28
 * Restricts get and set actions for a property if there is not getter/setter definicion for that property, by
29
 * encapsulating the class attributes.
30
 *
31
 * You can customize the properties validation/normalization without the need to call other functions/methods outside
32
 * the class _before_ to set the value of _after_ outputs.
33
 *
34
 * In addition, the class will be strict: any access to undefined property will be bloqued and informed in dev time.
35
 *
36
 * Also, any property can be restricted to "read-only" or "write-only" from outside the class if you simply exclude
37
 * the setter or getter for that property, respectively.
38
 *
39
 *
40
 * **Usage:**
41
 *
42
 * ***Example 1:*** Person with normalizations on its name:
43
 *
44
 * ```php
45
 * <?php
46
 * // You can document $name property using: "@property string $name Name of person" in the class definition
47
 * class Person implements \NelsonMartell\IStrictPropertiesContainer {
48
 *     use \NelsonMartell\PropertiesHandler;
49
 *
50
 *     public function __construct($name)
51
 *     {
52
 *         $this->setName($name); // Explicit call the setter inside constructor/class
53
 *     }
54
 *
55
 *     private $name = ''; // Property. 'private' in order to hide from inherited classes and public
56
 *
57
 *     protected function getName() // Getter. 'protected' to hide from public
58
 *     {
59
 *         return ucwords($this->name); // Format the $name output
60
 *     }
61
 *
62
 *     protected function setName($value) // Setter. 'protected' in order to hide from public
63
 *     {
64
 *         $this->name = strtolower($value); // Normalize the $name
65
 *     }
66
 * }
67
 *
68
 * $obj = new Person();
69
 * $obj->name = 'nelson maRtElL'; // Implicit call to setter
70
 * echo $obj->name; // 'Nelson Martell' // Implicit call to getter
71
 * echo $obj->Name; // Throws: InvalidArgumentException: "Name" property do not exists in "Nameable" class.
72
 * ```
73
 *
74
 *
75
 * ***Example 2:*** Same as before, but using a property wrapper (not recommended):
76
 *
77
 * ```php
78
 * <?php
79
 * class Nameable implements NelsonMartell\IStrictPropertiesContainer {
80
 *     use \NelsonMartell\PropertiesHandler;
81
 *
82
 *     private $_name = ''; // Attribute: Stores the value.
83
 *     public $name; // Property wrapper. Declare in order to be detected. Accesible name for the property.
84
 *
85
 *      public function __construct($name)
86
 *     {
87
 *         unset($this->name); // IMPORTANT: Unset the wrapper in order to redirect operations to the getter/setter
88
 *
89
 *         $this->name = $name; // Implicit call to the setter
90
 *     }
91
 *
92
 *     protected function getName()
93
 *     {
94
 *         return ucwords($this->_name);
95
 *     }
96
 *
97
 *     protected function setName($value)
98
 *     {
99
 *         $this->_name = strtolower($value);
100
 *     }
101
 * }
102
 *
103
 * $obj = new Nameable();
104
 * $obj->name = 'nelson maRtElL';
105
 * echo $obj->name; // 'Nelson Martell'
106
 *
107
 * ?>
108
 * ```
109
 *
110
 *
111
 * **Limitations:**
112
 * - You should not define properties wich names only are only different in the first letter upper/lowercase;
113
 *   it will be used the same getter/setter method (since in PHP methods are case-insensitive). In the last
114
 *   example, if you (in addition) define another property called `$Name`, when called, it will
115
 *   be used the same getter and setter method when you access or set both properties (`->Name` and `->name`).
116
 * - Only works for public properties (even if attribute and getter/setter methods are not `public`);
117
 *   this only will avoid the direct use of method (`$obj->getName(); // ERROR`), but the property
118
 *   value still will be accesible in child classes and public scope (`$value = $this->name; // No error`).
119
 * - Getter and Setter methods SHOULD NOT be declared as `private` in child classes if parent already
120
 *   uses this trait.
121
 * - Custom prefixes ability (by implementing ``ICustomPrefixedPropertiesContainer``) is not posible for
122
 *   multiple prefixes in multiples child classes by overriding ``ICustomPrefixedPropertiesContainer`` methods.
123
 *   If you extends a class that already implements it, if you override any methor to return another prefix,
124
 *   parent class properties may be unaccesible (know bug).
125
 * - Avoid the use of custom prefixes and use the standard 'get'/'set' instead. If you need to, maybe you
126
 *   could try to rename methods instead first.
127
 *
128
 * @author Nelson Martell <[email protected]>
129
 * @since 0.5.0
130
 * */
131
trait PropertiesHandler
132
{
133
    /**
134
     * Gets the property value using the auto-magic method `$getterPrefix.$name()` (getter),
135
     * where `$name` is the name of property and `$getterPrefix` is 'get' by default (but can be customized).
136
     *
137
     * @param string $name Property name.
138
     *
139
     * @return mixed
140
     * @throws BadMethodCallException If unable to get the property value.
141
     * @see PropertiesHandler::getPropertyGetter()
142
     * */
143 157
    public function __get($name)
144
    {
145
        try {
146 157
            $getter = static::getPropertyGetter($name);
147 17
        } catch (InvalidArgumentException $error) {
148 17
            $msg = msg('Unable to get the property value in "{0}" class.', get_class($this));
149 17
            throw new BadMethodCallException($msg, 31, $error);
150
        } catch (BadMethodCallException $error) {
151
            $msg = msg('Unable to get the property value in "{0}" class.', get_class($this));
152
            throw new BadMethodCallException($msg, 32, $error);
153
        }
154
155 140
        return $this->$getter();
156
    }
157
158
159
    /**
160
     * Sets the property value using the auto-magic method `$setterPrefix.$name()` (setter),
161
     * where `$name` is the name of property and `$setterPrefix` is 'set' by default (but can be customized).
162
     *
163
     * @param string $name  Property name.
164
     * @param mixed  $value Property value.
165
     *
166
     * @return void
167
     * @throws BadMethodCallException If unable to set property value.
168
     * @see PropertiesHandler::getPropertySetter()
169
     * */
170 34
    public function __set($name, $value)
171
    {
172
        try {
173 34
            $setter = static::getPropertySetter($name);
174 28
        } catch (InvalidArgumentException $error) {
175 28
            $msg = msg('Unable to set the property value in "{0}" class.', get_class($this));
176 28
            throw new BadMethodCallException($msg, 41, $error);
177
        } catch (BadMethodCallException $error) {
178
            $msg = msg('Unable to set the property value in "{0}" class.', get_class($this));
179
            throw new BadMethodCallException($msg, 42, $error);
180
        }
181
182 8
        $this->$setter($value);
183 6
    }
184
185
186
    /**
187
     * Ensures that property provided exists in this class.
188
     *
189
     * @param string $name Property name.
190
     *
191
     * @return string Same property name, but validated.
192
     * @throws InvalidArgumentException If property name is not valid (10) or do not exists (11).
193
     */
194 187
    protected static function ensurePropertyExists($name)
195
    {
196
        $args = [
197 187
            'class'    => get_called_class(),
198
        ];
199
200
        try {
201 187
            $args['property'] = Text::ensureIsValidVarName($name);
202
        } catch (InvalidArgumentException $error) {
203
            $msg = msg('Property name is not valid.');
204
            throw new InvalidArgumentException($msg, 10, $error);
205
        }
206
207 187
        if (!typeof(get_called_class(), true)->hasProperty($name)) {
208 24
            $msg = msg(
209 24
                '"{property}" property do not exists in "{class}" class or parent classes.',
210 24
                $args
211
            );
212
213 24
            throw new InvalidArgumentException($msg, 11);
214
        }
215
216 163
        return $name;
217
    }
218
219
220
    /**
221
     * Ensures that method provided exists in this class.
222
     *
223
     * @param string $name Method name.
224
     *
225
     * @return string Same method name, but validated.
226
     * @throws InvalidArgumentException If method name is not valid (20) or do not exists (21).
227
     */
228 163
    protected static function ensureMethodExists($name)
229
    {
230
        $args = [
231 163
            'class'  => get_called_class(),
232
        ];
233
234
        try {
235 163
            $args['method'] = Text::ensureIsValidVarName($name);
236
        } catch (InvalidArgumentException $error) {
237
            $msg = msg('Method name is not valid.');
238
            throw new InvalidArgumentException($msg, 20, $error);
239
        }
240
241 163
        if (method_exists($args['class'], $args['method']) === false) {
242 24
            $msg = msg('"{class}::{method}" do not exists.', $args);
243
244 24
            throw new InvalidArgumentException($msg, 21);
245
        }
246
247 145
        return $name;
248
    }
249
250
251
    /**
252
     * Gets the property setter method name.
253
     * You can customize the setter prefix by implementing ``ICustomPrefixedPropertiesContainer`` interface.
254
     *
255
     * @param string $name Property name.
256
     *
257
     * @return string
258
     * @throws InvalidArgumentException If property is not valid or has not setter.
259
     * @throws BadMethodCallException If custom prefix is not an ``string`` instance.
260
     * @see ICustomPrefixedPropertiesContainer::getCustomSetterPrefix()
261
     */
262 34
    protected static function getPropertySetter($name)
263
    {
264
        $args = [
265 34
            'class' => get_called_class(),
266
        ];
267
268 34
        $prefix = 'set';
269
270 34
        $args['name'] = static::ensurePropertyExists($name, $args['class']);
0 ignored issues
show
Unused Code introduced by
The call to NelsonMartell\Properties...:ensurePropertyExists() has too many arguments starting with $args['class']. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

270
        /** @scrutinizer ignore-call */ 
271
        $args['name'] = static::ensurePropertyExists($name, $args['class']);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
271
272
        try {
273 24
            $setter = static::ensureMethodExists($prefix.$args['name']);
274 20
        } catch (InvalidArgumentException $error) {
275 20
            $msg = msg('"{name}" property has not a setter method in "{class}".', $args);
276
277 20
            if (is_subclass_of($args['class'], ICustomPrefixedPropertiesContainer::class)) {
278
                // If not available standard setter, check if custom available
279
                try {
280 5
                    $prefix = Text::ensureIsString(static::getCustomSetterPrefix());
281
                } catch (InvalidArgumentException $e) {
282
                    $msg = msg(
283
                        '"{class}::getCustomSetterPrefix" method must to return an string.',
284
                        $args['class']
285
                    );
286
287
                    throw new BadMethodCallException($msg, 31, $e);
288
                }
289
290
                try {
291 5
                    $setter = static::ensureMethodExists($prefix.$args['name']);
292 3
                } catch (InvalidArgumentException $e) {
293 5
                    throw new InvalidArgumentException($msg, 32, $e);
294
                }
295
            } else {
296
                // Error for non custom prefixes
297 15
                throw new InvalidArgumentException($msg, 30, $error);
298
            }
299
        }
300
301 8
        return $setter;
302
    }
303
304
305
    /**
306
     * Gets the property getter method name.
307
     * You can customize the getter prefix by implementing ``ICustomPrefixedPropertiesContainer`` interface.
308
     *
309
     * @param string $name Property name.
310
     *
311
     * @return string
312
     * @throws InvalidArgumentException If property is not valid or has not getter.
313
     * @throws BadMethodCallException If custom prefix is not an ``string`` instance.
314
     * @see ICustomPrefixedPropertiesContainer::getCustomGetterPrefix()
315
     */
316 157
    protected static function getPropertyGetter($name)
317
    {
318
        $args = [
319 157
            'class' => get_called_class(),
320
        ];
321
322 157
        $prefix = 'get';
323
324 157
        $args['name'] = static::ensurePropertyExists($name, $args['class']);
0 ignored issues
show
Unused Code introduced by
The call to NelsonMartell\Properties...:ensurePropertyExists() has too many arguments starting with $args['class']. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

324
        /** @scrutinizer ignore-call */ 
325
        $args['name'] = static::ensurePropertyExists($name, $args['class']);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
325
326
        try {
327 143
            $getter = static::ensureMethodExists($prefix.$args['name']);
328 5
        } catch (InvalidArgumentException $error) {
329 5
            $msg = msg('"{name}" property has not a getter method in "{class}".', $args);
330
331 5
            if (is_subclass_of($args['class'], ICustomPrefixedPropertiesContainer::class)) {
332
                // If not available standard getter, check if custom available
333
                try {
334 3
                    $prefix = Text::ensureIsString(static::getCustomGetterPrefix());
335
                } catch (InvalidArgumentException $e) {
336
                    $msg = msg(
337
                        '"{class}::getCustomGetterPrefix" method must to return an string.',
338
                        $args['class']
339
                    );
340
341
                    throw new BadMethodCallException($msg, 31, $e);
342
                }
343
344
                try {
345 3
                    $getter = static::ensureMethodExists($prefix.$args['name']);
346 1
                } catch (InvalidArgumentException $e) {
347 3
                    throw new InvalidArgumentException($msg, 32, $e);
348
                }
349
            } else {
350
                // Error for non custom prefixes
351 2
                throw new InvalidArgumentException($msg, 30, $error);
352
            }
353
        }
354
355 140
        return $getter;
356
    }
357
}
358