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 Repository Laravel Package

tlikai/laravel-repository

A Laravel package that adds a clean repository layer to your app, wrapping database queries behind interfaces for better separation of concerns, easier testing, and swappable data sources. Ships with base repository classes and service provider bindings for quick setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up

composer require <vendor>/<package>   # 1.0.5 or later — see the note below
php artisan vendor:publish --tag=<package>-config

Create a repository for the model you actually search on, bind it in a service provider, and inject the interface — never the concrete class — into your controllers/services.

class UserRepository extends BaseRepository implements UserRepositoryInterface
{
    public function model(): string { return User::class; }
}

// AppServiceProvider::register()
$this->app->bind(UserRepositoryInterface::class, UserRepository::class);

Where to look first

  1. src/Criteria/ — this is where the value is. FuzzySearch and AttributeSearch are the two you will use daily; read them before writing your own.
  2. The base repository's pushCriteria() / popCriteria() / skipCriteria() / resetCriteria() methods — they define the entire lifecycle of a criterion (per-query, not per-instance, in most implementations).
  3. config/<package>.php — pagination defaults, request-key mapping, and any "searchable fields" allowlist.

First use case

A search endpoint with a free-text box and a few dropdown filters — exactly the scenario the release notes describe:

public function index(SearchUsersRequest $request)
{
    $repo = $this->users;

    if ($keyword = $request->validated('q')) {
        $repo->pushCriteria(new FuzzySearch($keyword, ['name', 'pinyin']));
    }

    foreach (array_filter($request->validated('filters', [])) as $field => $value) {
        $repo->pushCriteria(new AttributeSearch([$field => $value]));
    }

    return UserResource::collection($repo->paginate(20));
}

Upgrade note for 1.0.5: if you are on 1.0.4 or earlier, upgrade deliberately. The SQL for that exact snippet changes from

where `name` LIKE "%Jack%" OR `pinyin` LIKE "%%Jack" AND `city` = "北京"

to

where (`name` LIKE "%Jack%" OR `pinyin` LIKE "%%Jack") AND `city` = "北京"

The old query was wrong (operator precedence made the city filter apply only to the pinyin branch), so anyone relying on the buggy wider result set will see row counts drop after the upgrade. That is the fix working, not a regression — but it will break assertions and any paginated "result count" snapshot tests.

Implementation Patterns

Treat criteria as composable, ordered query fragments. Each pushCriteria() call appends a WHERE fragment; the order you push them is the order they land in SQL. Keep push order deterministic: text criteria first (they group their own ORs), attribute criteria after.

Keyword + filters is the canonical pattern. Because FuzzySearch now wraps its OR set in parentheses, (fuzzy) AND (attribute) AND (attribute) composes correctly no matter how many filters follow. Before 1.0.5 you had to hand-roll the grouping yourself; you no longer need to.

Keep the criteria chain in one place. Put the "build the criteria stack from a request" logic in a dedicated method or a form request → DTO, not scattered across controllers:

protected function applySearch(RepositoryInterface $repo, SearchCriteriaData $data): void
{
    $data->keyword && $repo->pushCriteria(new FuzzySearch($data->keyword, $data->columns ?? ['name']));
    foreach ($data->attributes as $field => $value) {
        $repo->pushCriteria(new AttributeSearch([$field => $value]));
    }
}

Scope resets. Repositories are often resolved as singletons; call resetCriteria() at the start of a request/command if your version persists criteria across calls, otherwise a criterion pushed for one endpoint leaks into the next repository call in the same request (queues, console commands, and Livewire actions are the usual victims).

Use skipCriteria() for privileged paths. Admin exports, internal jobs, and "select all matching" operations should bypass the fuzzy criteria you apply to the public search. This is a one-liner and far safer than duplicating query code.

Integration tips

  • Bind criteria to validated data only. Never build FuzzySearch or AttributeSearch from raw $request->all() — an unfiltered field name can reach the WHERE clause as an identifier.
  • Keep the column list for FuzzySearch a constant on the repository (UserRepository::SEARCHABLE), so it can't be widened from the request.
  • Paginate with ->paginate() after the criteria are applied; results are lazy, so order matters only for correctness, not performance.
  • For resources, cache only the un-paginated base query per filter hash — criteria-annotated queries rarely cache cleanly.

Testing pattern. Assert on generated SQL, not just row counts, when touching search:

DB::listen(fn ($q) => $this->assertStringContainsString('(`name` LIKE', $q->sql));
$repo->pushCriteria(new FuzzySearch('Jack', ['name', 'pinyin']));
$repo->pushCriteria(new AttributeSearch(['city' => '北京']));
$repo->all();

Add at least one fixture-based test where a row matches the keyword but not the attribute filter — that is exactly the case 1.0.5 changed.

Extension point. Custom criteria are the clean way to encode business search rules:

class ActiveOnly implements CriteriaInterface
{
    public function apply($model, RepositoryInterface $repository)
    {
        return $model->where('status', 'active');
    }
}

Subclass FuzzySearch if you need a different wildcard strategy (e.g. prefix-only for indexed columns) rather than patching vendor code.

Gotchas and Tips

1.0.5 is a silent behaviour change, not a cosmetic one. Pre-1.0.5, A OR B AND C evaluated as A OR (B AND C). Any report, export, or count query built on the old criteria chain will now return fewer rows. Audit anything that consumed those results — CSV exports, dashboards, notification recipient lists — before rolling it out. This is the single biggest thing to check in this release.

Beware the %%Jack wildcard. The second column gets a double leading % ("%%Jack") while the first gets "%Jack%". Functionally it behaves as a suffix/contains match, but it's almost certainly unintentional and it means your name and pinyin columns are treated differently by the same criterion. If pinyin matching looks asymmetric, this is why — pin the expected behaviour with a test or override the criterion.

Leading wildcards defeat indexes. %term% cannot use a B-tree index. Fine for small tables; on anything past a few hundred thousand rows you want a full-text index or Laravel Scout, not this criterion. Don't discover this in production.

No trimming or normalisation. The sample SQL shows = " 北京" with a leading space — the value passed in is bound verbatim. Trim, lowercase, and normalise inputs in the form request, or you will get mysterious zero-result searches from copy-pasted values.

Grouping is not transitive. Each criterion groups its own ORs. You cannot get (A OR B) AND (C OR D) by pushing two FuzzySearch instances — you'll get (A OR B) AND C AND D-style nesting that is wrong for your intent. Combine into one custom criterion when you need parallel OR groups.

Order of pushCriteria is observable. Reordering calls can change generated SQL and, with it, test snapshots. If you refactor a search method, re-run SQL assertions.

Debugging. DB::listen() or DB::enableQueryLog() is more reliable than toSql() (which drops bindings and can hide the parenthesisation you're trying to verify). For quick inspection, Laravel Debugbar/Telescope shows the final SQL with bindings — that's where you'll see whether the grouping fix is actually in play.

Empty inputs. Verify what FuzzySearch('') and AttributeSearch([]) do in your version — most implementations no-op, but if they don't you'll emit LIKE "%%" and match every row (and skip the index). Guard at the call site.

Extension points to prefer. Custom CriteriaInterface implementations and repository method overrides are safe. Editing vendor/ for the fuzzy wildcard or the grouping is not — 1.0.5 exists precisely because that logic belongs upstream.

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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator