├── rules-magic-access.neon ├── .phpstan ├── ignore-by-php-version.php └── baseline-lt-8.4.neon ├── src ├── Attributes │ └── ThrowOnInvalidProperty.php ├── SettingsContainerInterface.php └── SettingsContainerAbstract.php ├── LICENSE ├── composer.json └── README.md /rules-magic-access.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | # yes, these are magic 4 | - message: "#^Access to an undefined property chillerlan\\\\Settings\\\\SettingsContainerInterface\\:\\:\\$[\\w]+\\.$#" 5 | -------------------------------------------------------------------------------- /.phpstan/ignore-by-php-version.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2025 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\Settings\Attributes; 13 | 14 | use Attribute; 15 | 16 | /** 17 | * Tells the magic get/set methods whether to throw when a properety is inaccessible 18 | * 19 | * @see \chillerlan\Settings\SettingsContainerAbstract::throwOnInvalidProperty() 20 | */ 21 | #[Attribute(Attribute::TARGET_CLASS)] 22 | final class ThrowOnInvalidProperty{ 23 | 24 | public function __construct( 25 | public readonly bool $throwOnInvalid, 26 | ){} 27 | 28 | } 29 | -------------------------------------------------------------------------------- /.phpstan/baseline-lt-8.4.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | excludePaths: 3 | - ../tests/Subjects/PropertyHooksContainer.php 4 | 5 | ignoreErrors: 6 | - 7 | rawMessage: Access to constant Get on an unknown class PropertyHookType. 8 | identifier: class.notFound 9 | count: 1 10 | path: ../src/SettingsContainerAbstract.php 11 | 12 | - 13 | rawMessage: Access to constant Set on an unknown class PropertyHookType. 14 | identifier: class.notFound 15 | count: 1 16 | path: ../src/SettingsContainerAbstract.php 17 | 18 | - 19 | rawMessage: 'Call to an undefined method ReflectionProperty::getRawValue().' 20 | identifier: method.notFound 21 | count: 2 22 | path: ../src/SettingsContainerAbstract.php 23 | 24 | - 25 | rawMessage: 'Call to an undefined method ReflectionProperty::hasHook().' 26 | identifier: method.notFound 27 | count: 2 28 | path: ../src/SettingsContainerAbstract.php 29 | 30 | - 31 | rawMessage: 'Call to an undefined method ReflectionProperty::setRawValue().' 32 | identifier: method.notFound 33 | count: 1 34 | path: ../src/SettingsContainerAbstract.php 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Smiley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chillerlan/php-settings-container", 3 | "description": "A container class for immutable settings objects. Not a DI container.", 4 | "homepage": "https://github.com/chillerlan/php-settings-container", 5 | "license": "MIT", 6 | "type": "library", 7 | "minimum-stability": "stable", 8 | "keywords": [ 9 | "helper", "container", "settings", "configuration", "property hook" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Smiley", 14 | "email": "smiley@chillerlan.net", 15 | "homepage": "https://github.com/codemasher" 16 | } 17 | ], 18 | "support": { 19 | "issues": "https://github.com/chillerlan/php-settings-container/issues", 20 | "source": "https://github.com/chillerlan/php-settings-container" 21 | }, 22 | "require": { 23 | "php": "^8.1", 24 | "ext-json": "*" 25 | }, 26 | "require-dev": { 27 | "phan/phan": "^5.5.2", 28 | "phpmd/phpmd": "^2.15", 29 | "phpstan/phpstan": "^2.1.31", 30 | "phpstan/phpstan-deprecation-rules": "^2.0.3", 31 | "phpunit/phpunit": "^10.5", 32 | "slevomat/coding-standard": "^8.22", 33 | "squizlabs/php_codesniffer": "^4.0" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "chillerlan\\Settings\\": "src" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "chillerlan\\SettingsTest\\": "tests" 43 | } 44 | }, 45 | "scripts": { 46 | "phan": "@php vendor/bin/phan", 47 | "phpcs": "@php vendor/bin/phpcs -v", 48 | "phpmd": "@php vendor/bin/phpmd src text ./phpmd.xml.dist", 49 | "phpstan": "@php vendor/bin/phpstan", 50 | "phpstan-baseline": "@php vendor/bin/phpstan --generate-baseline", 51 | "phpunit": "@php vendor/bin/phpunit" 52 | }, 53 | "config": { 54 | "lock": false, 55 | "sort-packages": true, 56 | "platform-check": true, 57 | "allow-plugins": { 58 | "dealerdirect/phpcodesniffer-composer-installer": true 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/SettingsContainerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2018 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\Settings; 13 | 14 | use JsonSerializable, Serializable; 15 | 16 | /** 17 | * a generic container with magic getter and setter 18 | */ 19 | interface SettingsContainerInterface extends JsonSerializable, Serializable{ 20 | 21 | /** 22 | * Retrieve the value of $property 23 | * 24 | * @return mixed|null 25 | */ 26 | public function __get(string $property):mixed; 27 | 28 | /** 29 | * Set $property to $value while avoiding private and non-existing properties 30 | */ 31 | public function __set(string $property, mixed $value):void; 32 | 33 | /** 34 | * Checks if $property is set (aka. not null), excluding private properties 35 | */ 36 | public function __isset(string $property):bool; 37 | 38 | /** 39 | * Unsets $property while avoiding private and non-existing properties 40 | */ 41 | public function __unset(string $property):void; 42 | 43 | /** 44 | * @see \chillerlan\Settings\SettingsContainerInterface::toJSON() 45 | */ 46 | public function __toString():string; 47 | 48 | /** 49 | * Returns an array representation of the settings object 50 | * 51 | * The values will be run through the magic __get(), which may also call custom getters. 52 | * 53 | * @return array 54 | */ 55 | public function toArray():array; 56 | 57 | /** 58 | * Sets properties from a given iterable 59 | * 60 | * The values will be run through the magic __set(), which may also call custom setters. 61 | * 62 | * @param iterable $properties 63 | */ 64 | public function fromIterable(iterable $properties):static; 65 | 66 | /** 67 | * Returns a JSON representation of the settings object 68 | * 69 | * @see \json_encode() 70 | * @see \chillerlan\Settings\SettingsContainerInterface::toArray() 71 | * 72 | * @throws \JsonException 73 | */ 74 | public function toJSON(int|null $jsonOptions = null):string; 75 | 76 | /** 77 | * Sets properties from a given JSON string 78 | * 79 | * @see \chillerlan\Settings\SettingsContainerInterface::fromIterable() 80 | * 81 | * @throws \Exception 82 | * @throws \JsonException 83 | */ 84 | public function fromJSON(string $json):static; 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/SettingsContainerAbstract.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2018 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\Settings; 13 | 14 | use chillerlan\Settings\Attributes\ThrowOnInvalidProperty; 15 | use InvalidArgumentException, JsonException, ReflectionException, ReflectionObject, 16 | ReflectionProperty, ReflectionAttribute, RuntimeException; 17 | use function is_object, json_decode, json_encode, json_last_error_msg, 18 | method_exists, property_exists, serialize, sprintf, unserialize; 19 | use const JSON_THROW_ON_ERROR, PHP_VERSION_ID; 20 | 21 | abstract class SettingsContainerAbstract implements SettingsContainerInterface{ 22 | 23 | protected const SET_PREFIX = 'set_'; 24 | protected const GET_PREFIX = 'get_'; 25 | 26 | /** 27 | * SettingsContainerAbstract constructor. 28 | * 29 | * @phpstan-param array $properties 30 | */ 31 | public function __construct(iterable|null $properties = null){ 32 | 33 | if(!empty($properties)){ 34 | $this->fromIterable($properties); 35 | } 36 | 37 | $this->construct(); 38 | } 39 | 40 | /** 41 | * calls a method with trait name as replacement constructor for each used trait 42 | * (remember pre-php5 classname constructors? yeah, basically this.) 43 | */ 44 | protected function construct():void{ 45 | $traits = (new ReflectionObject($this))->getTraits(); 46 | 47 | foreach($traits as $trait){ 48 | $method = $trait->getShortName(); 49 | 50 | if(method_exists($this, $method)){ 51 | $this->{$method}(); 52 | } 53 | } 54 | 55 | } 56 | 57 | public function __get(string $property):mixed{ 58 | // back out if the property is inaccessible 59 | if(!property_exists($this, $property) || $this->isPrivate($property)){ 60 | 61 | if($this->throwOnInvalidProperty()){ 62 | throw new RuntimeException(sprintf('attempt to read invalid property: "$%s"', $property)); 63 | } 64 | 65 | return null; 66 | } 67 | // call an existing custom method, skip if the property has a hook 68 | if(method_exists($this, static::GET_PREFIX.$property) && !$this->hasGetHook($property)){ 69 | return $this->{static::GET_PREFIX.$property}(); 70 | } 71 | // retrieve the value (triggers an existing property hook) 72 | return $this->{$property}; 73 | } 74 | 75 | public function __set(string $property, mixed $value):void{ 76 | 77 | if(!property_exists($this, $property) || $this->isPrivate($property)){ 78 | 79 | if($this->throwOnInvalidProperty()){ 80 | throw new RuntimeException(sprintf('attempt to write invalid property: "$%s"', $property)); 81 | } 82 | 83 | return; 84 | } 85 | 86 | if(method_exists($this, static::SET_PREFIX.$property) && !$this->hasSetHook($property)){ 87 | $this->{static::SET_PREFIX.$property}($value); 88 | 89 | return; 90 | } 91 | 92 | $this->{$property} = $value; 93 | } 94 | 95 | public function __isset(string $property):bool{ 96 | return isset($this->{$property}) && !$this->isPrivate($property); 97 | } 98 | 99 | public function __unset(string $property):void{ 100 | 101 | if($this->__isset($property)){ 102 | unset($this->{$property}); 103 | } 104 | 105 | } 106 | 107 | public function __toString():string{ 108 | return $this->toJSON(); 109 | } 110 | 111 | /** 112 | * @internal Checks if a property is private 113 | */ 114 | final protected function isPrivate(string $property):bool{ 115 | return (new ReflectionProperty($this, $property))->isPrivate(); 116 | } 117 | 118 | /** 119 | * @internal Checks if a property has a "set" hook 120 | */ 121 | final protected function hasSetHook(string $property):bool{ 122 | 123 | if(PHP_VERSION_ID < 80400){ 124 | return false; 125 | } 126 | /** @phan-suppress-next-line PhanUndeclaredMethod, PhanUndeclaredClassConstant */ 127 | return (new ReflectionProperty($this, $property))->hasHook(\PropertyHookType::Set); 128 | } 129 | 130 | /** 131 | * @internal Checks if a property has a "get" hook 132 | */ 133 | final protected function hasGetHook(string $property):bool{ 134 | 135 | if(PHP_VERSION_ID < 80400){ 136 | return false; 137 | } 138 | /** @phan-suppress-next-line PhanUndeclaredMethod, PhanUndeclaredClassConstant */ 139 | return (new ReflectionProperty($this, $property))->hasHook(\PropertyHookType::Get); 140 | } 141 | 142 | /** 143 | * @internal Checks for the attribute "ThrowOnInvalidProperty", used in the magic get/set 144 | * 145 | * @see \chillerlan\Settings\Attributes\ThrowOnInvalidProperty 146 | */ 147 | final protected function throwOnInvalidProperty():bool{ 148 | 149 | $attributes = (new ReflectionObject($this)) 150 | ->getAttributes(ThrowOnInvalidProperty::class, ReflectionAttribute::IS_INSTANCEOF) 151 | ; 152 | 153 | if($attributes === []){ 154 | return false; 155 | } 156 | /** @var \chillerlan\Settings\Attributes\ThrowOnInvalidProperty $attr */ 157 | $attr = $attributes[0]->newInstance(); 158 | 159 | return $attr->throwOnInvalid; 160 | } 161 | 162 | public function toArray():array{ 163 | 164 | $properties = (new ReflectionObject($this)) 165 | ->getProperties(~(ReflectionProperty::IS_STATIC | ReflectionProperty::IS_READONLY | ReflectionProperty::IS_PRIVATE)) 166 | ; 167 | 168 | $data = []; 169 | 170 | foreach($properties as $reflectionProperty){ 171 | // the magic getter is called intentionally here, so that any existing hook methods are called on export 172 | $data[$reflectionProperty->name] = $this->__get($reflectionProperty->name); 173 | } 174 | 175 | return $data; 176 | } 177 | 178 | /** 179 | * @param iterable $properties 180 | */ 181 | public function fromIterable(iterable $properties):static{ 182 | 183 | foreach($properties as $key => $value){ 184 | $this->__set($key, $value); 185 | } 186 | 187 | return $this; 188 | } 189 | 190 | public function toJSON(int|null $jsonOptions = null):string{ 191 | $json = json_encode($this, ($jsonOptions ?? 0)); 192 | 193 | if($json === false){ 194 | throw new JsonException(json_last_error_msg()); // @codeCoverageIgnore 195 | } 196 | 197 | return $json; 198 | } 199 | 200 | public function fromJSON(string $json):static{ 201 | /** @phpstan-var array $data */ 202 | $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); 203 | 204 | return $this->fromIterable($data); 205 | } 206 | 207 | /** 208 | * @return array 209 | */ 210 | public function jsonSerialize():array{ 211 | return $this->toArray(); 212 | } 213 | 214 | /** 215 | * Returns a serialized string representation of the object in its current state (except static/readonly properties) 216 | */ 217 | public function serialize():string{ 218 | return serialize($this); 219 | } 220 | 221 | /** 222 | * Restores the data (except static/readonly properties) from the given serialized object to the current instance 223 | * 224 | * @throws \InvalidArgumentException 225 | */ 226 | public function unserialize(string $data):void{ 227 | $obj = unserialize($data); 228 | 229 | if(!is_object($obj)){ 230 | throw new InvalidArgumentException('The given serialized string is invalid'); 231 | } 232 | 233 | $reflection = new ReflectionObject($obj); 234 | 235 | if(!$reflection->isInstance($this)){ 236 | throw new InvalidArgumentException('The unserialized object does not match the class of this container'); 237 | } 238 | 239 | $properties = $reflection->getProperties(~(ReflectionProperty::IS_STATIC | ReflectionProperty::IS_READONLY)); 240 | $data = []; 241 | 242 | foreach($properties as $reflectionProperty){ 243 | $data[$reflectionProperty->name] = (PHP_VERSION_ID < 80400) 244 | ? $reflectionProperty->getValue($obj) 245 | /** @phan-suppress-next-line PhanUndeclaredMethod */ 246 | : $reflectionProperty->getRawValue($obj); 247 | } 248 | 249 | $this->__unserialize($data); 250 | } 251 | 252 | /** 253 | * Returns a serialized array representation of the object in its current state (except static/readonly properties), 254 | * bypassing custom getters and property hooks 255 | * 256 | * @return array 257 | */ 258 | public function __serialize():array{ 259 | 260 | $properties = (new ReflectionObject($this)) 261 | ->getProperties(~(ReflectionProperty::IS_STATIC | ReflectionProperty::IS_READONLY)) 262 | ; 263 | 264 | $data = []; 265 | 266 | foreach($properties as $reflectionProperty){ 267 | // bypass existing property hooks for PHP >= 8.4 268 | $data[$reflectionProperty->name] = (PHP_VERSION_ID < 80400) 269 | ? $reflectionProperty->getValue($this) 270 | /** @phan-suppress-next-line PhanUndeclaredMethod */ 271 | : $reflectionProperty->getRawValue($this); 272 | } 273 | 274 | return $data; 275 | } 276 | 277 | /** 278 | * Restores the data from the given array to the current instance, 279 | * bypassing custom setters and property hooks 280 | * 281 | * @param array $data 282 | */ 283 | public function __unserialize(array $data):void{ 284 | $reflection = new ReflectionObject($this); 285 | 286 | foreach($data as $key => $value){ 287 | try{ 288 | $reflectionProperty = $reflection->getProperty($key); 289 | 290 | if($reflectionProperty->isStatic() || $reflectionProperty->isReadOnly()){ 291 | continue; // @codeCoverageIgnore 292 | } 293 | 294 | (PHP_VERSION_ID < 80400) 295 | ? $reflectionProperty->setValue($this, $value) 296 | /** @phan-suppress-next-line PhanUndeclaredMethod */ 297 | : $reflectionProperty->setRawValue($this, $value); 298 | 299 | } 300 | // @codeCoverageIgnoreStart 301 | catch(ReflectionException){ 302 | // attempt to assign a non-existent property, skip 303 | continue; 304 | } 305 | // @codeCoverageIgnoreEnd 306 | } 307 | 308 | } 309 | 310 | } 311 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chillerlan/php-settings-container 2 | 3 | A container class for settings objects - decouple configuration logic from your application! Not a DI container. 4 | - [`SettingsContainerInterface`](https://github.com/chillerlan/php-settings-container/blob/main/src/SettingsContainerInterface.php) provides fancy [property hooks](https://wiki.php.net/rfc/property-hooks) for PHP < 8.4. 5 | 6 | [![PHP Version Support][php-badge]][php] 7 | [![version][packagist-badge]][packagist] 8 | [![license][license-badge]][license] 9 | [![Continuous Integration][gh-action-badge]][gh-action] 10 | [![Coverage][coverage-badge]][coverage] 11 | [![Codacy][codacy-badge]][codacy] 12 | [![Packagist downloads][downloads-badge]][downloads] 13 | 14 | [php-badge]: https://img.shields.io/packagist/php-v/chillerlan/php-settings-container?logo=php&color=8892BF 15 | [php]: https://www.php.net/supported-versions.php 16 | [packagist-badge]: https://img.shields.io/packagist/v/chillerlan/php-settings-container.svg?logo=packagist 17 | [packagist]: https://packagist.org/packages/chillerlan/php-settings-container 18 | [license-badge]: https://img.shields.io/github/license/chillerlan/php-settings-container.svg 19 | [license]: https://github.com/chillerlan/php-settings-container/blob/main/LICENSE 20 | [coverage-badge]: https://img.shields.io/codecov/c/github/chillerlan/php-settings-container.svg?logo=codecov 21 | [coverage]: https://codecov.io/github/chillerlan/php-settings-container 22 | [codacy-badge]: https://img.shields.io/codacy/grade/bd2467799e2943d2853ce3ebad5af490/main?logo=codacy 23 | [codacy]: https://app.codacy.com/gh/chillerlan/php-settings-container/dashboard?branch=main 24 | [downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-settings-container.svg?logo=packagist 25 | [downloads]: https://packagist.org/packages/chillerlan/php-settings-container/stats 26 | [gh-action-badge]: https://img.shields.io/github/actions/workflow/status/chillerlan/php-settings-container/ci.yml?branch=main&logo=github 27 | [gh-action]: https://github.com/chillerlan/php-settings-container/actions/workflows/ci.yml?query=branch%3Amain 28 | 29 | ## Documentation 30 | 31 | ### Installation 32 | **requires [composer](https://getcomposer.org)** 33 | 34 | *composer.json* (note: replace `dev-main` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^3.0` - see [releases](https://github.com/chillerlan/php-settings-container/releases) for valid versions) 35 | ```json 36 | { 37 | "require": { 38 | "php": "^8.1", 39 | "chillerlan/php-settings-container": "dev-main" 40 | } 41 | } 42 | ``` 43 | 44 | Profit! 45 | 46 | ## Usage 47 | 48 | The `SettingsContainerInterface` (wrapped in`SettingsContainerAbstract`) provides plug-in functionality for immutable object properties and adds some fancy, like loading/saving JSON, arrays etc. 49 | It takes an `iterable` as the only constructor argument and calls a method with the trait's name on invocation (`MyTrait::MyTrait()`) for each used trait. 50 | 51 | A PHPStan ruleset to exclude errors generated by accessing magic properties on `SettingsContainerInterface` can be found in `rules-magic-access.neon`. 52 | 53 | 54 | ### Simple usage 55 | ```php 56 | class MyContainer extends SettingsContainerAbstract{ 57 | protected string $foo; 58 | protected string $bar; 59 | } 60 | ``` 61 | 62 | ```php 63 | // use it just like a \stdClass (except the properties are fixed) 64 | $container = new MyContainer; 65 | $container->foo = 'what'; 66 | $container->bar = 'foo'; 67 | 68 | // which is equivalent to 69 | $container = new MyContainer(['bar' => 'foo', 'foo' => 'what']); 70 | // ...or try 71 | $container->fromJSON('{"foo": "what", "bar": "foo"}'); 72 | 73 | 74 | // fetch all properties as array 75 | $container->toArray(); // -> ['foo' => 'what', 'bar' => 'foo'] 76 | // or JSON 77 | $container->toJSON(); // -> {"foo": "what", "bar": "foo"} 78 | // JSON via JsonSerializable 79 | $json = json_encode($container); // -> {"foo": "what", "bar": "foo"} 80 | ``` 81 | 82 | By default, non-existing properties will be ignored and return `null`: 83 | 84 | ```php 85 | $container->nope = 'what'; 86 | 87 | var_dump($container->nope); // -> null 88 | ``` 89 | 90 | You can change this behaviour by adding the attribute `ThrowOnInvalidProperty` to your container class: 91 | 92 | ```php 93 | #[ThrowOnInvalidProperty(true)] 94 | class MyContainer extends SettingsContainerAbstract{ 95 | // ... 96 | } 97 | 98 | $container->nope = 'what'; // -> throws: attempt to write invalid property: "$nope" 99 | ``` 100 | 101 | 102 | ### Advanced usage 103 | 104 | Suppose the following trait from library 1: 105 | 106 | ```php 107 | trait SomeOptions{ 108 | protected string $foo; 109 | protected string $what; 110 | 111 | // this method will be called in SettingsContainerAbstract::construct() 112 | // after the properties have been set 113 | protected function SomeOptions():void{ 114 | // just some constructor stuff... 115 | $this->foo = strtoupper($this->foo); 116 | } 117 | 118 | /* 119 | * special prefixed magic setters & getters ("set_"/"get_" + property name) 120 | */ 121 | 122 | // this method will be called from __set() when property $what is set 123 | protected function set_what(string $value):void{ 124 | $this->what = md5($value); 125 | } 126 | 127 | // this method is called on __get() for the property $what 128 | protected function get_what():string{ 129 | return 'hash: '.$this->what; 130 | } 131 | } 132 | ``` 133 | 134 | And another trait from library 2: 135 | 136 | ```php 137 | trait MoreOptions{ 138 | protected string $bar = 'whatever'; // provide default values 139 | } 140 | ``` 141 | 142 | We can now plug the several library options together to a single class/object: 143 | 144 | ```php 145 | $commonOptions = [ 146 | // SomeOptions 147 | 'foo' => 'whatever', 148 | // MoreOptions 149 | 'bar' => 'nothing', 150 | ]; 151 | 152 | $container = new class ($commonOptions) extends SettingsContainerAbstract{ 153 | use SomeOptions, MoreOptions; 154 | }; 155 | 156 | var_dump($container->foo); // -> WHATEVER (constructor ran strtoupper on the value) 157 | var_dump($container->bar); // -> nothing 158 | 159 | $container->what = 'some value'; 160 | var_dump($container->what); // -> hash: 5946210c9e93ae37891dfe96c3e39614 (custom getter added "hash: ") 161 | ``` 162 | 163 | ### A note on property hooks (PHP 8.4+) 164 | 165 | Property hooks are called whenever a property is accessed (except from within the hook itself of course), which means that the custom get/set methods this library allows would conflict when a custom method is defined for a property that also has a hook defined. 166 | To prevent double method calls, the internal methods `hasSetHook()` and `hasGetHook()` have been introduced, and are called whenever the magic get/set methods are called: when both, a custom method and a property hook exist, only the property hook will be called. 167 |
Public properties will never call the magic get/set, however, their hooks *will* be called. (un)serializing a `SettingsContainerInterface` instance will bypass magic get/set and existing property hooks, while JSON de/encode as will call magic get/set or existing hooks explicitly via the `toArray()` and `fromIterable()` methods. 168 | 169 | ```php 170 | class PropertyHooksContainer extends SettingsContainerAbstract{ 171 | 172 | protected string $someValue{ 173 | set => doStuff($value); 174 | } 175 | 176 | // this method will be ignored in magic calls as a "set" hook on the property exists 177 | protected function set_someValue(string $value):void{ 178 | $this->someValue = doOtherStuff($value); 179 | } 180 | 181 | // this custom method will be called as the property has no "get" hook 182 | protected function get_someValue():string{ 183 | return doWhatever($this->someValue); 184 | } 185 | 186 | // this property will never trigger the magic get/set and associated methods 187 | public string $otherValue{ 188 | set => doStuff($value); 189 | get => $this->otherValue; 190 | } 191 | 192 | } 193 | ``` 194 | 195 | 196 | ### API 197 | 198 | #### [`SettingsContainerAbstract`](https://github.com/chillerlan/php-settings-container/blob/main/src/SettingsContainerAbstract.php) 199 | 200 | | method | return | info | 201 | |--------------------------------------------|------------------------------|---------------------------------------------------------------------------------------------------------------------| 202 | | `__construct(iterable $properties = null)` | - | calls `construct()` internally after the properties have been set | 203 | | `__get(string $property)` | mixed | calls `$this->{'get_'.$property}()` if such a method exists | 204 | | `__set(string $property, $value)` | void | calls `$this->{'set_'.$property}($value)` if such a method exists | 205 | | `__isset(string $property)` | bool | | 206 | | `__unset(string $property)` | void | | 207 | | `__toString()` | string | a JSON string | 208 | | `toArray()` | array | | 209 | | `fromIterable(iterable $properties)` | `SettingsContainerInterface` | | 210 | | `toJSON(int $jsonOptions = null)` | string | accepts [JSON options constants](http://php.net/manual/json.constants.php) | 211 | | `fromJSON(string $json)` | `SettingsContainerInterface` | | 212 | | `jsonSerialize()` | mixed | implements the [`JsonSerializable`](https://www.php.net/manual/en/jsonserializable.jsonserialize.php) interface | 213 | | `serialize()` | string | implements the [`Serializable`](https://www.php.net/manual/en/serializable.serialize.php) interface | 214 | | `unserialize(string $data)` | void | implements the [`Serializable`](https://www.php.net/manual/en/serializable.unserialize.php) interface | 215 | | `__serialize()` | array | implements the [`Serializable`](https://www.php.net/manual/en/language.oop5.magic.php#object.serialize) interface | 216 | | `__unserialize(array $data)` | void | implements the [`Serializable`](https://www.php.net/manual/en/language.oop5.magic.php#object.unserialize) interface | 217 | 218 | 219 | #### Internal (protected) methods 220 | 221 | | method | return | info | 222 | |--------------------------------------|--------|-------------------------------------------------------------------------------| 223 | | `construct()` | void | calls a method with trait name as replacement constructor for each used trait | 224 | | `isPrivate(string $property)` | bool | private properties are excluded from magic calls | 225 | | `hasSetHook(string $property)` | bool | | 226 | | `hasGetHook(string $property)` | bool | | 227 | 228 | 229 | ## Disclaimer 230 | This might be either an absolutely brilliant or completely stupid idea - you decide (in hindsight it was a great idea I guess - property hooks made their way into PHP 8.4). 231 |
Also, this is not a dependency injection container. Stop using DI containers FFS. 232 | --------------------------------------------------------------------------------