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

Typed Collection Laravel Package

gamez/typed-collection

gamez/typed-collection adds type-safe, strongly typed collections to Laravel and PHP. Define the item type once, keep the familiar Collection API, and get better IDE autocompletion, static analysis, and early errors when invalid values are added.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer; the package only needs illuminate/support (it is not a full Laravel package, so there is no service provider to register):

composer require gamez/typed-collection

Look at these first:

  1. README.md — the canonical list of accepted type names and the exact mutation methods that are guarded.
  2. src/TypedCollection.php — the abstract base class; this is the file you subclass and the source of truth for validation behavior.
  3. tests/ — short, readable examples of every supported type declaration.

The minimal use case is a domain collection: subclass Gamez\Illuminate\Support\TypedCollection, declare protected static $allowedTypes, and return it from repositories/services instead of a bare Collection.

use Gamez\Illuminate\Support\TypedCollection;

final class UserCollection extends TypedCollection
{
    protected static $allowedTypes = [User::class];
}

$users = new UserCollection([$userA, $userB]); // validated in the constructor
$users->push($userC);                         // validated on mutation
$users->push('not-a-user');                   // throws InvalidArgumentException

Allowed types generally include class/interface names plus the primitive names used by PHP's type system (string, int/integer, float, bool/boolean, array, object, callable, null, mixed). Confirm the exact spelling in the README before relying on aliases.

A good first use is replacing an untyped Collection return value on one service method and letting PHPStan/Psalm and your IDE document the element type at every call site. Start small: one collection class, one boundary, one test.

Implementation Patterns

One collection class per element type. Define UserCollection, InvoiceCollection, MoneyCollection, etc. Keep them final where possible, and add domain helpers that read naturally at call sites:

final class InvoiceCollection extends TypedCollection
{
    protected static $allowedTypes = [Invoice::class];

    public function total(): int
    {
        return $this->sum(fn (Invoice $i) => $i->amountCents());
    }

    public function unpaid(): self
    {
        return $this->filter(fn (Invoice $i) => ! $i->isPaid());
    }
}

Use typed collections as service/repository return types. This is where they pay for themselves: the type contract travels with the data, so controllers, jobs, and API resources do not need defensive instanceof checks or repeated docblocks.

final class UserRepository
{
    public function active(): UserCollection
    {
        return new UserCollection(
            User::query()->where('active', true)->get()->all()
        );
    }
}

Convert at the boundary, not in the middle. Eloquent returns Illuminate\Database\Eloquent\Collection; typed collections extend the base support collection. Wrap once when crossing from persistence into your domain layer, then keep the typed collection for the rest of the pipeline.

Handle type-changing transformations explicitly. Methods such as map(), filter(), and pluck() construct a new instance of the same class, so the result is re-validated against $allowedTypes. When the element type changes, drop to a base collection first and re-wrap in the target type:

$names = $users->toBase()->map(fn (User $u) => $u->name)->values();

$ids = new UserIdCollection(
    $users->toBase()->pluck('id')->all()
);

Add named constructors for common hydrations. Keeps array/JSON/Eloquent conversion in one tested place instead of scattered new XCollection(...) calls:

public static function fromJson(string $json): self
{
    return new self(json_decode($json, true, flags: JSON_THROW_ON_ERROR));
}

Integrate with Laravel without fighting it. Use typed collections in jobs, actions, and API resources. Do not try to make them the return type of an Eloquent relationship or Model::newCollection() — those slots require an Eloquent collection subclass. If you want Eloquent-level custom collections, that is a separate concern; convert to your typed collection when the data leaves the model layer.

Test the contract, not the internals. Data-provider tests for accepted and rejected types, plus one test per domain helper, are enough:

public function test_rejects_foreign_items(): void
{
    $this->expectException(InvalidArgumentException::class);
    new UserCollection([new stdClass()]);
}

Annotate for static analysis. Add @extends \Illuminate\Support\Collection<int, User> and @method tags for your helpers so PHPStan/Psalm understand element types through chain calls.

Gotchas and Tips

  • Derived collections inherit the type constraint. Because map()/filter() return new static(...), a UserCollection::map() that returns strings will throw at runtime. This is the most common surprise; use toBase() for cross-type transforms.
  • Check which mutators are guarded. The constructor and push() are the documented choke points; verify in src/TypedCollection.php whether offsetSet, put, prepend, merge, and concat also validate. If any bypass validation, route bulk writes through the constructor instead of mutating in place.
  • null is a type. An empty collection is fine, and first() returning null is normal collection behavior, but pushing null fails unless null is in $allowedTypes. Add it deliberately or not at all.
  • Scalar aliases are not guaranteed. int vs integer and bool vs boolean may behave differently depending on the validator. Pick one spelling, assert it in a test, and be consistent across the codebase.
  • Eloquent collections are not interchangeable. A UserCollection cannot be returned from HasMany::get() expectations or Model::newCollection(). Convert with new UserCollection($eloquent->all()) at the seam.
  • Serialization can skip validation. Collections serialized into queued jobs or the cache may be restored without running the constructor. Treat deserialized data as untrusted and re-wrap it (or assert it) on the receiving side.
  • $allowedTypes is static. You cannot vary allowed types per instance. If you need two element types, define two collection classes rather than toggling configuration at runtime.
  • Debugging failures. The exception message usually names the offending type; log the collection class plus get_debug_type($item) at the catch site. Add a small assertion helper in a base class if you need richer diagnostics across many collection types.
  • Extension points. Subclass helpers and named constructors are the intended extension surface. If you must customize validation, keep any override narrow and covered by tests — this class sits on the hot path of every collection construction.
  • Type-hint the concrete class, not the abstract. function handle(UserCollection $users) gives readers and static analysers real information; TypedCollection alone does not.
  • Version constraints matter. The package tracks illuminate/support ranges; if you are pinned to an older or newer Laravel, check the Composer constraint before adopting it broadly.
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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource