π Collection
Note: While collection are part of the Hector ORM ecosystem, they are available as a standalone package:
hectororm/collection
.
You can find it on
Packagist.
You can use them independently of the ORM, in any PHP application. π
Working with arrays in PHP is simple, but when it comes to chaining operations like filtering, mapping, or sorting, code can quickly become verbose and harder to read. Hector ORM introduces a powerful abstraction: collections β iterable data structures that combine the convenience of arrays with the expressiveness of modern fluent interfaces.
Collections are especially useful for transforming data retrieved from APIs, databases, or user input. They encourage a functional and declarative approach to manipulating data, resulting in cleaner and more maintainable code.
Two types of collections are available:
-
Collection
: An eager collection that behaves much like a traditional PHP array. It evaluates and stores all its values in memory. -
LazyCollection
: A lazy, generator-based collection built for handling large or potentially infinite datasets efficiently. Its values are generated on the fly, which makes it ideal when working with streams or when you want to avoid unnecessary memory usage.
π Creating Collections
use Hector\Collection\Collection;
use Hector\Collection\LazyCollection;
$collection = new Collection();
$collection = new Collection(['my', 'initial', 'array']);
$lazy = new LazyCollection();
lazy = new LazyCollection(['my', 'initial', 'array']);
All collections implement CollectionInterface
. Only Collection
is Countable
.
π CollectionInterface
Methods
static::new(iterable|Closure $iterable): static
Create a new instance of the current collection.
$collection = Collection::new(['foo', 'bar']);
count(): int
Count the number of items in the collection.
For lazy collections, the entire iterator will be traversed.
$collection = Collection::new(['foo', 'bar', 'baz']);
$collection->count(); // 3
count($collection); // 3
getArrayCopy(): array
Return the collection as a plain array.
$collection = new Collection();
$collection->getArrayCopy(); // []
$collection = new Collection(['my', 'initial', new Collection(['array'])]);
$collection->getArrayCopy(); // ['my', 'initial', ['array']]
all(): array
Get all elements of the collection (preserves inner collections).
$collection = new Collection();
$collection->all(); // []
$collection = new Collection(['my', 'initial', new Collection(['array'])]);
$collection->all(); // ['my', 'initial', new Collection(['array'])]
isEmpty(): bool
Determine if the collection is empty.
Collection::new(['foo', 'bar'])->isEmpty(); // false
Collection::new()->isEmpty(); // true
isList(): bool
Check if the collection is a list (sequential integer keys).
Collection::new(['foo', 'bar'])->isList(); // true
Collection::new(['foo', 'b' => 'bar'])->isList(); // false
collect(): self
Collect all data into a new classic Collection
. Useful for evaluating a LazyCollection
.
$lazy = LazyCollection::new(['foo', 'bar']);
$collection = $lazy->collect();
sort(callable|int|null $callback = null): static
Sort items using optional callback or sorting flag.
$collection = Collection::new(['foo', 'bar']);
$collection = $collection->sort();
$collection->getArrayCopy(); // ['bar', 'foo']
multiSort(callable ...$callbacks): static
Perform multi-level sorting with multiple comparison callbacks.
$collection = Collection::new([
'l' => ['name' => 'Lemon', 'nb' => 1],
'o' => ['name' => 'Orange', 'nb' => 1],
'b1' => ['name' => 'Banana', 'nb' => 5],
'b2' => ['name' => 'Banana', 'nb' => 1],
'a1' => ['name' => 'Apple', 'nb' => 10],
'a2' => ['name' => 'Apple', 'nb' => 1],
]);
$collection = $collection->multiSort(
fn($a, $b) => $a['name'] <=> $b['name'],
fn($a, $b) => $a['nb'] <=> $b['nb']
);
$collection->getArrayCopy();
// [
// 'a2' => ['name' => 'Apple', 'nb' => 1],
// 'a1' => ['name' => 'Apple', 'nb' => 10],
// 'b2' => ['name' => 'Banana', 'nb' => 1],
// 'b1' => ['name' => 'Banana', 'nb' => 5],
// 'l' => ['name' => 'Lemon', 'nb' => 1],
// 'o' => ['name' => 'Orange', 'nb' => 1]
// ]
filter(?callable $callback = null): static
Filter items using a callback.
$collection = Collection::new([1, 10, 20, 100]);
$filtered = $collection->filter(fn($v) => $v >= 20);
$filtered->getArrayCopy(); // [20, 100]
filterInstanceOf(string|object ...$classes): static
Filter items that are instances of given class(es).
$collection = Collection::new([new stdClass(), new SimpleXMLElement('<root/>')]);
$collection = $collection->filterInstanceOf(stdClass::class);
$collection->getArrayCopy(); // [object(stdClass)]
map(callable $callback): static
Map callback to each item.
each(callable $callback): static
Apply callback to each item and return original collection.
search(callable|mixed $needle, bool $strict = false): int|string|false
Search for a value or use callback to find matching item.
$collection = Collection::new(['foo', 'bar', '1', 1, 'quxx']);
$collection->search(1); // 2
$collection->search(1, true); // 3
$collection->search(fn($v) => str_starts_with($v, 'ba')); // 1
get(int $index = 0): mixed
Get item at given index (negative indexes allowed).
$collection = Collection::new(['foo', 'bar', 'baz']);
$collection->get(); // 'foo'
$collection->get(1); // 'bar'
$collection->get(-1); // 'baz'
first(?callable $callback = null): mixed
Return the first item (optionally filtered).
$collection = Collection::new(['foo', 'bar', 'baz']);
$collection->first(); // 'foo'
$collection->first(fn($v) => str_starts_with($v, 'ba')); // 'bar'
last(?callable $callback = null): mixed
Return the last item (optionally filtered).
$collection->last(); // 'baz'
$collection->last(fn($v) => str_starts_with($v, 'ba')); // 'baz'
slice(int $offset, ?int $length = null): static
Get a slice of the collection.
$collection = Collection::new(['foo', 'bar', 'baz']);
$collection->slice(0, 2)->getArrayCopy(); // ['foo', 'bar']
$collection->slice(-2, 2)->getArrayCopy(); // ['bar', 'baz']
contains(mixed $value, bool $strict = false): bool
Check if collection contains value.
$collection = Collection::new(['foo', 'bar', '2', 'baz']);
$collection->contains(2); // true
$collection->contains(2, true); // false
chunk(int $length): static
Split collection into chunks.
$collection = Collection::new(['foo', 'bar', 'baz']);
$chunks = $collection->chunk(2)->getArrayCopy();
// [['foo', 'bar'], ['baz']]
keys(): static
Return all keys.
values(): static
Return all values.
unique(): static
Remove duplicate values.
$collection = Collection::new(['k1' => 'foo', 1 => 'foo', 'bar', 'k2' => 'baz']);
$collection->unique()->getArrayCopy(); // ['k1' => 'foo', 'bar', 'k2' => 'baz']
flip(): static
Flip keys and values.
reverse(bool $preserve_keys = false): static
Reverse the order of items.
$collection = Collection::new(['k1' => 'foo', 'foo', 'bar', 'k2' => 'baz']);
$collection->reverse()->getArrayCopy();
$collection->reverse(true)->getArrayCopy();
column(string|int|Closure|null $column_key, string|int|Closure|null $index_key = null): static
Extract a column or reindex collection.
$collection = Collection::new([
['k1' => 'foo', 'value' => 'foo value'],
['k1' => 'bar', 'value' => 'bar value'],
['k1' => 'baz', 'value' => 'baz value'],
]);
$collection->column('k1')->getArrayCopy();
$collection->column('value', 'k1')->getArrayCopy();
rand(int $length = 1): static
Pick one or more random items.
sum(): int|float
Sum of values.
avg(): int|float
Average of values.
median(): int|float
Median of values.
variance(): int|float
Population variance.
deviation(): int|float
Standard deviation.
reduce(callable $callback, mixed $initial = null): mixed
Reduce to a single value.
Collection::new([1, 2, 3])->reduce(fn($c, $i) => $c + $i, 10); // 16
π Collection
Additional Methods
append(mixed ...$values): static
Append values to collection.
Collection::new(['foo'])->append('bar')->getArrayCopy(); // ['foo', 'bar']
prepend(mixed ...$values): static
Prepend values to collection.
Collection::new(['foo'])->prepend('bar')->getArrayCopy(); // ['bar', 'foo']
lazy(): LazyCollection
Convert to lazy collection.
π§΅ LazyCollection
LazyCollection
uses PHP generators to stream data efficiently, deferring evaluation until itβs needed. Lazy
collections are ideal when working with large datasets or expensive operations.
β οΈ Once a lazy collection is consumed (e.g., after a
foreach
,toArray()
orcount()
), it cannot be reused. To reuse its content, convert it to a standard collection usingcollect()
.
Create a LazyCollection
use Hector\Collection\LazyCollection;
$lazy = new LazyCollection(function () {
for ($i = 1; $i <= 5; $i++) {
yield $i;
}
});
Apply lazy operations
$filtered = $lazy->filter(fn($x) => $x % 2 === 0);
$mapped = $filtered->map(fn($x) => $x ** 2);
foreach ($mapped as $val) {
echo $val . PHP_EOL; // 4, 16
}
Convert between lazy and standard collections
$standard = $mapped->collect(); // => Hector\Collection\Collection
$lazyAgain = $standard->lazy(); // => LazyCollection
π‘ Common Use Cases
Lazy collections are especially useful for:
-
Large datasets: Reading a huge file (e.g., CSV, logs) without loading everything into memory.
$lazyCsv = new LazyCollection(function () { $handle = fopen('data.csv', 'r'); while (($row = fgetcsv($handle)) !== false) { yield $row; } fclose($handle); }); $names = $lazyCsv->map(fn($row) => $row[1]);
-
Streaming data from APIs:
$apiResults = new LazyCollection(function () { for ($page = 1; $page <= 100; $page++) { $data = fetchApiPage($page); foreach ($data as $item) { yield $item; } } });
-
Applying expensive transformations only when needed:
$users = new LazyCollection($dbRows); $emails = $users->filter(fn($u) => $u['active'])->map(fn($u) => strtolower($u['email']));
Limitations and Caveats
- A
LazyCollection
is not reusable once iterated. If you need to use the same data twice, callcollect()
first. - Side effects in callbacks (
map
,filter
, etc.) are only triggered when actually iterated. -
isEmpty()
,getArrayCopy()
,count()
will consume the generator. - Lazy collections are mostly read-only: you canβt push new elements after creation.
- Not all methods are optimized for laziness; some require full materialization internally (e.g.
reverse()
,unique()
).
π‘ Usage Examples
Filtering user input
use Hector\Collection\Collection;
$input = Collection::new($_POST);
$filtered = $input->filter(fn($value) => $value !== '');
Transforming API results
$response = Collection::new($api->fetchUsers());
$usernames = $response->map(fn($user) => $user['username']);
Sorting a list of products by price
$products = Collection::new($repository->all());
$sorted = $products->sort(fn($a, $b) => $a['price'] <=> $b['price']);
Extracting specific values
$orders = Collection::new($ordersData);
$orderIds = $orders->column('id');
Using LazyCollection for large data sets
use Hector\Collection\LazyCollection;
$lazy = new LazyCollection(function () {
foreach (range(1, 1000000) as $i) {
yield $i;
}
});
$even = $lazy->filter(fn($n) => $n % 2 === 0)->collect();
Aggregating numerical data
Suppose you have a list of invoices and want to compute the total and average amount:
$invoices = Collection::new($invoicingService->all());
$total = $invoices->column('amount')->sum();
$average = $invoices->column('amount')->avg();