├── LICENSE ├── README.md ├── composer.json ├── rules-magic-access.neon └── src ├── SettingsContainerAbstract.php └── SettingsContainerInterface.php /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 | -------------------------------------------------------------------------------- /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 immutable properties with magic getter & setter and some fancy. 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://www.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 | //non-existing properties will be ignored: 82 | $container->nope = 'what'; 83 | 84 | var_dump($container->nope); // -> null 85 | ``` 86 | 87 | ### Advanced usage 88 | ```php 89 | // from library 1 90 | trait SomeOptions{ 91 | protected string $foo; 92 | protected string $what; 93 | 94 | // this method will be called in SettingsContainerAbstract::construct() 95 | // after the properties have been set 96 | protected function SomeOptions():void{ 97 | // just some constructor stuff... 98 | $this->foo = strtoupper($this->foo); 99 | } 100 | 101 | /* 102 | * special prefixed magic setters & getters 103 | */ 104 | 105 | // this method will be called from __set() when property $what is set 106 | protected function set_what(string $value):void{ 107 | $this->what = md5($value); 108 | } 109 | 110 | // this method is called on __get() for the property $what 111 | protected function get_what():string{ 112 | return 'hash: '.$this->what; 113 | } 114 | } 115 | 116 | // from library 2 117 | trait MoreOptions{ 118 | protected string $bar = 'whatever'; // provide default values 119 | } 120 | ``` 121 | 122 | ```php 123 | $commonOptions = [ 124 | // SomeOptions 125 | 'foo' => 'whatever', 126 | // MoreOptions 127 | 'bar' => 'nothing', 128 | ]; 129 | 130 | // now plug the several library options together to a single object 131 | $container = new class ($commonOptions) extends SettingsContainerAbstract{ 132 | use SomeOptions, MoreOptions; 133 | }; 134 | 135 | var_dump($container->foo); // -> WHATEVER (constructor ran strtoupper on the value) 136 | var_dump($container->bar); // -> nothing 137 | 138 | $container->what = 'some value'; 139 | var_dump($container->what); // -> hash: 5946210c9e93ae37891dfe96c3e39614 (custom getter added "hash: ") 140 | ``` 141 | 142 | ### API 143 | 144 | #### [`SettingsContainerAbstract`](https://github.com/chillerlan/php-settings-container/blob/main/src/SettingsContainerAbstract.php) 145 | 146 | | method | return | info | 147 | |--------------------------------------------|------------------------------|---------------------------------------------------------------------------------------------------------------------| 148 | | `__construct(iterable $properties = null)` | - | calls `construct()` internally after the properties have been set | 149 | | (protected) `construct()` | void | calls a method with trait name as replacement constructor for each used trait | 150 | | `__get(string $property)` | mixed | calls `$this->{'get_'.$property}()` if such a method exists | 151 | | `__set(string $property, $value)` | void | calls `$this->{'set_'.$property}($value)` if such a method exists | 152 | | `__isset(string $property)` | bool | | 153 | | `__unset(string $property)` | void | | 154 | | `__toString()` | string | a JSON string | 155 | | `toArray()` | array | | 156 | | `fromIterable(iterable $properties)` | `SettingsContainerInterface` | | 157 | | `toJSON(int $jsonOptions = null)` | string | accepts [JSON options constants](http://php.net/manual/json.constants.php) | 158 | | `fromJSON(string $json)` | `SettingsContainerInterface` | | 159 | | `jsonSerialize()` | mixed | implements the [`JsonSerializable`](https://www.php.net/manual/en/jsonserializable.jsonserialize.php) interface | 160 | | `serialize()` | string | implements the [`Serializable`](https://www.php.net/manual/en/serializable.serialize.php) interface | 161 | | `unserialize(string $data)` | void | implements the [`Serializable`](https://www.php.net/manual/en/serializable.unserialize.php) interface | 162 | | `__serialize()` | array | implements the [`Serializable`](https://www.php.net/manual/en/language.oop5.magic.php#object.serialize) interface | 163 | | `__unserialize(array $data)` | void | implements the [`Serializable`](https://www.php.net/manual/en/language.oop5.magic.php#object.unserialize) interface | 164 | 165 | ## Disclaimer 166 | This might be either an utterly genius or completely stupid idea - you decide. However, i like it and it works. 167 | Also, this is not a dependency injection container. Stop using DI containers FFS. 168 | -------------------------------------------------------------------------------- /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" 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 | "phpmd/phpmd": "^2.15", 28 | "phpstan/phpstan": "^1.11", 29 | "phpstan/phpstan-deprecation-rules": "^1.2", 30 | "phpunit/phpunit": "^10.5", 31 | "squizlabs/php_codesniffer": "^3.10" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "chillerlan\\Settings\\": "src" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "chillerlan\\SettingsTest\\": "tests" 41 | } 42 | }, 43 | "scripts": { 44 | "phpunit": "@php vendor/bin/phpunit", 45 | "phpstan": "@php vendor/bin/phpstan" 46 | }, 47 | "config": { 48 | "lock": false, 49 | "sort-packages": true, 50 | "platform-check": true 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 InvalidArgumentException, JsonException, ReflectionClass, ReflectionProperty; 15 | use function array_keys, get_object_vars, is_object, json_decode, json_encode, 16 | json_last_error_msg, method_exists, property_exists, serialize, unserialize; 17 | use const JSON_THROW_ON_ERROR; 18 | 19 | abstract class SettingsContainerAbstract implements SettingsContainerInterface{ 20 | 21 | /** 22 | * SettingsContainerAbstract constructor. 23 | * 24 | * @phpstan-param array $properties 25 | */ 26 | public function __construct(iterable|null $properties = null){ 27 | 28 | if(!empty($properties)){ 29 | $this->fromIterable($properties); 30 | } 31 | 32 | $this->construct(); 33 | } 34 | 35 | /** 36 | * calls a method with trait name as replacement constructor for each used trait 37 | * (remember pre-php5 classname constructors? yeah, basically this.) 38 | */ 39 | protected function construct():void{ 40 | $traits = (new ReflectionClass($this))->getTraits(); 41 | 42 | foreach($traits as $trait){ 43 | $method = $trait->getShortName(); 44 | 45 | if(method_exists($this, $method)){ 46 | $this->{$method}(); 47 | } 48 | } 49 | 50 | } 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | public function __get(string $property):mixed{ 56 | 57 | if(!property_exists($this, $property) || $this->isPrivate($property)){ 58 | return null; 59 | } 60 | 61 | $method = 'get_'.$property; 62 | 63 | if(method_exists($this, $method)){ 64 | return $this->{$method}(); 65 | } 66 | 67 | return $this->{$property}; 68 | } 69 | 70 | /** 71 | * @inheritdoc 72 | */ 73 | public function __set(string $property, mixed $value):void{ 74 | 75 | if(!property_exists($this, $property) || $this->isPrivate($property)){ 76 | return; 77 | } 78 | 79 | $method = 'set_'.$property; 80 | 81 | if(method_exists($this, $method)){ 82 | $this->{$method}($value); 83 | 84 | return; 85 | } 86 | 87 | $this->{$property} = $value; 88 | } 89 | 90 | /** 91 | * @inheritdoc 92 | */ 93 | public function __isset(string $property):bool{ 94 | return isset($this->{$property}) && !$this->isPrivate($property); 95 | } 96 | 97 | /** 98 | * @internal Checks if a property is private 99 | */ 100 | protected function isPrivate(string $property):bool{ 101 | return (new ReflectionProperty($this, $property))->isPrivate(); 102 | } 103 | 104 | /** 105 | * @inheritdoc 106 | */ 107 | public function __unset(string $property):void{ 108 | 109 | if($this->__isset($property)){ 110 | unset($this->{$property}); 111 | } 112 | 113 | } 114 | 115 | /** 116 | * @inheritdoc 117 | */ 118 | public function __toString():string{ 119 | return $this->toJSON(); 120 | } 121 | 122 | /** 123 | * @inheritdoc 124 | */ 125 | public function toArray():array{ 126 | $properties = []; 127 | 128 | foreach(array_keys(get_object_vars($this)) as $key){ 129 | $properties[$key] = $this->__get($key); 130 | } 131 | 132 | return $properties; 133 | } 134 | 135 | /** 136 | * @inheritdoc 137 | */ 138 | public function fromIterable(iterable $properties):static{ 139 | 140 | foreach($properties as $key => $value){ 141 | $this->__set($key, $value); 142 | } 143 | 144 | return $this; 145 | } 146 | 147 | /** 148 | * @inheritdoc 149 | */ 150 | public function toJSON(int|null $jsonOptions = null):string{ 151 | $json = json_encode($this, ($jsonOptions ?? 0)); 152 | 153 | if($json === false){ 154 | throw new JsonException(json_last_error_msg()); 155 | } 156 | 157 | return $json; 158 | } 159 | 160 | /** 161 | * @inheritdoc 162 | */ 163 | public function fromJSON(string $json):static{ 164 | /** @phpstan-var array $data */ 165 | $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); 166 | 167 | return $this->fromIterable($data); 168 | } 169 | 170 | /** 171 | * @inheritdoc 172 | * @return array 173 | */ 174 | public function jsonSerialize():array{ 175 | return $this->toArray(); 176 | } 177 | 178 | /** 179 | * Returns a serialized string representation of the object in its current state (except static/readonly properties) 180 | * 181 | * @inheritdoc 182 | * @see \chillerlan\Settings\SettingsContainerInterface::toArray() 183 | */ 184 | public function serialize():string{ 185 | return serialize($this); 186 | } 187 | 188 | /** 189 | * Restores the data (except static/readonly properties) from the given serialized object to the current instance 190 | * 191 | * @inheritdoc 192 | * @see \chillerlan\Settings\SettingsContainerInterface::fromIterable() 193 | */ 194 | public function unserialize(string $data):void{ 195 | $obj = unserialize($data); 196 | 197 | if($obj === false || !is_object($obj)){ 198 | throw new InvalidArgumentException('The given serialized string is invalid'); 199 | } 200 | 201 | $reflection = new ReflectionClass($obj); 202 | 203 | if(!$reflection->isInstance($this)){ 204 | throw new InvalidArgumentException('The unserialized object does not match the class of this container'); 205 | } 206 | 207 | $properties = $reflection->getProperties(~(ReflectionProperty::IS_STATIC | ReflectionProperty::IS_READONLY)); 208 | 209 | foreach($properties as $reflectionProperty){ 210 | $this->{$reflectionProperty->name} = $reflectionProperty->getValue($obj); 211 | } 212 | 213 | } 214 | 215 | /** 216 | * Returns a serialized string representation of the object in its current state (except static/readonly properties) 217 | * 218 | * @inheritdoc 219 | * @see \chillerlan\Settings\SettingsContainerInterface::toArray() 220 | */ 221 | public function __serialize():array{ 222 | 223 | $properties = (new ReflectionClass($this)) 224 | ->getProperties(~(ReflectionProperty::IS_STATIC | ReflectionProperty::IS_READONLY)) 225 | ; 226 | 227 | $data = []; 228 | 229 | foreach($properties as $reflectionProperty){ 230 | $data[$reflectionProperty->name] = $reflectionProperty->getValue($this); 231 | } 232 | 233 | return $data; 234 | } 235 | 236 | /** 237 | * Restores the data from the given array to the current instance 238 | * 239 | * @inheritdoc 240 | * @see \chillerlan\Settings\SettingsContainerInterface::fromIterable() 241 | * 242 | * @param array $data 243 | */ 244 | public function __unserialize(array $data):void{ 245 | 246 | foreach($data as $key => $value){ 247 | $this->{$key} = $value; 248 | } 249 | 250 | } 251 | 252 | } 253 | -------------------------------------------------------------------------------- /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 | * @phpstan-param array $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 | --------------------------------------------------------------------------------