1: <?php
2:
3: namespace Guzzle\Common;
4:
5: use Guzzle\Common\Exception\InvalidArgumentException;
6:
7: /**
8: * Key value pair collection object
9: */
10: class Collection implements \ArrayAccess, \IteratorAggregate, \Countable, ToArrayInterface
11: {
12: /**
13: * @var array Data associated with the object.
14: */
15: protected $data;
16:
17: /**
18: * Constructor
19: *
20: * @param array $data Associative array of data to set
21: */
22: public function __construct(array $data = null)
23: {
24: $this->data = $data ?: array();
25: }
26:
27: /**
28: * Create a new collection from an array, validate the keys, and add default values where missing
29: *
30: * @param array $config Configuration values to apply.
31: * @param array $defaults Default parameters
32: * @param array $required Required parameter names
33: *
34: * @return self
35: * @throws InvalidArgumentException if a parameter is missing
36: */
37: public static function fromConfig(array $config = null, array $defaults = null, array $required = null)
38: {
39: $collection = new self($defaults);
40:
41: foreach ((array) $config as $key => $value) {
42: $collection->set($key, $value);
43: }
44:
45: foreach ((array) $required as $key) {
46: if ($collection->hasKey($key) === false) {
47: throw new InvalidArgumentException("Config must contain a '{$key}' key");
48: }
49: }
50:
51: return $collection;
52: }
53:
54: /**
55: * Add a value to a key. If a key of the same name has already been added, the key value will be converted into an
56: * array and the new value will be pushed to the end of the array.
57: *
58: * @param string $key Key to add
59: * @param mixed $value Value to add to the key
60: *
61: * @return Collection Returns a reference to the object.
62: */
63: public function add($key, $value)
64: {
65: if (!array_key_exists($key, $this->data)) {
66: $this->data[$key] = $value;
67: } elseif (is_array($this->data[$key])) {
68: $this->data[$key][] = $value;
69: } else {
70: $this->data[$key] = array($this->data[$key], $value);
71: }
72:
73: return $this;
74: }
75:
76: /**
77: * Removes all key value pairs
78: *
79: * @return Collection
80: */
81: public function clear()
82: {
83: $this->data = array();
84:
85: return $this;
86: }
87:
88: /**
89: * Return the number of keys
90: *
91: * @return integer
92: */
93: public function count()
94: {
95: return count($this->data);
96: }
97:
98: /**
99: * Iterates over each key value pair in the collection passing them to the Closure. If the Closure function returns
100: * true, the current value from input is returned into the result Collection. The Closure must accept three
101: * parameters: (string) $key, (string) $value and return Boolean TRUE or FALSE for each value.
102: *
103: * @param \Closure $closure Closure evaluation function
104: * @param bool $static Set to TRUE to use the same class as the return rather than returning a Collection
105: *
106: * @return Collection
107: */
108: public function filter(\Closure $closure, $static = true)
109: {
110: $collection = ($static) ? new static() : new self();
111: foreach ($this->data as $key => $value) {
112: if ($closure($key, $value)) {
113: $collection->add($key, $value);
114: }
115: }
116:
117: return $collection;
118: }
119:
120: /**
121: * Get an iterator object
122: *
123: * @return array
124: */
125: public function getIterator()
126: {
127: return new \ArrayIterator($this->data);
128: }
129:
130: /**
131: * Get a specific key value.
132: *
133: * @param string $key Key to retrieve.
134: *
135: * @return mixed|null Value of the key or NULL
136: */
137: public function get($key)
138: {
139: return isset($this->data[$key]) ? $this->data[$key] : null;
140: }
141:
142: /**
143: * Get all or a subset of matching key value pairs
144: *
145: * @param array $keys Pass an array of keys to retrieve only a subset of key value pairs
146: *
147: * @return array Returns an array of all matching key value pairs
148: */
149: public function getAll(array $keys = null)
150: {
151: return $keys ? array_intersect_key($this->data, array_flip($keys)) : $this->data;
152: }
153:
154: /**
155: * {@inheritdoc}
156: */
157: public function toArray()
158: {
159: return $this->data;
160: }
161:
162: /**
163: * Get all keys in the collection
164: *
165: * @return array
166: */
167: public function getKeys()
168: {
169: return array_keys($this->data);
170: }
171:
172: /**
173: * Returns whether or not the specified key is present.
174: *
175: * @param string $key The key for which to check the existence.
176: *
177: * @return bool
178: */
179: public function hasKey($key)
180: {
181: return array_key_exists($key, $this->data);
182: }
183:
184: /**
185: * Case insensitive search the keys in the collection
186: *
187: * @param string $key Key to search for
188: *
189: * @return bool|string Returns false if not found, otherwise returns the key
190: */
191: public function keySearch($key)
192: {
193: foreach (array_keys($this->data) as $k) {
194: if (!strcasecmp($k, $key)) {
195: return $k;
196: }
197: }
198:
199: return false;
200: }
201:
202: /**
203: * Checks if any keys contains a certain value
204: *
205: * @param string $value Value to search for
206: *
207: * @return mixed Returns the key if the value was found FALSE if the value was not found.
208: */
209: public function hasValue($value)
210: {
211: return array_search($value, $this->data);
212: }
213:
214: /**
215: * Returns a Collection containing all the elements of the collection after applying the callback function to each
216: * one. The Closure should accept three parameters: (string) $key, (string) $value, (array) $context and return a
217: * modified value
218: *
219: * @param \Closure $closure Closure to apply
220: * @param array $context Context to pass to the closure
221: * @param bool $static Set to TRUE to use the same class as the return rather than returning a Collection
222: *
223: * @return Collection
224: */
225: public function map(\Closure $closure, array $context = array(), $static = true)
226: {
227: $collection = $static ? new static() : new self();
228: foreach ($this as $key => $value) {
229: $collection->add($key, $closure($key, $value, $context));
230: }
231:
232: return $collection;
233: }
234:
235: /**
236: * Add and merge in a Collection or array of key value pair data.
237: *
238: * @param Collection|array $data Associative array of key value pair data
239: *
240: * @return Collection Returns a reference to the object.
241: */
242: public function merge($data)
243: {
244: if ($data instanceof self) {
245: $data = $data->getAll();
246: } elseif (!is_array($data)) {
247: return $this;
248: }
249:
250: if (empty($this->data)) {
251: $this->data = $data;
252: } else {
253: foreach ($data as $key => $value) {
254: $this->add($key, $value);
255: }
256: }
257:
258: return $this;
259: }
260:
261: /**
262: * ArrayAccess implementation of offsetExists()
263: *
264: * @param string $offset Array key
265: *
266: * @return bool
267: */
268: public function offsetExists($offset)
269: {
270: return $this->hasKey($offset) !== false;
271: }
272:
273: /**
274: * ArrayAccess implementation of offsetGet()
275: *
276: * @param string $offset Array key
277: *
278: * @return null|mixed
279: */
280: public function offsetGet($offset)
281: {
282: return $this->get($offset);
283: }
284:
285: /**
286: * ArrayAccess implementation of offsetGet()
287: *
288: * @param string $offset Array key
289: * @param mixed $value Value to set
290: */
291: public function offsetSet($offset, $value)
292: {
293: $this->set($offset, $value);
294: }
295:
296: /**
297: * ArrayAccess implementation of offsetUnset()
298: *
299: * @param string $offset Array key
300: */
301: public function offsetUnset($offset)
302: {
303: $this->remove($offset);
304: }
305:
306: /**
307: * Remove a specific key value pair
308: *
309: * @param string $key A key to remove
310: *
311: * @return Collection
312: */
313: public function remove($key)
314: {
315: unset($this->data[$key]);
316:
317: return $this;
318: }
319:
320: /**
321: * Replace the data of the object with the value of an array
322: *
323: * @param array $data Associative array of data
324: *
325: * @return Collection Returns a reference to the object
326: */
327: public function replace(array $data)
328: {
329: $this->data = $data;
330:
331: return $this;
332: }
333:
334: /**
335: * Set a key value pair
336: *
337: * @param string $key Key to set
338: * @param mixed $value Value to set
339: *
340: * @return Collection Returns a reference to the object
341: */
342: public function set($key, $value)
343: {
344: $this->data[$key] = $value;
345:
346: return $this;
347: }
348:
349: /**
350: * Inject configuration settings into an input string
351: *
352: * @param string $input Input to inject
353: *
354: * @return string
355: */
356: public function inject($input)
357: {
358: $replace = array();
359: foreach ($this->data as $key => $val) {
360: $replace['{' . $key . '}'] = $val;
361: }
362:
363: return strtr($input, $replace);
364: }
365:
366: /**
367: * Gets a value from the collection using an array path (e.g. foo/baz/bar would retrieve bar from two nested arrays)
368: * Allows for wildcard searches which recursively combine matches up to the level at which the wildcard occurs. This
369: * can be useful for accepting any key of a sub-array and combining matching keys from each diverging path.
370: *
371: * @param string $path Path to traverse and retrieve a value from
372: * @param string $separator Character used to add depth to the search
373: * @param mixed $data Optional data to descend into (used when wildcards are encountered)
374: *
375: * @return mixed|null
376: */
377: public function getPath($path, $separator = '/', $data = null)
378: {
379: // Assume the data of the collection if no data was passed into the method
380: if ($data === null) {
381: $data = &$this->data;
382: }
383:
384: // Break the path into an array if needed
385: if (!is_array($path)) {
386: $path = explode($separator, $path);
387: }
388:
389: // Using an iterative approach rather than recursion for speed
390: while (null !== ($part = array_shift($path))) {
391:
392: if (!is_array($data)) {
393: return null;
394: }
395:
396: // The value does not exist in the array or the path has more but the value is not an array
397: if (!isset($data[$part])) {
398:
399: // Not using a wildcard and the key was not found, so return null
400: if ($part != '*') {
401: return null;
402: }
403:
404: // If using a wildcard search, then diverge and combine paths
405: $result = array();
406: foreach ($data as $value) {
407: if (!$path) {
408: $result = array_merge_recursive($result, (array) $value);
409: } else {
410: $test = $this->getPath($path, $separator, $value);
411: if ($test !== null) {
412: $result = array_merge_recursive($result, (array) $test);
413: }
414: }
415: }
416:
417: return $result;
418: }
419:
420: // Descend deeper into the data
421: $data = &$data[$part];
422: }
423:
424: return $data;
425: }
426:
427: /**
428: * Over write key value pairs in this collection with all of the data from an array or collection.
429: *
430: * @param array|\Traversable $data Values to override over this config
431: *
432: * @return self
433: */
434: public function overwriteWith($data)
435: {
436: foreach ($data as $k => $v) {
437: $this->set($k, $v);
438: }
439:
440: return $this;
441: }
442: }
443: