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

Laravel Typed Collection Laravel Package

henzeb/laravel-typed-collection

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm version compatibility first. composer require henzeb/laravel-typed-collection. As of v1.14.0 the package supports Laravel 13 and PHP 8.5 — if you are bumping your app to either, this is the release to require. There is no service provider to register, no config file to publish and no migration: it is a thin base class over Illuminate\Support\Collection, so adoption cost is one composer require and one new class.

Where to look first: the package's TypedCollection base class in vendor/henzeb/laravel-typed-collection/src. Everything the package does is there — how the accepted type is declared, where the guard runs, and which exception is thrown when an item does not match. Read that class before writing your first collection; it is small and answers every question the README leaves open.

First use case: you have a bag of models being passed through a service, a job payload or a view composer, and someone eventually pushes an array, an ID or the wrong model into it. The failure surfaces far from the bug. A typed collection moves the failure to the exact line that pushed the bad value.

namespace App\Collections;

use App\Models\Post;
use Henzeb\TypedCollection\TypedCollection;

class PostCollection extends TypedCollection
{
    protected string $type = Post::class; // confirm the exact property/attribute in your installed version
}
$posts = new PostCollection(Post::all());

$posts->push($post);        // fine
$posts->push('some-id');    // throws immediately, at this line

Start with a single collection class around the model that hurts most, then expand.

Implementation Patterns

Wire it into Eloquent with newCollection(). This is the highest-leverage integration and the one that pays off daily — every relation result comes back typed for free.

class Post extends Model
{
    public function newCollection(array $models = []): PostCollection
    {
        return new PostCollection($models);
    }
}

$post->comments;      // PostCollection
$post->comments->first()->body;  // IDE and static analysis now know the type

Mind the base class here: relations return Illuminate\Database\Eloquent\Collection, so extend that (or the package equivalent) if you want newCollection() to work transparently, rather than Illuminate\Support\Collection.

Use collections as method signatures, not just as containers. A typed collection is a cheap, self-documenting contract for the service layer:

class PublishPosts
{
    public function handle(PostCollection $posts): void { /* ... */ }
}

Callers can no longer hand you an array or a mixed Collection, and iterable ambiguity disappears from the codebase.

Give the collection class business logic. This is where the pattern earns its keep — instead of $posts->filter(fn ($p) => $p->isPublished()) scattered in five places:

class PostCollection extends TypedCollection
{
    protected string $type = Post::class;

    public function published(): static
    {
        return $this->filter(fn (Post $post) => $post->published_at !== null);
    }

    public function wordCount(): int
    {
        return $this->sum(fn (Post $post) => str_word_count($post->body));
    }
}

Polymorphic bags: declare an interface or abstract base as the type, then let subclasses flow through. Useful for media libraries, notification payloads and event collections where homogeneity means "same contract", not "same class".

Boundary wrapping: when data arrives from a repository, an HTTP client or a cache read as a plain array, convert once at the edge of your domain and let everything downstream assume the type:

$posts = new PostCollection(
    collect($response->json('data'))->map(fn (array $row) => Post::fromApi($row))
);

Escaping the type deliberately: ->toBase() gives you an untyped collection to transform freely; re-wrap when you are done. Use this for any pipeline whose intermediate values are not the declared type.

Gotchas and Tips

Collection methods return static, so transformations can throw. map(), filter(), merge(), push(), put(), transform() and friends construct a new instance of your class — meaning a map() that produces arrays or DTOs throws mid-pipeline. Either ->toBase()->map(...) first, or map into the destination typed collection explicitly. This is the single most common surprise; the guard is doing its job, but the stack trace points at a line you did not expect.

->all() and array_* bypass everything. Validation happens at insertion, not on read. Once you hand $collection->all() to a helper, the guarantee is gone. Prefer iterating the collection itself or type-hinting the collection class all the way down.

Model::all() and query builder results are not automatically your typed collection. newCollection() covers relation hydration and model construction paths; a raw ->get() on some versions returns the default Eloquent collection. Wrap explicitly at those call sites or centralise it in a repository method.

Serialization is unchanged. A typed collection JSON-encodes exactly like a normal collection, so you can introduce it without touching API payloads or Inertia props. This makes it a low-risk refactor inside an existing endpoint.

Type resolution is a class-string. Interfaces and abstract classes work (via instanceof), mixed or a missing type defeats the point entirely. Keep the declared type as narrow as you can actually guarantee.

Debugging a stray push: the exception is thrown from the offset-set path, so the trace's first meaningful frame is your offending push/put/array-literal — that is the fix location, not the collection constructor. If you need to tolerate legacy data during a migration, catch at the boundary, log the type, and rebuild the collection from the filtered set rather than loosening the declared type.

Performance is not a concern. The check is an instanceof per item; it will never be your bottleneck next to the query that produced the models. Do not avoid typed collections on hot paths.

Extension points: override the guard/exception if you want a domain-specific error; add toArray() shaping, ->ids(), ->groupByAuthor() and similar domain helpers on your subclass — that is the intended use, and it keeps controllers thin.

Version note for upgrades: v1.14.0 is a compatibility-only release (Laravel 13, PHP 8.5). No API changes, no deprecations, so there is no migration work from v1.13.x — just verify your composer.json platform constraints and your CI matrix (PHP 8.5 + Laravel 13) before bumping. If you pin laravel/framework conservatively, the package will still resolve on earlier versions as before.

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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel