friendship ended with social-app. php is my new best friend
1<?php
2/**
3 * Class SettingsContainerAbstract
4 *
5 * @created 28.08.2018
6 * @author Smiley <smiley@chillerlan.net>
7 * @copyright 2018 Smiley
8 * @license MIT
9 */
10declare(strict_types=1);
11
12namespace chillerlan\Settings;
13
14use InvalidArgumentException, JsonException, ReflectionClass, ReflectionProperty;
15use function array_keys, get_object_vars, is_object, json_decode, json_encode,
16 json_last_error_msg, method_exists, property_exists, serialize, unserialize;
17use const JSON_THROW_ON_ERROR;
18
19abstract class SettingsContainerAbstract implements SettingsContainerInterface{
20
21 /**
22 * SettingsContainerAbstract constructor.
23 *
24 * @phpstan-param array<string, mixed> $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<string, mixed> $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<string, mixed>
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<string, mixed> $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}