Collections
Collection — type-safe ordered collections of domain objects with add, remove, contains, filter, and first.
Collections
Collection is the abstract base for typed domain collections. It enforces type safety at runtime — only items of the declared type can be added. It implements Countable and IteratorAggregate.
Define a Collection
use Vortos\Domain\Collection\Collection;
final class OrderLines extends Collection
{
protected function itemType(): string
{
return OrderLine::class;
}
}One line. The base class provides everything else.
API
$lines = new OrderLines();
// Add items — throws InvalidArgumentException for wrong type
$lines->add(new OrderLine(...));
$lines->add(new OrderLine(...));
$lines->add(new WrongType()); // throws InvalidArgumentException
// Remove by reference (or by value equality for ValueObjects)
$lines->remove($line);
// Check membership
$lines->contains($line); // bool
// Count
count($lines); // int
$lines->isEmpty(); // bool
// Convert to array
$lines->toArray(); // OrderLine[]
// Filter — returns new collection of same type
$expensiveLines = $lines->filter(fn(OrderLine $l) => $l->getPrice() > 100);
// First matching item
$line = $lines->first(fn(OrderLine $l) => $l->getSku() === 'SKU-001');
// returns null if not found
// Iterate
foreach ($lines as $line) { ... }Use Inside an Aggregate
final class Order extends AggregateRoot
{
private OrderLines $lines;
private function __construct(private OrderId $id)
{
$this->lines = new OrderLines();
}
public function addLine(OrderLine $line): void
{
if ($this->lines->contains($line)) {
return; // idempotent
}
$this->lines->add($line);
$this->recordEvent(new OrderLineAddedEvent((string) $this->id, $line->getSku()));
}
public function removeLine(OrderLine $line): void
{
$this->lines->remove($line);
$this->recordEvent(new OrderLineRemovedEvent((string) $this->id, $line->getSku()));
}
public function getTotal(): Money
{
return array_reduce(
$this->lines->toArray(),
fn(Money $carry, OrderLine $line) => $carry->add($line->getSubtotal()),
Money::of(0, 'USD'),
);
}
}Type Safety
$lines = new OrderLines(); // accepts OrderLine only
$lines->add(new OrderLine(...)); // OK
$lines->add(new ProductLine(...)); // throws InvalidArgumentException
$lines->add('string'); // throws InvalidArgumentExceptionThe error message: "Provided items type doesn't match with collections type".
ValueObject Equality in remove() and contains()
When the item implements ValueObject, remove() and contains() use equals() for comparison — not reference equality:
final class EmailList extends Collection
{
protected function itemType(): string { return Email::class; }
}
$list = new EmailList();
$list->add(Email::fromString('alice@example.com'));
// Different object instance, same value
$email = Email::fromString('alice@example.com');
$list->contains($email); // true — uses equals()
$list->remove($email); // removes — uses equals()For non-ValueObject items, strict reference equality (===) is used.
Collections Are Mutable
Unlike value objects, collections are mutable — add() and remove() modify the collection in place. This is intentional: collections model the aggregate's current state, which changes as commands are processed.
Collections are never readonly — you cannot have a readonly class with a mutable property.