xi/collections
xi/collections extends Laravel Collections with handy, chainable macros and helpers for cleaner, more expressive array and data manipulation. Lightweight and easy to drop into any Laravel or PHP project.
Install via Composer and confirm the PHP constraint from the package's composer.json (it targets PHP 5.3):
composer require xi/collections
The codebase predates PHP 7/8, so before adopting, run the package's own test suite on your target runtime:
vendor/bin/phpunit vendor/xi/collections/tests
Where to look first
src/Xi/Collections/Collection/ArrayCollection.php — the canonical implementation and the best source of truth for the available transform methods and their callback signatures.src/Xi/Collections/Enumerable.php — the contract every collection/map/filter/reduce pipeline honors; read this to know what you can safely program against.src/Xi/Collections/Enumeration.php — the base class for type-safe enums (get(), isValid(), equals(), reflection over class constants).tests/ — the README is thin; the tests are the executable spec for edge cases like key preservation and comparator semantics.First use case: wrapping a repository result set
The most natural day-one use is turning an array returned by a repository or query into a pipeline:
use Xi\Collections\Collection\ArrayCollection;
$users = new ArrayCollection($repository->findActiveUsers());
$emails = $users
->filter(function ($user) { return $user->hasEmail(); })
->map(function ($user) { return $user->getEmail(); })
->toArray();
First use case: replacing loose string constants
use Xi\Collections\Enumeration;
class OrderStatus extends Enumeration
{
const PENDING = 'pending';
const PAID = 'paid';
const SHIPPED = 'shipped';
}
$status = OrderStatus::get($row['status']); // throws on invalid value
if ($status->equals(OrderStatus::get(OrderStatus::PAID))) {
// ...
}
Pipeline style for read models
Compose filter → map → reduce and only call toArray() at the boundary (controller, view, JSON encoder). Keeping the intermediate values as collections lets you add steps later without rewriting array plumbing:
$total = (new ArrayCollection($lineItems))
->filter(function ($item) { return !$item->isDiscounted(); })
->reduce(function ($carry, $item) {
return $carry + $item->getPrice();
}, 0);
Domain collections via subclassing
The main extension point. Subclass ArrayCollection (or implement Enumerable) to attach named, tested operations to a specific type instead of scattering closures across the app:
class UserCollection extends ArrayCollection
{
public function emails()
{
return $this->map(function (User $user) { return $user->getEmail(); });
}
public function byId()
{
return $this->indexBy(function (User $user) { return $user->getId(); });
}
}
Have repositories return UserCollection rather than array — callers then get emails()/byId() for free and the collection becomes the natural place for query-result shaping.
Side effects with each
Use each() for dispatch loops (sending mail, writing logs) where you want the result discarded but the fluent style kept. Keep map/filter pure; mixing side effects into them makes pipelines hard to reason about and to test.
Enum hydration at the boundary
Parse raw input into enum instances once — in form types, request mappers, or Doctrine hydrators — and pass the Enumeration instance through the domain. That way equals() is the only comparison you need and isValid() becomes dead code in the middle of the app:
// Form/request boundary
if (!OrderStatus::isValid($input['status'])) {
throw new InvalidArgumentException('Unknown status');
}
$status = OrderStatus::get($input['status']);
Whole-collection transforms
For operations that can't be expressed per-element (sorting, reversing, re-indexing), look for the collection-wide apply/transform method on ArrayCollection and override it in subclasses if you need domain-specific ordering. Prefer a named subclass method (->sortedByDate()) over an inline closure so the order is documented and reused.
Adapters at framework boundaries
Treat toArray() as an explicit serialization step. If a library expects a plain PHP array (templating, json_encode, legacy service), convert once at the edge — don't leak the collection object into APIs that will foreach it and silently rely on IteratorAggregate semantics.
Immutability is the #1 source of "why is my collection empty?"
Every map/filter returns a new instance. $collection->map($fn); without assignment is a no-op. This is the most common bug in this style of library; enable a static analyser or a strict code review rule that mutating calls must be assigned.
Keys after filter
Underlying array functions like array_filter preserve keys, so a filtered collection will often produce a sparse toArray():
// [0 => 'a', 2 => 'c'] — not ['a', 'c']
$filtered = (new ArrayCollection(['a', 'b', 'c']))
->filter(function ($v) { return $v !== 'b'; })
->toArray();
Call array_values() (or the collection's re-index method, if present in your version) before json_encode, otherwise you get a JSON object instead of an array.
Verify callback signatures against the source
map/filter/each may pass value-only or value+key depending on the method and the version you pinned. Read ArrayCollection once, then write callbacks to match; an extra ignored argument is harmless, a missing one is a fatal error.
Eager evaluation and memory
These are eager collections, not lazy/generator-backed. A 100k-row result set will allocate the intermediate arrays at every pipeline stage. For large datasets, keep the array_*-level pagination in the repository and only wrap the final page.
PHP 5.3 closure semantics
Closures in 5.3 do not bind $this, so callbacks can't call $this->helper() — capture dependencies explicitly with use ($service) or make callbacks static. Also, scalar type hints and return types are absent; rely on docblocks and subclass-return-type declarations to keep IDE assistance.
Enumeration pitfalls
get() ambiguous. Add a test that asserts count(getConstants()) equals the count of unique values.get() typically throws on unknown values — decide whether your boundary wants that (validation) or a nullable variant, and wrap accordingly.myclabs/php-enum or native enum map over cleanly since the API is small (get, isValid, equals).PHP 8 compatibility risk
This package is from the PHP 5.3 era. Expect deprecations (implicit nullable parameters, dynamic properties, each()/create_function in transitive test code). Pin the exact version in composer.lock, run the package tests under your runtime, and be prepared to fork — with 26 stars and zero dependents, you are effectively the maintainer of your vendored copy.
Sorting
Comparator-based sorting inherits PHP's instability before 8.0. If ordering matters for output, add a tiebreaker to your comparator rather than relying on insertion order.
Debugging
var_dump($collection->toArray()) at each pipeline stage is faster than stepping through closures. When behavior surprises you, look in tests/ for the exact method — the test cases document intended semantics better than the source comments.
Extension points summary
ArrayCollection for typed/domain collections.Enumerable if you need a non-array-backed collection.Enumeration with static helper methods (e.g., OrderStatus::openStatuses() returning a collection) rather than switch statements at call sites.Strategic note
Given the low adoption and age, treat xi/collections as a small, well-understood utility you can reimplement in an afternoon if needed. On modern PHP, illuminate/collections covers the collection pipeline, and native enum (8.1+) covers the enumeration role — so keep your usage confined to the patterns above (pipelines, domain subclasses, boundary hydration) to make a future migration mechanical.
How can I help you explore Laravel packages today?