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 /**
12 * DataSet is a collection class that allows the developer to operate on a set of DataObject objects as if they were in a
13 * typical PHP array.
14 *
15 * @since 1.0
16 */
17 class DataSet implements DumpableInterface, \ArrayAccess, \Countable, \Iterator
18 {
19 /**
20 * The current position of the iterator.
21 *
22 * @var integer
23 * @since 1.0
24 */
25 private $current = false;
26
27 /**
28 * The iterator objects.
29 *
30 * @var DataObject[]
31 * @since 1.0
32 */
33 private $objects = array();
34
35 /**
36 * The class constructor.
37 *
38 * @param DataObject[] $objects An array of DataObject objects to bind to the data set.
39 *
40 * @since 1.0
41 * @throws \InvalidArgumentException if an object is not an instance of Data\Object.
42 */
43 public function __construct(array $objects = array())
44 {
45 // Set the objects.
46 $this->_initialise($objects);
47 }
48
49 /**
50 * The magic call method is used to call object methods using the iterator.
51 *
52 * Example: $array = $objectList->foo('bar');
53 *
54 * The object list will iterate over its objects and see if each object has a callable 'foo' method.
55 * If so, it will pass the argument list and assemble any return values. If an object does not have
56 * a callable method no return value is recorded.
57 * The keys of the objects and the result array are maintained.
58 *
59 * @param string $method The name of the method called.
60 * @param array $arguments The arguments of the method called.
61 *
62 * @return array An array of values returned by the methods called on the objects in the data set.
63 *
64 * @since 1.0
65 */
66 public function __call($method, $arguments = array())
67 {
68 $return = array();
69
70 // Iterate through the objects.
71 foreach ($this->objects as $key => $object)
72 {
73 // Create the object callback.
74 $callback = array($object, $method);
75
76 // Check if the callback is callable.
77 if (is_callable($callback))
78 {
79 // Call the method for the object.
80 $return[$key] = call_user_func_array($callback, $arguments);
81 }
82 }
83
84 return $return;
85 }
86
87 /**
88 * The magic get method is used to get a list of properties from the objects in the data set.
89 *
90 * Example: $array = $dataSet->foo;
91 *
92 * This will return a column of the values of the 'foo' property in all the objects
93 * (or values determined by custom property setters in the individual Data\Object's).
94 * The result array will contain an entry for each object in the list (compared to __call which may not).
95 * The keys of the objects and the result array are maintained.
96 *
97 * @param string $property The name of the data property.
98 *
99 * @return array An associative array of the values.
100 *
101 * @since 1.0
102 */
103 public function __get($property)
104 {
105 $return = array();
106
107 // Iterate through the objects.
108 foreach ($this->objects as $key => $object)
109 {
110 // Get the property.
111 $return[$key] = $object->$property;
112 }
113
114 return $return;
115 }
116
117 /**
118 * The magic isset method is used to check the state of an object property using the iterator.
119 *
120 * Example: $array = isset($objectList->foo);
121 *
122 * @param string $property The name of the property.
123 *
124 * @return boolean True if the property is set in any of the objects in the data set.
125 *
126 * @since 1.0
127 */
128 public function __isset($property)
129 {
130 $return = array();
131
132 // Iterate through the objects.
133 foreach ($this->objects as $object)
134 {
135 // Check the property.
136 $return[] = isset($object->$property);
137 }
138
139 return in_array(true, $return, true) ? true : false;
140 }
141
142 /**
143 * The magic set method is used to set an object property using the iterator.
144 *
145 * Example: $objectList->foo = 'bar';
146 *
147 * This will set the 'foo' property to 'bar' in all of the objects
148 * (or a value determined by custom property setters in the Data\Object).
149 *
150 * @param string $property The name of the property.
151 * @param mixed $value The value to give the data property.
152 *
153 * @return void
154 *
155 * @since 1.0
156 */
157 public function __set($property, $value)
158 {
159 // Iterate through the objects.
160 foreach ($this->objects as $object)
161 {
162 // Set the property.
163 $object->$property = $value;
164 }
165 }
166
167 /**
168 * The magic unset method is used to unset an object property using the iterator.
169 *
170 * Example: unset($objectList->foo);
171 *
172 * This will unset all of the 'foo' properties in the list of Data\Object's.
173 *
174 * @param string $property The name of the property.
175 *
176 * @return void
177 *
178 * @since 1.0
179 */
180 public function __unset($property)
181 {
182 // Iterate through the objects.
183 foreach ($this->objects as $object)
184 {
185 unset($object->$property);
186 }
187 }
188
189 /**
190 * Gets an array of keys, existing in objects
191 *
192 * @param string $type Selection type 'all' or 'common'
193 *
194 * @return array Array of keys
195 *
196 * @since 1.2.0
197 * @throws \InvalidArgumentException
198 */
199 public function getObjectsKeys($type = 'all')
200 {
201 $keys = null;
202
203 if ($type == 'all')
204 {
205 $function = 'array_merge';
206 }
207 elseif ($type == 'common')
208 {
209 $function = 'array_intersect_key';
210 }
211 else
212 {
213 throw new \InvalidArgumentException("Unknown selection type: $type");
214 }
215
216 foreach ($this->objects as $object)
217 {
218 if (version_compare(PHP_VERSION, '5.4.0', '<'))
219 {
220 $object_vars = json_decode(json_encode($object->jsonSerialize()), true);
221 }
222 else
223 {
224 $object_vars = json_decode(json_encode($object), true);
225 }
226
227 $keys = (is_null($keys)) ? $object_vars : $function($keys, $object_vars);
228 }
229
230 return array_keys($keys);
231 }
232
233 /**
234 * Gets all objects as an array
235 *
236 * @param boolean $associative Option to set return mode: associative or numeric array.
237 * @param string $k Unlimited optional property names to extract from objects.
238 *
239 * @return array Returns an array according to defined options.
240 *
241 * @since 1.2.0
242 */
243 public function toArray($associative = true, $k = null)
244 {
245 $keys = func_get_args();
246 $associative = array_shift($keys);
247
248 if (empty($keys))
249 {
250 $keys = $this->getObjectsKeys();
251 }
252
253 $return = array();
254
255 $i = 0;
256
257 foreach ($this->objects as $key => $object)
258 {
259 $array_item = array();
260
261 $key = ($associative) ? $key : $i++;
262
263 $j = 0;
264
265 foreach ($keys as $property)
266 {
267 $property_key = ($associative) ? $property : $j++;
268 $array_item[$property_key] = (isset($object->$property)) ? $object->$property : null;
269 }
270
271 $return[$key] = $array_item;
272 }
273
274 return $return;
275 }
276
277 /**
278 * Gets the number of data objects in the set.
279 *
280 * @return integer The number of objects.
281 *
282 * @since 1.0
283 */
284 public function count()
285 {
286 return count($this->objects);
287 }
288
289 /**
290 * Clears the objects in the data set.
291 *
292 * @return DataSet Returns itself to allow chaining.
293 *
294 * @since 1.0
295 */
296 public function clear()
297 {
298 $this->objects = array();
299 $this->rewind();
300
301 return $this;
302 }
303
304 /**
305 * Get the current data object in the set.
306 *
307 * @return DataObject The current object, or false if the array is empty or the pointer is beyond the end of the elements.
308 *
309 * @since 1.0
310 */
311 public function current()
312 {
313 return is_scalar($this->current) ? $this->objects[$this->current] : false;
314 }
315
316 /**
317 * Dumps the data object in the set, recursively if appropriate.
318 *
319 * @param integer $depth The maximum depth of recursion (default = 3).
320 * For example, a depth of 0 will return a stdClass with all the properties in native
321 * form. A depth of 1 will recurse into the first level of properties only.
322 * @param \SplObjectStorage $dumped An array of already serialized objects that is used to avoid infinite loops.
323 *
324 * @return array An associative array of the data objects in the set, dumped as a simple PHP stdClass object.
325 *
326 * @see DataObject::dump()
327 * @since 1.0
328 */
329 public function dump($depth = 3, \SplObjectStorage $dumped = null)
330 {
331 // Check if we should initialise the recursion tracker.
332 if ($dumped === null)
333 {
334 $dumped = new \SplObjectStorage;
335 }
336
337 // Add this object to the dumped stack.
338 $dumped->attach($this);
339
340 $objects = array();
341
342 // Make sure that we have not reached our maximum depth.
343 if ($depth > 0)
344 {
345 // Handle JSON serialization recursively.
346 foreach ($this->objects as $key => $object)
347 {
348 $objects[$key] = $object->dump($depth, $dumped);
349 }
350 }
351
352 return $objects;
353 }
354
355 /**
356 * Gets the data set in a form that can be serialised to JSON format.
357 *
358 * Note that this method will not return an associative array, otherwise it would be encoded into an object.
359 * JSON decoders do not consistently maintain the order of associative keys, whereas they do maintain the order of arrays.
360 *
361 * @param mixed $serialized An array of objects that have already been serialized that is used to infinite loops
362 * (null on first call).
363 *
364 * @return array An array that can be serialised by json_encode().
365 *
366 * @since 1.0
367 */
368 public function jsonSerialize($serialized = null)
369 {
370 // Check if we should initialise the recursion tracker.
371 if ($serialized === null)
372 {
373 $serialized = array();
374 }
375
376 // Add this object to the serialized stack.
377 $serialized[] = spl_object_hash($this);
378 $return = array();
379
380 // Iterate through the objects.
381 foreach ($this->objects as $object)
382 {
383 // Call the method for the object.
384 $return[] = $object->jsonSerialize($serialized);
385 }
386
387 return $return;
388 }
389
390 /**
391 * Gets the key of the current object in the iterator.
392 *
393 * @return scalar The object key on success; null on failure.
394 *
395 * @since 1.0
396 */
397 public function key()
398 {
399 return $this->current;
400 }
401
402 /**
403 * Gets the array of keys for all the objects in the iterator (emulates array_keys).
404 *
405 * @return array The array of keys
406 *
407 * @since 1.0
408 */
409 public function keys()
410 {
411 return array_keys($this->objects);
412 }
413
414 /**
415 * Applies a function to every object in the set (emulates array_walk).
416 *
417 * @param callable $funcname Callback function.
418 *
419 * @return boolean
420 *
421 * @since 1.2.0
422 * @throws \InvalidArgumentException
423 */
424 public function walk($funcname)
425 {
426 if (!is_callable($funcname))
427 {
428 $message = __METHOD__ . '() expects parameter 1 to be a valid callback';
429
430 if (is_string($funcname))
431 {
432 $message .= sprintf(', function \'%s\' not found or invalid function name', $funcname);
433 }
434
435 throw new \InvalidArgumentException($message);
436 }
437
438 foreach ($this->objects as $key => $object)
439 {
440 $funcname($object, $key);
441 }
442
443 return true;
444 }
445
446 /**
447 * Advances the iterator to the next object in the iterator.
448 *
449 * @return void
450 *
451 * @since 1.0
452 */
453 public function next()
454 {
455 // Get the object offsets.
456 $keys = $this->keys();
457
458 // Check if _current has been set to false but offsetUnset.
459 if ($this->current === false && isset($keys[0]))
460 {
461 // This is a special case where offsetUnset was used in a foreach loop and the first element was unset.
462 $this->current = $keys[0];
463 }
464 else
465 {
466 // Get the current key.
467 $position = array_search($this->current, $keys);
468
469 // Check if there is an object after the current object.
470 if ($position !== false && isset($keys[$position + 1]))
471 {
472 // Get the next id.
473 $this->current = $keys[$position + 1];
474 }
475 else
476 {
477 // That was the last object or the internal properties have become corrupted.
478 $this->current = null;
479 }
480 }
481 }
482
483 /**
484 * Checks whether an offset exists in the iterator.
485 *
486 * @param mixed $offset The object offset.
487 *
488 * @return boolean True if the object exists, false otherwise.
489 *
490 * @since 1.0
491 */
492 public function offsetExists($offset)
493 {
494 return isset($this->objects[$offset]);
495 }
496
497 /**
498 * Gets an offset in the iterator.
499 *
500 * @param mixed $offset The object offset.
501 *
502 * @return DataObject The object if it exists, null otherwise.
503 *
504 * @since 1.0
505 */
506 public function offsetGet($offset)
507 {
508 return isset($this->objects[$offset]) ? $this->objects[$offset] : null;
509 }
510
511 /**
512 * Sets an offset in the iterator.
513 *
514 * @param mixed $offset The object offset.
515 * @param DataObject $object The object object.
516 *
517 * @return void
518 *
519 * @since 1.0
520 * @throws \InvalidArgumentException if an object is not an instance of Data\Object.
521 */
522 public function offsetSet($offset, $object)
523 {
524 if (!($object instanceof DataObject))
525 {
526 throw new \InvalidArgumentException(sprintf('%s("%s", *%s*)', __METHOD__, $offset, gettype($object)));
527 }
528
529 // Set the offset.
530 $this->objects[$offset] = $object;
531 }
532
533 /**
534 * Unsets an offset in the iterator.
535 *
536 * @param mixed $offset The object offset.
537 *
538 * @return void
539 *
540 * @since 1.0
541 */
542 public function offsetUnset($offset)
543 {
544 if (!$this->offsetExists($offset))
545 {
546 // Do nothing if the offset does not exist.
547 return;
548 }
549
550 // Check for special handling of unsetting the current position.
551 if ($offset == $this->current)
552 {
553 // Get the current position.
554 $keys = $this->keys();
555 $position = array_search($this->current, $keys);
556
557 // Check if there is an object before the current object.
558 if ($position > 0)
559 {
560 // Move the current position back one.
561 $this->current = $keys[$position - 1];
562 }
563 else
564 {
565 // We are at the start of the keys AND let's assume we are in a foreach loop and `next` is going to be called.
566 $this->current = false;
567 }
568 }
569
570 unset($this->objects[$offset]);
571 }
572
573 /**
574 * Rewinds the iterator to the first object.
575 *
576 * @return void
577 *
578 * @since 1.0
579 */
580 public function rewind()
581 {
582 // Set the current position to the first object.
583 if (empty($this->objects))
584 {
585 $this->current = false;
586 }
587 else
588 {
589 $keys = $this->keys();
590 $this->current = array_shift($keys);
591 }
592 }
593
594 /**
595 * Validates the iterator.
596 *
597 * @return boolean True if valid, false otherwise.
598 *
599 * @since 1.0
600 */
601 public function valid()
602 {
603 // Check the current position.
604 if (!is_scalar($this->current) || !isset($this->objects[$this->current]))
605 {
606 return false;
607 }
608
609 return true;
610 }
611
612 /**
613 * Initialises the list with an array of objects.
614 *
615 * @param array $input An array of objects.
616 *
617 * @return void
618 *
619 * @since 1.0
620 * @throws \InvalidArgumentException if an object is not an instance of Data\DataObject.
621 */
622 private function _initialise(array $input = array())
623 {
624 foreach ($input as $key => $object)
625 {
626 if (!is_null($object))
627 {
628 $this->offsetSet($key, $object);
629 }
630 }
631
632 $this->rewind();
633 }
634 }
635