1: <?php
2:
3: namespace Charcoal\Config;
4:
5: use ArrayAccess;
6: use InvalidArgumentException;
7:
8: /**
9: * Default data model.
10: *
11: * ### Notes on {@see \ArrayAccess}:
12: *
13: * - Keys SHOULD be formatted as "snake_case" (e.g., "first_name") or "camelCase" (e.g., "firstName").
14: * and WILL be converted to the latter {@see PSR-1} to access or assign values.
15: * - Values are accessed and assigned via methods and properties which MUST be formatted as "camelCase",
16: * e.g.: `$firstName`, `firstName()`, `setFirstName()`.
17: * - A key-value pair is internally passed to a (non-private / non-static) setter method (if present)
18: * or assigned to a (non-private / non-static) property (declared or not) and tracks affected keys.
19: */
20: abstract class AbstractEntity implements EntityInterface
21: {
22: /**
23: * Holds a list of all data keys.
24: *
25: * @var array
26: */
27: protected $keys = [];
28:
29: /**
30: * Gets the data keys on this entity.
31: *
32: * @return array
33: */
34: public function keys()
35: {
36: return array_keys($this->keys);
37: }
38:
39: /**
40: * Gets all data, or a subset, from this entity.
41: *
42: * @uses self::offsetExists()
43: * @uses self::offsetGet()
44: * @param string[] $keys Optional. Extracts only the requested data.
45: * @return array Key-value array of data, excluding pairs with NULL values.
46: */
47: public function data(array $keys = null)
48: {
49: if ($keys === null) {
50: $keys = $this->keys();
51: }
52:
53: $data = [];
54: foreach ($keys as $key) {
55: if (strtolower($key) === 'data') {
56: /** @internal Edge Case: Avoid recursive call */
57: continue;
58: }
59:
60: if (isset($this[$key])) {
61: $data[$key] = $this[$key];
62: }
63: }
64: return $data;
65: }
66:
67: /**
68: * Sets data on this entity.
69: *
70: * @uses self::offsetSet()
71: * @param array $data Key-value array of data to append.
72: * @return self
73: */
74: public function setData(array $data)
75: {
76: foreach ($data as $key => $value) {
77: if (strtolower($key) === 'data') {
78: /** @internal Edge Case: Avoid recursive call */
79: continue;
80: }
81:
82: $this[$key] = $value;
83: }
84: return $this;
85: }
86:
87: /**
88: * Determines if this entity contains the specified key and if its value is not NULL.
89: *
90: * @uses self::offsetExists()
91: * @param string $key The data key to check.
92: * @return boolean TRUE if $key exists and has a value other than NULL, FALSE otherwise.
93: */
94: public function has($key)
95: {
96: return isset($this[$key]);
97: }
98:
99: /**
100: * Find an entry of the configuration by its key and retrieve it.
101: *
102: * @uses self::offsetGet()
103: * @param string $key The data key to retrieve.
104: * @return mixed Value of the requested $key on success, NULL if the $key is not set.
105: */
106: public function get($key)
107: {
108: return $this[$key];
109: }
110:
111: /**
112: * Assign a value to the specified key on this entity.
113: *
114: * @uses self::offsetSet()
115: * @param string $key The data key to assign $value to.
116: * @param mixed $value The data value to assign to $key.
117: * @return self Chainable
118: */
119: public function set($key, $value)
120: {
121: $this[$key] = $value;
122: return $this;
123: }
124:
125: /**
126: * Determines if this entity contains the specified key and if its value is not NULL.
127: *
128: * Routine:
129: * - If the entity has a getter method (e.g., "foo_bar" → `fooBar()`),
130: * its called and its value is checked;
131: * - If the entity has a property (e.g., `$fooBar`), its value is checked;
132: * - If the entity has neither, FALSE is returned.
133: *
134: * @see \ArrayAccess
135: * @param string $key The data key to check.
136: * @throws InvalidArgumentException If the $key is not a string or is a numeric value.
137: * @return boolean TRUE if $key exists and has a value other than NULL, FALSE otherwise.
138: */
139: public function offsetExists($key)
140: {
141: if (is_numeric($key)) {
142: throw new InvalidArgumentException(
143: 'Entity array access only supports non-numeric keys'
144: );
145: }
146:
147: $key = $this->camelize($key);
148:
149: /** @internal Edge Case: "_" → "" */
150: if ($key === '') {
151: return false;
152: }
153:
154: if (is_callable([ $this, $key ])) {
155: $value = $this->{$key}();
156: } else {
157: if (!isset($this->{$key})) {
158: return false;
159: }
160: $value = $this->{$key};
161: }
162:
163: return ($value !== null);
164: }
165:
166: /**
167: * Returns the value from the specified key on this entity.
168: *
169: * Routine:
170: * - If the entity has a getter method (e.g., "foo_bar" → `fooBar()`),
171: * its called and returns its value;
172: * - If the entity has a property (e.g., `$fooBar`), its value is returned;
173: * - If the entity has neither, NULL is returned.
174: *
175: * @see \ArrayAccess
176: * @param string $key The data key to retrieve.
177: * @throws InvalidArgumentException If the $key is not a string or is a numeric value.
178: * @return mixed Value of the requested $key on success, NULL if the $key is not set.
179: */
180: public function offsetGet($key)
181: {
182: if (is_numeric($key)) {
183: throw new InvalidArgumentException(
184: 'Entity array access only supports non-numeric keys'
185: );
186: }
187:
188: $key = $this->camelize($key);
189:
190: /** @internal Edge Case: "_" → "" */
191: if ($key === '') {
192: return null;
193: }
194:
195: if (is_callable([ $this, $key ])) {
196: return $this->{$key}();
197: } else {
198: if (isset($this->{$key})) {
199: return $this->{$key};
200: } else {
201: return null;
202: }
203: }
204: }
205:
206: /**
207: * Assigns the value to the specified key on this entity.
208: *
209: * Routine:
210: * - The data key is added to the {@see self::$keys entity's key pool}.
211: * - If the entity has a setter method (e.g., "foo_bar" → `setFooBar()`),
212: * its called and passed the value;
213: * - If the entity has NO setter method, the value is assigned to a property (e.g., `$fooBar`).
214: *
215: * @see \ArrayAccess
216: * @param string $key The data key to assign $value to.
217: * @param mixed $value The data value to assign to $key.
218: * @throws InvalidArgumentException If the $key is not a string or is a numeric value.
219: * @return void
220: */
221: public function offsetSet($key, $value)
222: {
223: if (is_numeric($key)) {
224: throw new InvalidArgumentException(
225: 'Entity array access only supports non-numeric keys'
226: );
227: }
228:
229: $key = $this->camelize($key);
230:
231: /** @internal Edge Case: "_" → "" */
232: if ($key === '') {
233: return;
234: }
235:
236: $setter = 'set'.ucfirst($key);
237: if (is_callable([ $this, $setter ])) {
238: $this->{$setter}($value);
239: } else {
240: $this->{$key} = $value;
241: }
242:
243: $this->keys[$key] = true;
244: }
245:
246: /**
247: * Removes the value from the specified key on this entity.
248: *
249: * Routine:
250: * - The data key is removed from the {@see self::$keys entity's key pool}.
251: * - NULL is {@see self::offsetSet() assigned} to the entity.
252: *
253: * @see \ArrayAccess
254: * @uses self::offsetSet()
255: * @param string $key The data key to remove.
256: * @throws InvalidArgumentException If the $key is not a string or is a numeric value.
257: * @return void
258: */
259: public function offsetUnset($key)
260: {
261: if (is_numeric($key)) {
262: throw new InvalidArgumentException(
263: 'Entity array access only supports non-numeric keys'
264: );
265: }
266:
267: $key = $this->camelize($key);
268:
269: /** @internal Edge Case: "_" → "" */
270: if ($key === '') {
271: return;
272: }
273:
274: $this[$key] = null;
275: unset($this->keys[$key]);
276: }
277:
278: /**
279: * Gets the data that can be serialized with {@see json_encode()}.
280: *
281: * @see \JsonSerializable
282: * @return array Key-value array of data.
283: */
284: public function jsonSerialize()
285: {
286: return $this->data();
287: }
288:
289: /**
290: * Serializes the data on this entity.
291: *
292: * @see \Serializable
293: * @return string Returns a string containing a byte-stream representation of the object.
294: */
295: public function serialize()
296: {
297: return serialize($this->data());
298: }
299:
300: /**
301: * Applies the serialized data to this entity.
302: *
303: * @see \Serializable
304: * @param string $data The serialized data to extract.
305: * @return void
306: */
307: public function unserialize($data)
308: {
309: $data = unserialize($data);
310: $this->setData($data);
311: }
312:
313: /**
314: * Transform a string from "snake_case" to "camelCase".
315: *
316: * @param string $str The string to camelize.
317: * @return string The camelized string.
318: */
319: final protected function camelize($str)
320: {
321: if (strstr($str, '_') === false) {
322: return $str;
323: }
324: return lcfirst(implode('', array_map('ucfirst', explode('_', $str))));
325: }
326: }
327: