Skip to main content

Custom Collections in PHP

tip

Why avoid plain arrays?

Arrays in PHP are flexible but lack type safety, which can lead to hidden bugs and unpredictable behavior, especially in large codebases or frameworks like Laravel.

Why Use Custom Collections?

  • Type Safety: Custom collections enforce that only specific types of objects are stored, catching errors early ("fail fast").
  • Readability: Returning FacultyCollection instead of a generic array makes intent clear—anyone reading the code knows exactly what is inside.
  • Built-in Methods: Collections can provide methods like groupBy, first, last, map, filter, etc., out of the box.
  • Consistency: Using collections creates a unified coding style and predictable behavior across your project.
danger

Avoid using arrays as function inputs/outputs unless required by a framework or external contract.

Example: Typed Collection Implementation

src/Shared/Domain/Read/TypedCollection.php
<?php
declare(strict_types=1);
namespace App\Shared\Domain\Read;
use Webmozart\Assert\Assert;
abstract class TypedCollection extends Collection {
public function __construct(array $elements = []) {
Assert::allIsInstanceOf($elements, $this->type());
parent::__construct($elements);
}
abstract protected function type(): string;
public function add(mixed $element): void {
Assert::isInstanceOf($element, $this->type());
parent::add($element);
}
}
src/Context/Foo/Domain/Read/View/FooCollection.php
<?php
declare(strict_types=1);
namespace App\Context\Foo\Domain\Read\View;
use App\Shared\Domain\Read\TypedCollection;
final class FooCollection extends TypedCollection {
protected function type(): string {
return Foo::class;
}
}

When Should You Use a Custom Collection?

  • When logic related to an array is being duplicated in multiple places (e.g., filtering, mapping, counting).
  • When you want to decouple clients from the internal structure of your data.
  • When you want to provide meaningful, named transformations (e.g., $faculties->activeOnly() instead of array_filter(...)).

Best Practices

  • Immutability: Prefer returning new collection instances for transformations, rather than mutating the original.
  • Minimal Interface: Only implement methods that your clients actually use.
  • Iterator Support: Implement IteratorAggregate and return an ArrayIterator for easy foreach support.
final class Names implements IteratorAggregate {
private array $names;
public function __construct(array $names) {
Assert::that()->allIsString($names);
$this->names = $names;
}
public function getIterator(): Iterator {
return new ArrayIterator($this->names);
}
}

Separation of Concerns: Business Logic vs Collections

Common Mistake

Placing business logic and domain-specific decisions inside collection classes violates the Single Responsibility Principle and reduces maintainability.

What Collections Should NOT Contain

Bad: Business logic in collection
class PricingPlanCollection extends TypedCollection
{
// ❌ Business decision
public function getCashPlans(): self {
return $this->filter(fn($p) => $p->type === PlanType::Cash);
}

// ❌ Domain rule
public function getDefaultPlan(): ?PricingPlan {
return $this->first(fn($p) => $p->isDefault);
}
}

Problems:

  • Tight coupling to business rules
  • Hard to test independently
  • Violates Open/Closed Principle

Responsibility Distribution

LayerResponsibilityExample
CollectionGeneric data operationsfilter(), map(), groupBy()
ServiceBusiness logic & decisionsfindActivePlans(), calculateTotal()
PolicyAccess control & permissionscanPurchase(), isAvailable()
ModelSimple entity rulesisActive(), isExpired()
Golden Rule

Collections = Data Structure
Services/Policies = Business Logic

Keep collections generic and reusable. Move all domain-specific logic to appropriate service or policy classes.

Summary

Custom collections are a simple but powerful tool for improving code quality, development speed, and reducing bugs in PHP projects. They provide structure, type safety, and clarity—making your codebase more maintainable and robust.

info

Team CTO Guidance: Avoid General Collections

"General-purpose collections are only about 30% better than arrays. But to achieve the remaining 70% of benefits, we should avoid generic collections as much as possible and instead create highly specific collections. Each collection should have a precise, expected class type. Always ask yourself: Is this collection as specific as it can be?"

Actionable Rule:

  • Prefer highly specific collections with strict expected types over generic collections.
  • Only use general collections when absolutely necessary.