Passed
Branch master (283c0c)
by Nelson
02:38
created

PropertiesHandler   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 235
Duplicated Lines 46.81 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 72.62%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
dl 110
loc 235
ccs 61
cts 84
cp 0.7262
rs 10
c 3
b 1
f 0
wmc 25
lcom 1
cbo 1

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __get() 14 14 3
A __set() 14 14 3
B ensurePropertyExists() 0 32 6
A ensureMethodExists() 0 21 3
B getPropertySetter() 41 41 5
B getPropertyGetter() 41 41 5

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
/**
3
 * PHP: Nelson Martell Library file
4
 *
5
 * Content:
6
 * - Trait definition:  [NelsonMartell]  PropertiesHandler
7
 *
8
 * Copyright © 2015-2017 Nelson Martell (http://nelson6e65.github.io)
9
 *
10
 * Licensed under The MIT License (MIT)
11
 * For full copyright and license information, please see the LICENSE
12
 * Redistributions of files must retain the above copyright notice.
13
 *
14
 * @copyright 2015-2017 Nelson Martell
15
 * @link      http://nelson6e65.github.io/php_nml/
16
 * @since     v0.5.0
17
 * @license   http://www.opensource.org/licenses/mit-license.php The MIT License (MIT)
18
 * */
19
20
namespace NelsonMartell;
21
22
use NelsonMartell\Extensions\Text;
23
use \BadMethodCallException;
24
use \InvalidArgumentException;
25
26
/**
27
 * Enables the class to use properties, by encapsulating class attributes in order to use with
28
 * auto-setters/getters methods instead of direct access.
29
 *
30
 * Using this trail will restrict get and set actions for a property if there is not defined in
31
 * the class or if there is not a getter or setter method (respectively) for that property.
32
 *
33
 * So, you MUST (1) create the property in the class and (2) then unset it in the constructor
34
 * (*this requirements will change in next releases to be more 'auto-magic'*).
35
 * You can, also, expose read-only attributes to be public by creating only a getter method and declare
36
 * visibility of attribute as private.
37
 *
38
 * @example
39
 * ```php
40
 * <?php
41
 * class Nameable implements NelsonMartell\IStrictPropertiesContainer {
42
 *     use NelsonMartell\PropertiesHandler;
43
 *
44
 *     public function __construct()
45
 *     {
46
 *         unset($this->Name); // (2)
47
 *     }
48
 *
49
 *     private $_name = ''; // Attribute: Stores the value.
50
 *     public $Name; // (1) Property: Accesible name for the property.
51
 *
52
 *     public function getName()
53
 *     {
54
 *         return ucwords($this->_name);
55
 *     }
56
 *
57
 *     public function setName($value)
58
 *     {
59
 *         $this->_name = strtolower($value);
60
 *     }
61
 * }
62
 *
63
 * $obj = new Nameable();
64
 * $obj->Name = 'nelson maRtElL';
65
 * echo $obj->Name; // 'Nelson Martell'
66
 * echo $obj->name; // Throws: InvalidArgumentException: "name" property do not exists in "Nameable" class.
67
 *
68
 * ?>
69
 * ```
70
 *
71
 * ## Notes:
72
 * - You should not define properties wich names only are only different in the first letter upper/lowercase;
73
 *   it will be used the same getter/setter method (since in PHP methods are case-insensitive). In the last
74
 *   example, if you (in addition) define the `public $name` and `unset($this->name)` in the constructor, it will
75
 *   be used the same getter and setter method when you access or set both properties (`->Name` and `->name`).
76
 * - Only works for public properties (even if you declare visibility of getter/setter methods as `private`
77
 *   or `protected`); this only will avoid the direct use of method (``$obj->getName(); // ERROR``), but property
78
 *   value still will be available in child classes and main (``$value = $this->name; // No error``).
79
 * - Getter and Setter methods SHOULD NOT be declared as ``private`` in child classes if parent already
80
 *   uses this trait.
81
 * - Custom prefixes ability (by implementing ``ICustomPrefixedPropertiesContainer``) is not posible for
82
 *   multiple prefixes in multiples child classes by overriding ``ICustomPrefixedPropertiesContainer`` methods.
83
 *   If you extends a class that already implements it, if you override any methor to return another prefix,
84
 *   parent class properties may be unaccesible (know bug).
85
 * - Avoid the use of custom prefixes and use the standard 'get'/'set' instead. If you need to, maybe you
86
 *   could try to rename methods instead first.
87
 *
88
 * @author Nelson Martell <[email protected]>
89
 * */
90
trait PropertiesHandler
91
{
92
    /**
93
     * Gets the property value using the auto-magic method `$getterPrefix.$name()` (getter),
94
     * where `$name` is the name of property and `$getterPrefix` is 'get' by default (but can be customized).
95
     *
96
     * @param string $name Property name.
97
     *
98
     * @return mixed
99
     * @throws BadMethodCallException If unable to get the property value.
100
     * @see PropertiesHandler::getPropertyGetter()
101
     * */
102 271 View Code Duplication
    public function __get($name)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
103
    {
104
        try {
105 271
            $getter = static::getPropertyGetter($name);
106 3
        } catch (InvalidArgumentException $error) {
107 3
            $msg = msg('Unable to get the property value in "{0}" class.', get_class($this));
108 3
            throw new BadMethodCallException($msg, 31, $error);
109
        } catch (BadMethodCallException $error) {
110
            $msg = msg('Unable to get the property value in "{0}" class.', get_class($this));
111
            throw new BadMethodCallException($msg, 32, $error);
112
        }
113
114 271
        return $this->$getter();
115
    }
116
117
118
    /**
119
     * Sets the property value using the auto-magic method `$setterPrefix.$name()` (setter),
120
     * where `$name` is the name of property and `$setterPrefix` is 'set' by default (but can be customized).
121
     *
122
     * @param string $name  Property name.
123
     * @param mixed  $value Property value.
124
     *
125
     * @return void
126
     * @throws BadMethodCallException If unable to set property value.
127
     * @see PropertiesHandler::getPropertySetter()
128
     * */
129 14 View Code Duplication
    public function __set($name, $value)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
130
    {
131
        try {
132 14
            $setter = static::getPropertySetter($name);
133 11
        } catch (InvalidArgumentException $error) {
134 11
            $msg = msg('Unable to set the property value in "{0}" class.', get_class($this));
135 11
            throw new BadMethodCallException($msg, 41, $error);
136
        } catch (BadMethodCallException $error) {
137
            $msg = msg('Unable to set the property value in "{0}" class.', get_class($this));
138
            throw new BadMethodCallException($msg, 42, $error);
139
        }
140
141 4
        $this->$setter($value);
142 3
    }
143
144
145
    /**
146
     * Ensures that property provided exists in this class.
147
     *
148
     * @param string $name Property name.
149
     *
150
     * @return string Same property name, but validated.
151
     * @throws InvalidArgumentException If property name is not valid (10) or do not exists (11).
152
     */
153 272
    protected static function ensurePropertyExists($name)
154
    {
155
        $args = [
156 272
            'class'    => get_called_class(),
157
        ];
158
159
        try {
160 272
            $args['property'] = Text::ensureIsValidVarName($name);
161
        } catch (InvalidArgumentException $error) {
162
            $msg = msg('Property name is not valid.');
163
            throw new InvalidArgumentException($msg, 10, $error);
164
        }
165
166 272
        if (!property_exists($args['class'], $args['property'])) {
167
            // Check in parent classes for private property
168 8
            $current = $args['class'];
169 8
            $exists = false;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 2 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
170 8
            while ($current = get_parent_class($current) and !$exists) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
171 5
                $exists = property_exists($current, $args['property']);
172
            }
173
174 8
            if (!$exists) {
175 6
                $msg = msg(
176 6
                    '"{property}" property do not exists in "{class}" class or parent classes.',
177 6
                    $args
178
                );
179 6
                throw new InvalidArgumentException($msg, 11);
180
            }
181
        }
182
183 272
        return $name;
184
    }
185
186
187
    /**
188
     * Ensures that method provided exists in this class.
189
     *
190
     * @param string $name Method name.
191
     *
192
     * @return string Same method name, but validated.
193
     * @throws InvalidArgumentException If method name is not valid (20) or do not exists (21).
194
     */
195 272
    protected static function ensureMethodExists($name)
196
    {
197
        $args = [
198 272
            'class'  => get_called_class(),
199
        ];
200
201
        try {
202 272
            $args['method'] = Text::ensureIsValidVarName($name);
203
        } catch (InvalidArgumentException $error) {
204
            $msg = msg('Method name is not valid.');
205
            throw new InvalidArgumentException($msg, 20, $error);
206
        }
207
208 272
        if (method_exists($args['class'], $args['method']) === false) {
209 12
            $msg = msg('"{class}::{method}" do not exists.', $args);
210
211 12
            throw new InvalidArgumentException($msg, 21);
212
        }
213
214 272
        return $name;
215
    }
216
217
218
    /**
219
     * Gets the property setter method name.
220
     * You can customize the setter prefix by implementing ``ICustomPrefixedPropertiesContainer`` interface.
221
     *
222
     * @param string $name Property name.
223
     *
224
     * @return string
225
     * @throws InvalidArgumentException If property is not valid or has not setter.
226
     * @throws BadMethodCallException If custom prefix is not an ``string`` instance.
227
     * @see ICustomPrefixedPropertiesContainer::getCustomSetterPrefix()
228
     */
229 14 View Code Duplication
    protected static function getPropertySetter($name)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
230
    {
231
        $args   = [
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 3 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
232 14
            'class' => get_called_class(),
233
        ];
234
235 14
        $prefix = 'set';
236
237 14
        $args['name'] = static::ensurePropertyExists($name, $args['class']);
0 ignored issues
show
Unused Code introduced by
The call to PropertiesHandler::ensurePropertyExists() has too many arguments starting with $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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
238
239
        try {
240 10
            $setter = static::ensureMethodExists($prefix.$args['name']);
241 8
        } catch (InvalidArgumentException $error) {
242 8
            $msg = msg('"{name}" property has not a setter method in "{class}".', $args);
243
244 8
            if (is_subclass_of($args['class'], ICustomPrefixedPropertiesContainer::class)) {
245
                // If not available standard setter, check if custom available
246
                try {
247 2
                    $prefix = Text::ensureIsString(static::getCustomSetterPrefix());
248
                } catch (InvalidArgumentException $e) {
249
                    $msg = msg(
250
                        '"{class}::getCustomSetterPrefix" method must to return an string.',
251
                        $args['class']
252
                    );
253
254
                    throw new BadMethodCallException($msg, 31, $e);
255
                }
256
257
                try {
258 2
                    $setter = static::ensureMethodExists($prefix.$args['name']);
259 1
                } catch (InvalidArgumentException $e) {
260 1
                    throw new InvalidArgumentException($msg, 32, $e);
261
                }
262
            } else {
263
                // Error for non custom prefixes
264 6
                throw new InvalidArgumentException($msg, 30, $error);
265
            }
266
        }
267
268 4
        return $setter;
269
    }
270
271
272
    /**
273
     * Gets the property getter method name.
274
     * You can customize the getter prefix by implementing ``ICustomPrefixedPropertiesContainer`` interface.
275
     *
276
     * @param string $name Property name.
277
     *
278
     * @return string
279
     * @throws InvalidArgumentException If property is not valid or has not getter.
280
     * @throws BadMethodCallException If custom prefix is not an ``string`` instance.
281
     * @see ICustomPrefixedPropertiesContainer::getCustomGetterPrefix()
282
     */
283 271 View Code Duplication
    protected static function getPropertyGetter($name)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
284
    {
285
        $args   = [
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 3 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
286 271
            'class' => get_called_class(),
287
        ];
288
289 271
        $prefix = 'get';
290
291 271
        $args['name'] = static::ensurePropertyExists($name, $args['class']);
0 ignored issues
show
Unused Code introduced by
The call to PropertiesHandler::ensurePropertyExists() has too many arguments starting with $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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
292
293
        try {
294 271
            $getter = static::ensureMethodExists($prefix.$args['name']);
295 4
        } catch (InvalidArgumentException $error) {
296 4
            $msg = msg('"{name}" property has not a getter method in "{class}".', $args);
297
298 4
            if (is_subclass_of($args['class'], ICustomPrefixedPropertiesContainer::class)) {
299
                // If not available standard getter, check if custom available
300
                try {
301 4
                    $prefix = Text::ensureIsString(static::getCustomGetterPrefix());
302
                } catch (InvalidArgumentException $e) {
303
                    $msg = msg(
304
                        '"{class}::getCustomGetterPrefix" method must to return an string.',
305
                        $args['class']
306
                    );
307
308
                    throw new BadMethodCallException($msg, 31, $e);
309
                }
310
311
                try {
312 4
                    $getter = static::ensureMethodExists($prefix.$args['name']);
313 1
                } catch (InvalidArgumentException $e) {
314 1
                    throw new InvalidArgumentException($msg, 32, $e);
315
                }
316
            } else {
317
                // Error for non custom prefixes
318
                throw new InvalidArgumentException($msg, 30, $error);
319
            }
320
        }
321
322 271
        return $getter;
323
    }
324
}
325