PropertiesHandler::ensureMethodExists()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * PHP: Nelson Martell Library file
5
 *
6
 * Copyright © 2015-2021 Nelson Martell (http://nelson6e65.github.io)
7
 *
8
 * Licensed under The MIT License (MIT)
9
 * For full copyright and license information, please see the LICENSE
10
 * Redistributions of files must retain the above copyright notice.
11
 *
12
 * @copyright 2015-2021 Nelson Martell
13
 * @link      http://nelson6e65.github.io/php_nml/
14
 * @since     0.5.0
15
 * @license   http://www.opensource.org/licenses/mit-license.php The MIT License (MIT)
16
 * */
17
18
declare(strict_types=1);
19
20
namespace NelsonMartell;
21
22
use BadMethodCallException;
23
use InvalidArgumentException;
24
use NelsonMartell\Extensions\MethodExtension;
25
use NelsonMartell\Extensions\PropertyExtension;
26
27
/**
28
 * Enables the class to call, implicitly, getter and setters for its properties, allowing to use properties directly.
29
 *
30
 * Restricts get and set actions for a property if there is not getter/setter definition 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 by simply excluding
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 inside the class
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
 * ***Example 3:*** Same as before, but implementing `IMagicPropertiesContainer`
114
 *
115
 * ```php
116
 * <?php
117
 * use NelsonMartell\IStrictPropertiesContainer;
118
 * use NelsonMartell\PropertiesHandler;
119
 * use NelsonMartell\IMagicPropertiesContainer;
120
 *
121
 *
122
 * // Rest of class DocBlock...
123
 * // @property string $name Name of person
124
 * class Nameable implements IStrictPropertiesContainer, IMagicPropertiesContainer {
125
 *     use PropertiesHandler;
126
 *
127
 *     private $_name = ''; // Attribute: Stores the value.
128
 *
129
 *      public function __construct($name)
130
 *     {
131
 *         $this->name = $name; // Implicit call to the setter inside the class
132
 *     }
133
 *
134
 *     protected function getName()
135
 *     {
136
 *         return ucwords($this->_name);
137
 *     }
138
 *
139
 *     protected function setName($value)
140
 *     {
141
 *         $this->_name = strtolower($value);
142
 *     }
143
 * }
144
 *
145
 * $obj = new Nameable();
146
 * $obj->name = 'nelson maRtElL';
147
 * echo $obj->name; // 'Nelson Martell'
148
 *
149
 * ?>
150
 * ```
151
 *
152
 *
153
 * **Limitations:**
154
 * - You should not define properties wich names only are only different in the first letter upper/lowercase;
155
 *   it will be used the same getter/setter method (since in PHP methods are case-insensitive). In the last
156
 *   example, if you (in addition) define another property called `$Name`, when called, it will
157
 *   be used the same getter and setter method when you access or set both properties (`->Name` and `->name`).
158
 * - Only works for public properties (even if attribute and getter/setter methods are not `public`);
159
 *   this only will avoid the direct use of method (`$obj->getName(); // ERROR`), but the property
160
 *   value still will be accesible in child classes and public scope (`$value = $this->name; // No error`).
161
 * - Getter and Setter methods SHOULD NOT be declared as `private` in child classes if parent already
162
 *   uses this trait.
163
 * - Custom prefixes ability (by implementing ``ICustomPrefixedPropertiesContainer``) is not posible for
164
 *   multiple prefixes in multiples child classes by overriding ``ICustomPrefixedPropertiesContainer`` methods.
165
 *   If you extends a class that already implements it, if you override any methor to return another prefix,
166
 *   parent class properties may be unaccesible (know bug).
167
 * - Avoid the use of custom prefixes and use the standard 'get'/'set' instead. If you need to, maybe you
168
 *   could try to rename methods instead first.
169
 *
170
 * @author Nelson Martell <[email protected]>
171
 * @since 0.5.0
172
 * @since 1.0.0 Auto-detect magic properties defined in class DocBlock.
173
 *
174
 * @see IMagicPropertiesContainer
175
 * */
176
trait PropertiesHandler
177
{
178
    /**
179
     * Gets the property value using the auto-magic method `$getterPrefix.$name()` (getter),
180
     * where `$name` is the name of property and `$getterPrefix` is 'get' by default (but can be customized).
181
     *
182
     * @param string $name Property name.
183
     *
184
     * @return mixed
185
     * @throws BadMethodCallException If unable to get the property value.
186
     * @see PropertiesHandler::getPropertyGetter()
187
     * */
188 114
    public function __get($name)
189
    {
190
        try {
191 114
            $getter = static::getPropertyGetter($name);
192 18
        } catch (InvalidArgumentException $error) {
193 18
            $msg = msg('Unable to get the property value in "{0}" class.', get_class($this));
194 18
            throw new BadMethodCallException($msg, 31, $error);
195
        } catch (BadMethodCallException $error) {
196
            $msg = msg('Unable to get the property value in "{0}" class.', get_class($this));
197
            throw new BadMethodCallException($msg, 32, $error);
198
        }
199
200 96
        return $this->$getter();
201
    }
202
203
204
    /**
205
     * Sets the property value using the auto-magic method `$setterPrefix.$name()` (setter),
206
     * where `$name` is the name of property and `$setterPrefix` is 'set' by default (but can be customized).
207
     *
208
     * @param string $name  Property name.
209
     * @param mixed  $value Property value.
210
     *
211
     * @return void
212
     * @throws BadMethodCallException If unable to set property value.
213
     * @see PropertiesHandler::getPropertySetter()
214
     * */
215 39
    public function __set($name, $value)
216
    {
217
        try {
218 39
            $setter = static::getPropertySetter($name);
219 30
        } catch (InvalidArgumentException $error) {
220 30
            $msg = msg('Unable to set the property value in "{0}" class.', get_class($this));
221 30
            throw new BadMethodCallException($msg, 41, $error);
222
        } catch (BadMethodCallException $error) {
223
            $msg = msg('Unable to set the property value in "{0}" class.', get_class($this));
224
            throw new BadMethodCallException($msg, 42, $error);
225
        }
226
227 11
        $this->$setter($value);
228
    }
229
230
231
    /**
232
     * Ensures that property provided exists in this class.
233
     *
234
     * @param string $name Property name.
235
     *
236
     * @return string Same property name, but validated.
237
     * @throws InvalidArgumentException If property name is not valid (10) or do not exists (11).
238
     *
239
     * @deprecated 1.0.0 Implementation moved to Extensions\PropertyExtension::ensureIsDefined()
240
     *
241
     * @see PropertyExtension::ensureIsDefined()
242
     */
243
    protected static function ensurePropertyExists($name)
244
    {
245
        return PropertyExtension::ensureIsDefined($name, get_called_class());
246
    }
247
248
249
    /**
250
     * Ensures that method provided exists in this class.
251
     *
252
     * @param string $name Method name.
253
     *
254
     * @return string Same method name, but validated.
255
     * @throws InvalidArgumentException If method name is not valid (20) or do not exists (21).
256
     *
257
     * @deprecated 1.0.0 Implementation moved to Extensions\MethodExtension::ensureIsDefined()
258
     *
259
     * @see MethodExtension::ensureIsDefined()
260
     */
261
    protected static function ensureMethodExists($name)
262
    {
263
        return MethodExtension::ensureIsDefined($name, get_called_class());
264
    }
265
266
267
    /**
268
     * Gets the property setter method name.
269
     * You can customize the setter prefix by implementing ``ICustomPrefixedPropertiesContainer`` interface.
270
     *
271
     * @param string $name      Property name.
272
     * @param string $prefix    Property setter prefix.
273
     * @param bool   $useCustom Check for custom setter prefixes.
274
     *
275
     * @return string
276
     * @throws InvalidArgumentException If property is not valid or has not setter.
277
     * @throws BadMethodCallException If custom prefix is not an ``string`` instance.
278
     * @see ICustomPrefixedPropertiesContainer::getCustomSetterPrefix()
279
     *
280
     * @since 1.0.0 Add `$prefix` and `$useCustom` params.
281
     */
282 39
    protected static function getPropertySetter(string $name, string $prefix = 'set', bool $useCustom = true): string
283
    {
284 39
        $class = get_called_class();
285
286 39
        PropertyExtension::ensureIsDefined($name, $class, is_subclass_of($class, IMagicPropertiesContainer::class));
287
288
        try {
289 28
            $setter = MethodExtension::ensureIsDefined($prefix . $name, $class);
290 21
        } catch (InvalidArgumentException $error) {
291 21
            if ($useCustom && is_subclass_of($class, ICustomPrefixedPropertiesContainer::class)) {
292
                // If not available standard setter, check if custom available
293
                // `false` to stop recursion
294
                // @phpstan-ignore-next-line
295 5
                return static::getPropertySetter($name, static::getCustomSetterPrefix(), false);
296
            } else {
297 19
                $msg = msg(
298 19
                    '"{name}" property has not a setter method in "{class}" ("{prefix}{name}").',
299 19
                    compact('class', 'name', 'prefix')
300 19
                );
301 19
                throw new InvalidArgumentException($msg, 40, $error);
302
            }
303
        }
304
305 11
        return $setter;
306
    }
307
308
309
    /**
310
     * Gets the property getter method name.
311
     * You can customize the getter prefix by implementing ``ICustomPrefixedPropertiesContainer`` interface.
312
     *
313
     * @param string $name Property name.
314
     * @param string $prefix    Property getter prefix.
315
     * @param bool   $useCustom Check for custom getter prefixes.
316
     *
317
     * @return string
318
     * @throws InvalidArgumentException If property is not valid or has not getter.
319
     * @throws BadMethodCallException If custom prefix is not an ``string`` instance.
320
     * @see ICustomPrefixedPropertiesContainer::getCustomGetterPrefix()
321
     *
322
     * @since 1.0.0 Add `$prefix` and `$useCustom` params.
323
     */
324 114
    protected static function getPropertyGetter(string $name, string $prefix = 'get', bool $useCustom = true): string
325
    {
326 114
        $class = get_called_class();
327
328 114
        PropertyExtension::ensureIsDefined($name, $class, is_subclass_of($class, IMagicPropertiesContainer::class));
329
330
        try {
331 99
            $getter = MethodExtension::ensureIsDefined($prefix . $name, $class);
332 5
        } catch (InvalidArgumentException $error) {
333 5
            if ($useCustom && is_subclass_of($class, ICustomPrefixedPropertiesContainer::class)) {
334
                // If not available standard getter, check if custom available
335
                // @phpstan-ignore-next-line
336 3
                return static::getPropertyGetter($name, static::getCustomGetterPrefix(), false);
337
            } else {
338 3
                $msg = msg(
339 3
                    '"{name}" property has not a getter method in "{class}" ("{prefix}{name}").',
340 3
                    compact('class', 'name', 'prefix')
341 3
                );
342 3
                throw new InvalidArgumentException($msg, 30, $error);
343
            }
344
        }
345
346 96
        return $getter;
347
    }
348
}
349