Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Collections Laravel Package

doctrine/collections

Doctrine Collections is a PHP library providing powerful collection abstractions, including ArrayCollection, typed collections, Criteria filtering, expression-based matching, selectors, and iterators for flexible data manipulation.

View on GitHub
Deep Wiki
Context7

Getting Started

doctrine/collections is almost certainly already in your vendor/ directory — it arrives transitively with doctrine/dbal, doctrine/orm, laravel-doctrine, and a long tail of packages that need a typed collection or Criteria. Verify before doing anything:

composer why doctrine/collections
composer show doctrine/collections

Check the version constraint first. The 3.x line requires PHP 8.4 and makes every class final and readonly. Most Laravel apps on PHP 8.1–8.3 will resolve to 2.x. Don't add "doctrine/collections": "^3.0" to composer.json unless your runtime is 8.4+ and nothing else pins you down.

Where to look first, in order:

  1. Doctrine\Common\Collections\ArrayCollection — the only class you'll instantiate directly 95% of the time.
  2. Doctrine\Common\Collections\Criteria + Doctrine\Common\Collections\ExpressionBuilder — the filtering DSL.
  3. ReadableCollection / Selectable — the interfaces to type-hint against (so you accept both ArrayCollection and lazy/persistent collections).

First use case — replace a foreach filter with a reusable Criteria:

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;

$orders = new ArrayCollection($orders); // or $customer->getOrders()

$criteria = Criteria::create()
    ->where(Criteria::expr()->eq('status', 'paid'))
    ->andWhere(Criteria::expr()->gte('total', 100_00))
    ->orderBy(['createdAt' => Criteria::DESC])
    ->setMaxResults(20);

$recent = $orders->matching($criteria);

The payoff is that the same Criteria object can be handed to a Doctrine repository ($repo->matching($criteria)) and executed as SQL. That portability is the main reason to reach for this package inside a Laravel app rather than collect().

Implementation Patterns

One Criteria, two execution engines. Build filter objects on Criteria in a service or form request, then use them in-memory in unit tests and against the DB in production:

final class PaidOrders
{
    public static function criteria(int $minCents): Criteria
    {
        return Criteria::create()
            ->where(Criteria::expr()->eq('status', 'paid'))
            ->andWhere(Criteria::expr()->gte('total', $minCents))
            ->orderBy(['createdAt' => Criteria::DESC]);
    }
}

// repository (SQL)
$orders = $this->em->getRepository(Order::class)->matching(PaidOrders::criteria(100_00));

// tests / in-memory (array walk)
$orders = (new ArrayCollection($fixtures))->matching(PaidOrders::criteria(100_00));

Doctrine entity relations. Any OneToMany/ManyToMany property must be typed Collection and initialized in the constructor. This is the surface you'll touch daily even if you never write a Criteria:

/**
 * @var Collection<int, OrderLine>
 */
#[OneToMany(targetEntity: OrderLine::class, mappedBy: 'order')]
private Collection $lines;

public function __construct()
{
    $this->lines = new ArrayCollection();
}

public function addLine(OrderLine $line): void
{
    if (!$this->lines->contains($line)) {
        $this->lines->add($line);
    }
}

Always add the Collection<int, X> docblock — Larastan/PHPStan resolve it, and without it every ->first() is mixed.

Composition, not inheritance. In 3.x ArrayCollection is final, so a domain collection must wrap it rather than extend it. Do this from day one even on 2.x so the upgrade is a no-op:

final class OrderLines implements ReadableCollection
{
    /** @param ArrayCollection<int, OrderLine> $lines */
    public function __construct(private readonly ArrayCollection $lines) {}

    public function total(): int { /* domain logic */ }

    public function matching(Criteria $c): Collection { return $this->lines->matching($c); }
    // ...delegate the rest
}

Bridging to Laravel collections. Doctrine collections are deliberately thin. For anything beyond map/filter, convert:

$lines = collect($order->getLines()->toArray());
$invoices = $lines->groupBy('taxRate')->map(fn ($g) => $g->sum('amount'));

Convert back when writing to an entity: new ArrayCollection($array). Don't try to pass an Illuminate\Support\Collection into a Doctrine setter.

Lazy collections for expensive hydration. Extend AbstractLazyCollection when the backing data is costly (a report query, an API call) and you want the collection to look eager:

final class LazyReportRows extends AbstractLazyCollection
{
    protected function doInitialize(): void
    {
        $this->collection = new ArrayCollection($this->fetchRows());
    }
}

isInitialized() is Psalm-asserted, so if (!$c->isInitialized()) { ... } narrows types properly.

ExpressionBuilder for composable filters. andX() / orX() are variadic (since 1.6.4) and take arrays or expressions, which makes dynamic filter building readable:

$expr = new ExpressionBuilder();

$where = $expr->andX(
    $expr->in('status', $statuses),
    $expr->isNotNull('paidAt'),
    $expr->orX(
        $expr->gte('total', $min),
        $expr->startsWith('reference', 'VIP-'),
    ),
);

$collection->matching(Criteria::create()->where($where));

CompositeExpression also supports a NOT type (2.1+), useful for exclusion filters: new CompositeExpression(CompositeExpression::TYPE_NOT, $inner).

Custom matching with closures. ArrayCollection::matching() resolves a closure used as a comparison value against the field value, which lets you express predicates that have no SQL analogue while keeping the Criteria shape:

$expr->eq('shippedAt', static fn ($value) => $value?->isWeekend() === false);

Keep these out of Criteria that you also hand to a repository — SQL won't understand them.

Use it as a test double. When a service depends on ReadableCollection, inject a real ArrayCollection in tests. You get real filtering semantics without a database, and you exercise the same Criteria your production code builds.

Gotchas and Tips

  • add() returns void since 2.0. Code like if ($collection->add($x)) is dead or broken. Same for any old 1.x return true pattern.
  • firstResult/maxResults preserve keys. matching() does not reindex. toArray() then serializes as a JSON object, not an array. Wrap in array_values() before returning from a controller or resource.
  • Scalar comparisons are strict. Since 1.6.7/2.x, in/contains on arrays of scalars use strict comparison — '1' will not match 1. Cast IDs coming out of HTTP requests, or you get silently empty results.
  • matching() on ArrayCollection never validates field names. A typo in 'satuts' yields an empty collection; the same Criteria against a repository throws a DB error. Never conclude a Criteria is correct because an in-memory test passed — add one integration test that runs it through the ORM.
  • Field resolution uses getters, not properties. eq('total', …) resolves to getTotal(), and since 1.7 a bare total() is also accepted. Embeddables work with dot notation ('address.city', 1.7+); arbitrary nested arrays do not.
  • Order enum is deprecated as of 3.1. Use the Criteria::ASC / Criteria::DESC string constants for forward compatibility.
  • null as an offset is deprecated (1.7) and gone in 3.0. $collection[null] and get(null) will bite during upgrade; prefer containsKey().
  • In-memory matching() is O(n) per call and re-walks the array. Calling it inside a loop over a large collection is a classic quadratic blowup. Push filtering into the repository, or filter once and reuse the result.
  • Serialization is shallow and uses __sleep/__wakeup. If you cache or queue an ArrayCollection containing entities, you're serializing the whole object graph. Prefer caching IDs and rehydrating; if you must unserialize(), pass explicit allowed_classes.
  • Debugging tip: Criteria::expr() on an ArrayCollection goes through ClosureExpressionVisitor. Step through that single class — it's ~100 lines and explains every "why did this match / not match" question.
  • Extension points worth knowing: AbstractLazyCollection::doInitialize() for deferred loading, ExpressionVisitor for a custom Criteria backend, and Selectable::matching() (now on ReadableCollection in 3.0) as the interface to type-hint when you accept both arrays and DB-backed sets.
  • Don't fight Illuminate\Support\Collection. Use doctrine/collections only where interop demands the Collection interface (Doctrine entities, repositories, third-party signatures) or where a Criteria needs to be portable between memory and SQL. Everywhere else, Laravel's collection is richer and faster to write.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
vento/laravel-airbrake
wfeller/laravel-batch
maco-studios/mage-os-installer
busarm/armie
siberfx/laravel-logger
kfteam1991/dcat-admin
dashed/dashed-laposta
dashed/dashed-ecommerce-montaportal
dashed/dashed-ecommerce-etsy
neohq/string-blade-compiler
log1x/acf-composer
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager