1 <?php
2 /**
3 * Part of the Joomla Framework Data Package
4 *
5 * @copyright Copyright (C) 2005 - 2016 Open Source Matters, Inc. All rights reserved.
6 * @license GNU General Public License version 2 or later; see LICENSE
7 */
8
9 namespace Joomla\Data;
10
11 use Joomla\Registry\Registry;
12
13 /**
14 * DataObject is a class that is used to store data but allowing you to access the data
15 * by mimicking the way PHP handles class properties.
16 *
17 * @since 1.0
18 */
19 class DataObject implements DumpableInterface, \IteratorAggregate, \JsonSerializable, \Countable
20 {
21 /**
22 * The data object properties.
23 *
24 * @var array
25 * @since 1.0
26 */
27 private $properties = array();
28
29 /**
30 * The class constructor.
31 *
32 * @param mixed $properties Either an associative array or another object
33 * by which to set the initial properties of the new object.
34 *
35 * @since 1.0
36 * @throws \InvalidArgumentException
37 */
38 public function __construct($properties = array())
39 {
40 // Check the properties input.
41 if (!empty($properties))
42 {
43 // Bind the properties.
44 $this->bind($properties);
45 }
46 }
47
48 /**
49 * The magic get method is used to get a data property.
50 *
51 * This method is a public proxy for the protected getProperty method.
52 *
53 * Note: Magic __get does not allow recursive calls. This can be tricky because the error generated by recursing into
54 * __get is "Undefined property: {CLASS}::{PROPERTY}" which is misleading. This is relevant for this class because
55 * requesting a non-visible property can trigger a call to a sub-function. If that references the property directly in
56 * the object, it will cause a recursion into __get.
57 *
58 * @param string $property The name of the data property.
59 *
60 * @return mixed The value of the data property, or null if the data property does not exist.
61 *
62 * @see DataObject::getProperty()
63 * @since 1.0
64 */
65 public function __get($property)
66 {
67 return $this->getProperty($property);
68 }
69
70 /**
71 * The magic isset method is used to check the state of an object property.
72 *
73 * @param string $property The name of the data property.
74 *
75 * @return boolean True if set, otherwise false is returned.
76 *
77 * @since 1.0
78 */
79 public function __isset($property)
80 {
81 return isset($this->properties[$property]);
82 }
83
84 /**
85 * The magic set method is used to set a data property.
86 *
87 * This is a public proxy for the protected setProperty method.
88 *
89 * @param string $property The name of the data property.
90 * @param mixed $value The value to give the data property.
91 *
92 * @return void
93 *
94 * @see DataObject::setProperty()
95 * @since 1.0
96 */
97 public function __set($property, $value)
98 {
99 $this->setProperty($property, $value);
100 }
101
102 /**
103 * The magic unset method is used to unset a data property.
104 *
105 * @param string $property The name of the data property.
106 *
107 * @return void
108 *
109 * @since 1.0
110 */
111 public function __unset($property)
112 {
113 unset($this->properties[$property]);
114 }
115
116 /**
117 * Binds an array or object to this object.
118 *
119 * @param mixed $properties An associative array of properties or an object.
120 * @param boolean $updateNulls True to bind null values, false to ignore null values.
121 *
122 * @return DataObject Returns itself to allow chaining.
123 *
124 * @since 1.0
125 * @throws \InvalidArgumentException
126 */
127 public function bind($properties, $updateNulls = true)
128 {
129 // Check the properties data type.
130 if (!is_array($properties) && !is_object($properties))
131 {
132 throw new \InvalidArgumentException(sprintf('%s(%s)', __METHOD__, gettype($properties)));
133 }
134
135 // Check if the object is traversable.
136 if ($properties instanceof \Traversable)
137 {
138 // Convert iterator to array.
139 $properties = iterator_to_array($properties);
140 }
141 elseif (is_object($properties))
142 // Check if the object needs to be converted to an array.
143 {
144 // Convert properties to an array.
145 $properties = (array) $properties;
146 }
147
148 // Bind the properties.
149 foreach ($properties as $property => $value)
150 {
151 // Check if the value is null and should be bound.
152 if ($value === null && !$updateNulls)
153 {
154 continue;
155 }
156
157 // Set the property.
158 $this->setProperty($property, $value);
159 }
160
161 return $this;
162 }
163
164 /**
165 * Dumps the data properties into a stdClass object, recursively if appropriate.
166 *
167 * @param integer $depth The maximum depth of recursion (default = 3).
168 * For example, a depth of 0 will return a stdClass with all the properties in native
169 * form. A depth of 1 will recurse into the first level of properties only.
170 * @param \SplObjectStorage $dumped An array of already serialized objects that is used to avoid infinite loops.
171 *
172 * @return \stdClass The data properties as a simple PHP stdClass object.
173 *
174 * @since 1.0
175 */
176 public function dump($depth = 3, \SplObjectStorage $dumped = null)
177 {
178 // Check if we should initialise the recursion tracker.
179 if ($dumped === null)
180 {
181 $dumped = new \SplObjectStorage;
182 }
183
184 // Add this object to the dumped stack.
185 $dumped->attach($this);
186
187 // Setup a container.
188 $dump = new \stdClass;
189
190 // Dump all object properties.
191 foreach (array_keys($this->properties) as $property)
192 {
193 // Get the property.
194 $dump->$property = $this->dumpProperty($property, $depth, $dumped);
195 }
196
197 return $dump;
198 }
199
200 /**
201 * Gets this object represented as an ArrayIterator.
202 *
203 * This allows the data properties to be access via a foreach statement.
204 *
205 * @return \ArrayIterator This object represented as an ArrayIterator.
206 *
207 * @see IteratorAggregate::getIterator()
208 * @since 1.0
209 */
210 public function getIterator()
211 {
212 return new \ArrayIterator($this->dump(0));
213 }
214
215 /**
216 * Gets the data properties in a form that can be serialised to JSON format.
217 *
218 * @return string An object that can be serialised by json_encode().
219 *
220 * @since 1.0
221 */
222 public function jsonSerialize()
223 {
224 return $this->dump();
225 }
226
227 /**
228 * Dumps a data property.
229 *
230 * If recursion is set, this method will dump any object implementing Data\Dumpable (like Data\Object and Data\Set); it will
231 * convert a Date object to a string; and it will convert a Registry to an object.
232 *
233 * @param string $property The name of the data property.
234 * @param integer $depth The current depth of recursion (a value of 0 will ignore recursion).
235 * @param \SplObjectStorage $dumped An array of already serialized objects that is used to avoid infinite loops.
236 *
237 * @return mixed The value of the dumped property.
238 *
239 * @since 1.0
240 */
241 protected function dumpProperty($property, $depth, \SplObjectStorage $dumped)
242 {
243 $value = $this->getProperty($property);
244
245 if ($depth > 0)
246 {
247 // Check if the object is also an dumpable object.
248 if ($value instanceof DumpableInterface)
249 {
250 // Do not dump the property if it has already been dumped.
251 if (!$dumped->contains($value))
252 {
253 $value = $value->dump($depth - 1, $dumped);
254 }
255 }
256
257 // Check if the object is a date.
258 if ($value instanceof \DateTime)
259 {
260 $value = $value->format('Y-m-d H:i:s');
261 }
262 elseif ($value instanceof Registry)
263 // Check if the object is a registry.
264 {
265 $value = $value->toObject();
266 }
267 }
268
269 return $value;
270 }
271
272 /**
273 * Gets a data property.
274 *
275 * @param string $property The name of the data property.
276 *
277 * @return mixed The value of the data property.
278 *
279 * @see DataObject::__get()
280 * @since 1.0
281 */
282 protected function getProperty($property)
283 {
284 // Get the raw value.
285 $value = array_key_exists($property, $this->properties) ? $this->properties[$property] : null;
286
287 return $value;
288 }
289
290 /**
291 * Sets a data property.
292 *
293 * If the name of the property starts with a null byte, this method will return null.
294 *
295 * @param string $property The name of the data property.
296 * @param mixed $value The value to give the data property.
297 *
298 * @return mixed The value of the data property.
299 *
300 * @see DataObject::__set()
301 * @since 1.0
302 */
303 protected function setProperty($property, $value)
304 {
305 /*
306 * Check if the property starts with a null byte. If so, discard it because a later attempt to try to access it
307 * can cause a fatal error. See http://us3.php.net/manual/en/language.types.array.php#language.types.array.casting
308 */
309 if (strpos($property, "\0") === 0)
310 {
311 return null;
312 }
313
314 // Set the value.
315 $this->properties[$property] = $value;
316
317 return $value;
318 }
319
320 /**
321 * Count the number of data properties.
322 *
323 * @return integer The number of data properties.
324 *
325 * @since 1.0
326 */
327 public function count()
328 {
329 return count($this->properties);
330 }
331 }
332