Skip to content

Commit 295c166

Browse files
authored
Added support for customizing accessor method names using #[Format] attribute.
* Added support for customizing accessor method names using #[Format] attribute. * Cleaned up Accessible trait and moved most of the code into ClassConf. * Added missing copyright banners. * Add @api tags to appropriate classes. * Update documentation.
1 parent ba22991 commit 295c166

16 files changed

+767
-313
lines changed

README.md

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Current library can create automatic accessors (e.g. _getters_ and _setters_) fo
88
* direct assignment syntax:
99
* `$value = $foo->property`
1010
* `$foo->property = 'value'`
11-
* method syntax:
11+
* method syntax (**customizable format**):
1212
* `$value = $foo->get('property')`
1313
* `$value = $foo->property()`
1414
* `$foo->property('value')`
@@ -20,14 +20,14 @@ Current library can create automatic accessors (e.g. _getters_ and _setters_) fo
2020
* Accessors can be configured _per_ property or for all class at once.
2121
* Inheritance and override support. E.g. set default behaviour for whole class and make exceptions for specific properties.
2222
* No variables, functions nor methods will be polluted into user classes or global namespace (except necessary `__get()`/`__set()`/`__isset()`/`__unset()`/`__call()`).
23-
* [_PHPDoc_](https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/property.html) tags `@property`, `@property-read` and `@property-write` are also supported and can be used instead of Attributes on basic cases.
23+
* [_PHPDoc_](https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/property.html) tags `@property`, `@property-read` and `@property-write` can be used instead of basic Attributes.
2424
* _Weak_ **immutability** support backed by _wither_ methods.
2525
* **Mutator** support for _setters_.
2626

2727
## Requirements
2828

2929
PHP >= 8.0
30-
~~~~
30+
3131
## Installation
3232

3333
Install with composer:
@@ -69,6 +69,7 @@ echo $a->getFoo(); // Outputs "foo"
6969
echo $a->getBar(); // Outputs "bar"
7070
echo $a->getBaz(); // Outputs "baz"
7171
```
72+
7273
This has boilerplate code just to make 3 properties readable. In case there are tens of properties things could get quite tedious.
7374

7475
By using `Accessible` trait this class can be rewritten:
@@ -284,6 +285,7 @@ Notes:
284285
* Immutable properties can be still changed inside the owner object.
285286
* To prevent ambiguity, immutable properties must be changed using method `with` instead of `set`. Using `set` for immutable properties results in exception and vice versa.
286287
* Unsetting immutable properties is not possible and results in exception.
288+
* When `#[Immutable]` is defined on a class then it must be done on top of hierarchy and not in somewhere between. This is to enforce consistency throughout all of the inheritance. This effectively prohibits situations when there's mutable parent but in derived class the same inherited properties can be turned suddenly immutable.
287289

288290
### Mutator
289291

@@ -398,11 +400,11 @@ unset($a->bar); // Results in Exception
398400
```
399401

400402
Notes:
401-
* since `Unset` is reserved word, `Delete` was chosen for attribute name instead.
403+
* since `Unset` is reserved word, `Delete` is used as Attribute name instead.
402404

403405
### Configuration inheritance
404406

405-
Inheritance is quite straightforward. Attributes in parent class are inherited by children and can be overwritten (except for `Immutable`):
407+
Inheritance is quite straightforward. Attributes in parent class are inherited by children and can be overwritten (except `#[Immutable]` and `#[Format]`):
406408

407409
```php
408410
use margusk\Accessors\Attr\{
@@ -464,6 +466,61 @@ echo $a->FOO; // Outputs "FOO"
464466
echo $a->Foo; // Results in Exception because property "Foo" doesn't exist
465467
```
466468

469+
### Customizing format of accessor method names
470+
Usually the names for accessor methods are just straightforward _camel-case_'d names like `get<property>`, `set<property>` and so on. But if necessary, it can be customized.
471+
472+
Customization can be achieved using `#[Format(class-string<FormatContract>)]` attribute with first parameter specifying class name. This class is responsible for parsing out accessor methods names.
473+
474+
Following example turns _camel-case_ methods into _snake-case_:
475+
476+
```php
477+
use margusk\Accessors\Attr\{
478+
Get, Set, Format
479+
};
480+
use margusk\Accessors\Format\Method;
481+
use margusk\Accessors\Format\Standard;
482+
use margusk\Accessors\Accessible;
483+
484+
class SnakeCaseFmt extends Standard
485+
{
486+
public function matchCalled(string $method): ?Method
487+
{
488+
if (
489+
preg_match(
490+
'/^(' . implode('|', Method::TYPES) . ')_(.*)/i',
491+
strtolower($method),
492+
$matches
493+
)
494+
) {
495+
$methodName = $matches[1];
496+
$propertyName = $matches[2];
497+
498+
return new Method(
499+
Method::TYPES[$methodName],
500+
$propertyName
501+
);
502+
}
503+
504+
return null;
505+
}
506+
}
507+
508+
#[Get,Set,Format(SnakeCaseFmt::class)]
509+
class A
510+
{
511+
use Accessible;
512+
513+
protected string $foo = "foo";
514+
}
515+
516+
$a = new A();
517+
echo $a->set_foo("new foo")->get_foo(); // Outputs "new foo"
518+
echo $a->setFoo("new foo"); // Results in Exception
519+
520+
```
521+
Notes:
522+
* `#[Format(...)]` can be defined only for a class and on top of hierarchy. This is to enforce consistency throughout all of the inheritance tree. This effectively prohibits situations when there's one syntax in parent but in derived classes the syntax suddenly changes.
523+
467524
### IDE autocompletion
468525

469526
Having accessors with _magic methods_ can bring the disadvantages of losing somewhat of IDE autocompletion and make static code analyzers grope in the dark.
@@ -512,9 +569,10 @@ Since `@property[<-read>|<-write>]` tags act also in exposing properties, you ge
512569
* if `string` type is used then it must contain regular function name or syntax `$this->someMutatorMethod` implies instance method.
513570
* use `array` type for specifying static class method.
514571
* and use `null` to discard any previously set mutator.
515-
* `#[Immutable]`: turns an property or whole class immutable. Once the attribute is added, it can't be disabled later.
572+
* `#[Immutable]`: turns an property or whole class immutable. When used on a class, then it must be defined on top of hierarchy. Once defined, it can't be disabled later.
573+
* `#[Format(class-string<FormatContract>)]`: allows customization of accessor method names.
516574

517-
### Properties can be accessed with following syntaxes:
575+
### Accessor methods:
518576

519577
#### Reading properties:
520578
* `$value = $obj->foo;`

src/Accessible.php

Lines changed: 11 additions & 215 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,9 @@
1212

1313
namespace margusk\Accessors;
1414

15-
use margusk\Accessors\Exception\BadMethodCallException;
16-
use margusk\Accessors\Exception\InvalidArgumentException;
1715
use ReflectionException;
1816

19-
use function array_shift;
20-
use function count;
21-
use function current;
22-
use function in_array;
23-
use function is_array;
24-
use function is_string;
25-
use function strlen;
26-
use function strtolower;
27-
use function substr;
28-
17+
/** @api */
2918
trait Accessible
3019
{
3120
/**
@@ -37,185 +26,8 @@ trait Accessible
3726
*/
3827
public function __call(string $method, array $args): mixed
3928
{
40-
$classConf = ClassConf::factory(static::class);
41-
42-
$lcaseMethod = strtolower($method);
43-
44-
// Try to extract accessor method from magic method name
45-
if (
46-
!in_array(($accessorMethod = substr($lcaseMethod, 0, 3)), ['get', 'set'], true)
47-
&& 'with' !== ($accessorMethod = substr($lcaseMethod, 0, 4))
48-
&& !in_array(($accessorMethod = substr($lcaseMethod, 0, 5)), ['unset', 'isset'], true)
49-
) {
50-
$accessorMethod = null;
51-
}
52-
53-
$nArgs = count($args);
54-
$propertyName = substr($method, strlen((string)$accessorMethod));
55-
$propertyValue = null;
56-
$propertiesList = [];
57-
$accessorMethodIsSetOrWith = in_array($accessorMethod, ['set', 'with'], true);
58-
59-
// Check if the call is multi-property accessor, that is if first
60-
// argument is array and accessor method is set at this point and is "set", "with" or "unset"
61-
if (
62-
'' === $propertyName
63-
&& $nArgs > 0
64-
&& is_array(current($args))
65-
&& ($accessorMethodIsSetOrWith || 'unset' === $accessorMethod)
66-
) {
67-
if ($nArgs > 1) {
68-
throw InvalidArgumentException::dueMultiPropertyAccessorCanHaveExactlyOneArgument(
69-
static::class,
70-
$method
71-
);
72-
}
73-
74-
/** @var mixed[] $propertiesList */
75-
$propertiesList = array_shift($args);
76-
77-
// Check if whole method name is property name like
78-
// $obj->somePropertyName('somevalue')
79-
} elseif (
80-
null === $accessorMethod
81-
&& null !== $classConf->properties()->findConf($propertyName, true)
82-
) {
83-
// If there are zero arguments, then interpret the call as Getter
84-
// If there are arguments, then it's Setter
85-
if ($nArgs > 0) {
86-
$accessorMethodIsSetOrWith = true;
87-
$accessorMethod = 'set';
88-
} else {
89-
$accessorMethod = 'get';
90-
}
91-
}
92-
93-
// Accessor method must be resolved at this point, or we fail
94-
if (null === $accessorMethod) {
95-
throw BadMethodCallException::dueUnknownAccessorMethod(static::class, $method);
96-
}
97-
98-
$propertyNameCI = false;
99-
100-
// If accessorProperties are not set at this point (thus not specified using array
101-
// as first parameter to set or with), then extract them as separate arguments to current method
102-
if (0 === count($propertiesList)) {
103-
if ('' === $propertyName) {
104-
if (!count($args)) {
105-
throw InvalidArgumentException::dueMethodIsMissingPropertyNameArgument(static::class, $method);
106-
}
107-
108-
$propertyName = array_shift($args);
109-
110-
if (!is_string($propertyName)) {
111-
throw InvalidArgumentException::duePropertyNameArgumentMustBeString(
112-
static::class,
113-
$method,
114-
count($args) + 1
115-
);
116-
}
117-
/** @var string $propertyName */
118-
} else {
119-
// If we arrive here, then property name was specified partially or fully in method name and
120-
// in this case we always interpret it as case-insensitive
121-
$propertyNameCI = true;
122-
}
123-
124-
if ($accessorMethodIsSetOrWith) {
125-
if (!count($args)) {
126-
throw InvalidArgumentException::dueMethodIsMissingPropertyValueArgument(
127-
static::class,
128-
$method,
129-
$nArgs + 1
130-
);
131-
}
132-
133-
$propertyValue = array_shift($args);
134-
}
135-
136-
// Fail if there are more arguments specified than we are willing to process
137-
if (count($args)) {
138-
throw InvalidArgumentException::dueMethodHasMoreArgumentsThanExpected(
139-
static::class,
140-
$method,
141-
$nArgs - count($args)
142-
);
143-
}
144-
145-
if ($accessorMethodIsSetOrWith) {
146-
$propertiesList[$propertyName] = $propertyValue;
147-
} else {
148-
$propertiesList[] = $propertyName;
149-
}
150-
}
151-
152-
$result = $this;
153-
154-
// Call Set or With
155-
if ($accessorMethodIsSetOrWith) {
156-
if ('with' === $accessorMethod) {
157-
$result = clone $result;
158-
}
159-
160-
$accessorImpl = $classConf->getSetter();
161-
162-
foreach ($propertiesList as $propertyName => $propertyValue) {
163-
if (!is_string($propertyName)) {
164-
throw InvalidArgumentException::dueMultiPropertyArrayContainsNonStringProperty(
165-
static::class,
166-
$method,
167-
$propertyName
168-
);
169-
}
170-
171-
$propertyConf = $classConf->properties()->findConf($propertyName, $propertyNameCI);
172-
$immutable = ($propertyConf?->isImmutable()) ?? false;
173-
174-
// Check if mutable/immutable property was called using correct method:
175-
// - mutable properties must be accessed using "set"
176-
// - immutable properties must be accessed using "with"
177-
if (
178-
($immutable === true && 'set' === $accessorMethod)
179-
|| ($immutable === false && 'with' === $accessorMethod)
180-
) {
181-
if ($immutable) {
182-
throw BadMethodCallException::dueImmutablePropertiesMustBeCalledUsingWith(
183-
static::class,
184-
$propertyName
185-
);
186-
} else {
187-
throw BadMethodCallException::dueMutablePropertiesMustBeCalledUsingSet(
188-
static::class,
189-
$propertyName
190-
);
191-
}
192-
}
193-
194-
$result = $accessorImpl($result, $accessorMethod, $propertyName, $propertyValue, $propertyConf);
195-
}
196-
} else {
197-
/** @var 'get'|'isset'|'unset' $accessorMethod */
198-
$accessorImpl = match ($accessorMethod) {
199-
'get' => $classConf->getGetter(),
200-
'isset' => $classConf->getIsSetter(),
201-
'unset' => $classConf->getUnSetter()
202-
};
203-
204-
foreach ($propertiesList as $propertyName) {
205-
if (!is_string($propertyName)) {
206-
throw InvalidArgumentException::dueMultiPropertyArrayContainsNonStringProperty(
207-
static::class,
208-
$method,
209-
$propertyName
210-
);
211-
}
212-
213-
$propertyConf = $classConf->properties()->findConf($propertyName, $propertyNameCI);
214-
$result = $accessorImpl($result, $propertyName, $propertyConf);
215-
}
216-
}
217-
218-
return $result;
29+
return ClassConf::factory(static::class)
30+
->handleMagicCall($this, $method, $args);
21931
}
22032

22133
/**
@@ -226,10 +38,8 @@ public function __call(string $method, array $args): mixed
22638
*/
22739
public function __get(string $propertyName): mixed
22840
{
229-
$classConf = ClassConf::factory(static::class);
230-
$propertyConf = $classConf->properties()->findConf($propertyName);
231-
232-
return ($classConf->getGetter())($this, $propertyName, $propertyConf);
41+
return ClassConf::factory(static::class)
42+
->handleMagicGet($this, $propertyName);
23343
}
23444

23545
/**
@@ -241,18 +51,8 @@ public function __get(string $propertyName): mixed
24151
*/
24252
public function __set(string $propertyName, mixed $propertyValue): void
24353
{
244-
$classConf = ClassConf::factory(static::class);
245-
$propertyConf = $classConf->properties()->findConf($propertyName);
246-
$immutable = $propertyConf?->isImmutable();
247-
248-
if ($immutable) {
249-
throw BadMethodCallException::dueImmutablePropertiesCantBeSetUsingAssignmentOperator(
250-
static::class,
251-
$propertyName
252-
);
253-
}
254-
255-
($classConf->getSetter())($this, 'set', $propertyName, $propertyValue, $propertyConf);
54+
ClassConf::factory(static::class)
55+
->handleMagicSet($this, $propertyName, $propertyValue);
25656
}
25757

25858
/**
@@ -263,10 +63,8 @@ public function __set(string $propertyName, mixed $propertyValue): void
26363
*/
26464
public function __isset(string $propertyName): bool
26565
{
266-
$classConf = ClassConf::factory(static::class);
267-
$propertyConf = $classConf->properties()->findConf($propertyName);
268-
269-
return ($classConf->getIsSetter())($this, $propertyName, $propertyConf);
66+
return ClassConf::factory(static::class)
67+
->handleMagicIsset($this, $propertyName);
27068
}
27169

27270
/**
@@ -277,9 +75,7 @@ public function __isset(string $propertyName): bool
27775
*/
27876
public function __unset(string $propertyName): void
27977
{
280-
$classConf = ClassConf::factory(static::class);
281-
$propertyConf = $classConf->properties()->findConf($propertyName);
282-
283-
($classConf->getUnSetter())($this, $propertyName, $propertyConf);
78+
ClassConf::factory(static::class)
79+
->handleMagicUnset($this, $propertyName);
28480
}
28581
}

0 commit comments

Comments
 (0)