Completed
Push — master ( 14a06b...6322f3 )
by Nelson
05:31
created

PropertiesHandler::getPropertySetter()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

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