Collection
Most data in an application is nested. An API response contains an order, the order contains a customer and a list of items, and each item contains its own fields. Collection wraps such an array so that you can read, change, and copy any part of it with a path string such as order.customer.name. The same object also gives you a way to load data from a string or a file, and to convert it back into a format.
Collection extends the ArrayObject class of windwalker/scalars, so it also has every array helper of that class, such as column(), map(), filter(), and sum(). The methods described here add the deep-path and loading features. The inherited methods are listed in Inherited Methods at the end of this page.
Collection Methods
The table lists the methods that Collection declares itself. Each row links to the section of this page that explains it. The methods it inherits from ArrayObject are listed in Inherited Methods.
| Method | Returns | Description |
|---|---|---|
from() | Static | Creates a collection from data and an optional format. |
extract() | Static | Returns a copy of a sub-tree, or a proxy when $reference is true. |
proxy() | Static | Returns a proxy to a sub-tree; same as extract($path, true). |
isProxy() | bool | Whether the object is a proxy to a parent collection. |
mapProxy() | MapProxy | Applies a change to each item, by column or by method. |
getDeep() | Mixed | Reads a value by path; returns by reference. |
setDeep() | Static | Writes a value by path; changes the object. |
hasDeep() | bool | Whether a path exists. |
removeDeep() | Static | Removes a value by path; changes the object. |
withDeep() | Static | Copy with a value set at a path. |
withRemoveDeep() | Static | Copy without the value at a path. |
load() | Static | Merges data from an array or string into the object. |
loadFile() | Static | Merges data from a file into the object. |
withLoad() | Static | Copy with data merged in. |
toString() | string | Dumps the data in the given format; JSON by default. |
toStringObject() | StringObject | Same as toString(), returned as a StringObject. |
toJson(), toXml(), and other to*() | string | Shortcuts for toString() with a fixed format. |
getFormatRegistry() | FormatRegistry | Returns the registry this object uses. |
setFormatRegistry() | Static | Sets a shared registry for this object. |
__toString() | string | Same as toString() with the default format. |
Creating a Collection
Use the constructor for an array, collect() as a short helper, or Collection::from() for a string or a file:
use Windwalker\Data\Collection;
use function Windwalker\collect;
$a = new Collection(['flower' => 'sakura']);
$b = collect(['flower' => 'sakura']);
$c = Collection::from('{"flower":"sakura"}', 'json');Collection::from() returns the same instance when you pass a Collection into it. It does not copy the data.
Showcase
The following example reads a fictional order response, calculates a value from its items, and creates a changed copy. The original object is not changed by the last step:
use Windwalker\Data\Collection;
$json = <<<JSON
{
"order": {
"id": 1001,
"customer": {"name": "Rose", "tier": "gold"},
"items": [
{"sku": "SAK-01", "qty": 2, "price": 12.5},
{"sku": "ROS-03", "qty": 1, "price": 8.0}
]
}
}
JSON;
$order = Collection::from($json, 'json');
echo $order->getDeep('order.customer.name'); // Rose
$units = $order->extract('order.items')->column('qty')->sum();
echo $units; // 3
$shipped = $order->withDeep('order.status', 'shipped');
echo $shipped->getDeep('order.status'); // shipped
echo $order->hasDeep('order.status') ? 'yes' : 'no'; // nogetDeep() and withDeep() both take the path as the first argument. The path is split by . unless you give another delimiter.
Deep Paths
The deep-path methods let you address nested values by a single string:
getDeep()reads a value and returnsnullwhen the path is missing.hasDeep()checks whether a path exists.setDeep()writes a value. It creates the missing intermediate arrays, and it changes the current object.removeDeep()deletes a value from the current object.
Each of these methods accepts a delimiter as the last argument, so a path can use another separator:
$config = collect(['database' => ['host' => 'localhost']]);
echo $config->getDeep('database/host', '/'); // localhost
$config->setDeep('cache/driver', 'redis', '/');
echo $config->getDeep('cache/driver', '/'); // redisTIP
Use a delimiter other than . only when the keys themselves contain dots, such as hostnames or file names used as keys.
Immutable Copies
Each mutating method has a with variant that works on a copy: withDeep() and withRemoveDeep(). The with() method from the array base class follows the same rule for a top-level key. Use these methods when the object is shared, such as a base configuration that several services read:
$base = collect(['debug' => false, 'name' => 'app']);
$local = $base->withDeep('debug', true);
var_dump($base->getDeep('debug')); // false
var_dump($local->getDeep('debug')); // trueLoading Data
load() reads a string or an array into the current object. loadFile() reads a file, and withLoad() does the same on a copy. The format is detected from the file extension, or you pass it explicitly. See Formats for the supported formats.
$settings = new Collection();
$settings->loadFile(__DIR__ . '/settings.json');
$settings->load('{"theme":"dark"}', 'json');WARNING
load() merges the new data into the existing data with a recursive merge. A list in the new data is appended to the existing list, not used to replace it. The merge succeeds without an error, but the result is often not what you expected:
$config = collect(['tags' => ['red']]);
$config->load(['tags' => ['blue']]);
// ['tags' => ['red', 'blue']]To replace a list, use setDeep() instead:
$config->setDeep('tags', ['blue']);Proxies
A proxy is a Collection that writes its changes back into the parent object. There are two ways to get a sub-tree:
extract($path)returns a copy. Changes to the copy do not affect the parent.extract($path, true), or its aliasproxy($path), returns a view. Changes to the view are written into the parent.
$order = Collection::from($json, 'json');
$items = $order->proxy('order.items');
$items->push(['sku' => 'LIL-02', 'qty' => 1, 'price' => 4.0]);
echo count($order->getDeep('order.items')); // 3INFO
When the path does not exist, a proxy creates an empty array at that path in the parent. The parent gains a key even though you only asked to read it. A proxy also requires the target to be an array. Otherwise, the call throws an exception.
Mapping a Column
mapProxy() applies a change to every item of a list. It returns a new collection and leaves the original unchanged.
Pass a column name to change one field of each item:
$order = Collection::from($json, 'json');
$items = $order->extract('order.items');
$doubled = $items->mapProxy('qty')->map(
fn(int $qty): int => $qty * 2
);
echo $doubled->column('qty')->sum(); // 6
echo $items->column('qty')->sum(); // 3The method called after mapProxy('qty') must return a Collection, for example map() or filter(). Calling a method such as sum() in this mode throws an InvalidArgumentException.
When you omit the column name, the proxy calls a method on each item. This works for list items that are objects:
$labels = collect([new Label('one'), new Label('two')]);
$labels->mapProxy()->text(); // ['one!', 'two!']Converting Back to a String
toString() dumps the data with the given format. It uses JSON when you do not pass a format. toStringObject() returns a StringObject from windwalker/scalars, and the to*() shortcuts, such as toYaml() and toXml(), call toString() with the matching format:
$order = Collection::from($json, 'json');
echo $order->toString('json');
echo $order->toXml();Format-specific options and the dependencies they need are described on the Formats page.
Inherited Methods
Collection inherits the public methods of ArrayObject from windwalker/scalars. Each method below links to its explanation in the Array Object page of the Scalars component. Methods that return a new object leave the original unchanged, and methods marked there as in-place change the object.
Creating and Wrapping
create()explode()range()fillWith()fillKeys()wrap()tryWrap()unwrap()wrapAll()dump()as()mapAs()reset()
Immutable Updates
Changing the Object in Place
Reading Values
first()last()findFirst()search()indexOf()contains()keyExists()keys()values()isEmpty()notEmpty()isNull()notNull()
Looping and Transforming
each()walk()walkRecursive()find()query()filter()reject()partition()map()mapRecursive()mapWithKeys()flatMap()reduce()apply()unique()implode()countValues()
Columns, Grouping, and Paging
Aggregation
Sorting
sort()sortDesc()sortKeys()sortKeysDesc()sortBy()natureSort()natureSortCaseInsensitive()natureSortKeys()shuffle()rand()reverse()
Combining Arrays
merge()mergeRecursive()replace()replaceRecursive()union()combine()diff()diffKeys()intersect()intersectKey()crossJoin()only()except()flip()
Adding and Padding Items
concat()concatStart()append()prepend()insertAfter()insertBefore()pad()leftPad()rightPad()removeFirst()removeLast()slice()
Pipelines and Macros
These helpers come from windwalker/utilities, and the Scalars page does not cover them:
pipe(),pipeIf(): pass the object through a callback and use its result.tap(),tapIf(): call a callback with the object and keep the object.macro(),hasMacro(),clearMarco(),do(): register and call custom methods for the class.
Array Access and Iteration
Collection implements ArrayAccess, IteratorAggregate, and Countable through the array base class. Use $collection['key'], foreach, and count() on any collection. Magic property access such as $collection->key works through __get() and __set().