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 Scope Checks Laravel Package

beyondcode/laravel-scope-checks

Laravel Scope Checks offers simple helpers for verifying that an Eloquent model has a given query scope before you apply it. It helps keep dynamic query building safe and predictable, with readable checks that are easy to use and test.

View on GitHub
Deep Wiki
Context7

Getting Started

What you're actually adopting. A single trait that reflects over your model's scope* methods and exposes boolean predicates — scopeActive() becomes isActive(), scopePublished() becomes isPublished(). It answers "does this hydrated record satisfy the scope?" It does not make your queries safer, and it does not replace ->active() on a builder. Read src/ before you write a line of app code: the README and the release notes are inconsistent about whether this is an assertion helper or a generated-predicate layer, and those are different call sites. Confirm the trait's namespace and read the reflection logic for how it handles zero-argument scopes.

Install and wire up.

composer require beyondcode/laravel-scope-checks

Then add the trait to one model — not to a base model, not to ten models:

use BeyondCode\ScopeChecks\...; // confirm exact namespace from src/

class Post extends Model
{
    use /* trait */;

    public function scopePublished($query)
    {
        return $query->whereNotNull('published_at')
                     ->where('published_at', '<=', now());
    }

    public function scopeActive($query)
    {
        return $query->where('status', 'active');
    }
}

Verify the Composer constraint resolves on your Laravel version first. This shipped in February 2020, so treat Laravel 9/10/11 and PHP 8+ support as unverified until you've run it.

Where to look first. src/ — it's roughly 40 lines. Read the reflection loop and find the answer to three questions before you go further: does the generated method issue a query on every call, how does it detect an existing same-named method, and what happens when the scope takes parameters. Those three answers determine whether the rest of your integration is safe.

First real use case. Single-record presentation logic. You have a Blade partial or an API resource that repeatedly writes:

@if ($post->published_at && $post->published_at->lte(now()))

Replace it with @if ($post->isPublished()) and you've achieved the one genuine win: one named definition of "published" instead of five scattered inline variants. Do this on one model, one view, one PR. Write a feature test that asserts the boolean matches the inline equivalent for a published record, an unpublished record, and a record with a null timestamp. That parity test is the whole deliverable — if the booleans diverge from the queries they replaced, you've made the codebase worse.

Implementation Patterns

Predicate over query, not inside loops. The intended shape is a handful of checks on already-hydrated single records:

class PostResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            // fine: one record, one check
            'status_badge' => $this->isActive() ? 'live' : 'draft',
        ];
    }
}

The pattern to avoid is the same call inside a collection. If each check issues a database round trip, a paginated index with 25 rows becomes 25 extra queries the moment you touch the check in a loop.

Cache the result per record when you need it twice. If a Blade view branches on the same status in three places, compute it once:

@php($isPublished = $post->isPublished())

Or better, don't reach for the generated method in hot paths at all — compute the flag in the query with withExists() or a cast and let the predicate read a loaded attribute. The package is a vocabulary tool, not a query-optimization tool.

Use it to consolidate, not to introduce. The highest-value workflow is cleanup of an existing model: inventory every inline status query for that model, pick the two or three with the most call sites, convert those, and stop. Converting a scope that only one call site uses adds indirection for nothing.

Keep parameters out of scope. Only zero-argument scopes convert cleanly. If scopePublishedIn($query, $year) exists on the model, do not expect a generated counterpart — and if the package does generate something, verify what arguments it passes before trusting the result. Leave parameterized scopes alone and call them explicitly.

Pair it with a Policy for anything consequential. If a boolean gates an action rather than describing a record, put the decision in a Policy and let the model predicate feed it:

public function update(User $user, Post $post): bool
{
    return $user->id === $post->user_id && $post->isActive();
}

The naming consistency is the point; the package doesn't get to make the authorization decision.

Write the parity test as a test-case-per-scope. For each generated check, one test that loads a matching record and one that loads a non-matching record, asserting the boolean against the original query:

$this->assertSame(
    Post::whereKey($post->id)->published()->exists(),
    $post->isPublished()
);

That suite is what lets you reimplement the trait yourself later without re-deriving the semantics.

Gotchas and Tips

It's archived with a 2020 release date. No upgrade path, no issue triage, zero dependents. If you adopt it for real, you own the code forever. The honest play: spike it pinned to a tag, prove the pattern on one model, then copy the ~40 lines into app/Support/Concerns/ under your own namespace with a named owner. MIT license makes that trivially legal. Don't leave it in composer.json for a production path.

Unsaved models. Reflection-generated predicates typically answer false or throw on a record with no key, depending on how the underlying query is built. Assert on a new Post instance in a test before you rely on it anywhere a model might be unsaved.

Soft deletes and global scopes. A check that runs a query through the model inherits SoftDeletes and any global scopes you've registered. That's usually what you want, but confirm it — a withTrashed() context elsewhere in the app may expect the opposite answer, and a silently wrong false is the failure mode this package is worst at surfacing.

Non-filtering scopes produce nonsense booleans. scopeOrderedByDate or scopeWithComments don't describe a property of a record. If a generated check exists for them, it will return a plausible but meaningless answer. Audit your scope* methods and check the package's behavior on ordering/eager-loading scopes specifically — if it doesn't skip them, you have a footgun in the model.

Naming collisions silently override. A generated isActive() will clobber or be clobbered by a hand-written method or accessor of the same name. Grep the model (and any parent classes and traits) for is* methods before turning the trait on. If the package wins over your hand-written method, existing behavior changes with no error.

Refactor-away scopes fail loudly — that's the good case. Delete scopeActive and calls to isActive() throw. Verify this in the spike: rename a scope on a branch and confirm you get an exception rather than a false. If you get a false, add a test that pins the loud failure, because everything else about this package is silent.

Count your queries. Before merging, run a request through Telescope or DB::listen with the checks in place. If you see a query per record, either hoist the check out of the loop or replace it with an withExists() subselect or a cached column. Make this a review checklist item rather than a memory.

Keep it off the critical path. Never let these booleans govern billing, entitlements, access, or compliance state. A predicate that can return a plausible wrong answer belongs in a badge, not in a gate.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource