user123
user123

Reputation: 267

Way to structure code for "Search Engine"

I'm currently putting together some code that allows for a user to search an activities table in multiple ways (i.e. if title checkbox is selected) I feel like my code looks a little messy so I wanted to come on stack overflow and ask everyone what would be the best way to make this code more elegant? I'm just looking for ways to improve, have more readable code, and have a better structure for it.

    if (request('name')){
        $name = request('name');
        $user = User::where('name', $name)->firstOrFail();

        if (request('title') == 1) {
            $activities = Activity::with('activity')->where('user_id', $user->id)->whereHas('thread', function ($query) use ($search, $user) {
                $query->where('threads.user_id', '=', $user->id)
                    ->where('threads.title', 'LIKE', '%' . $search . '%');
            })->get();
            dd($activities);
        } else {
            $activities = Activity::with('activity')->where('user_id', $user->id)->whereHas('thread', function ($query) use ($search, $user) {
                $query->where('threads.user_id', '=', $user->id)
                    ->where('threads.title', 'LIKE', '%' . $search . '%')
                    ->orWhere('threads.body', 'LIKE', '%' . $search . '%');
            })->orWhereHas('reply', function ($query) use ($search, $user) {
                $query->where('replies.user_id', '=', $user->id)
                    ->where('replies.body', 'LIKE', '%' . $search . '%');
            })->get();
            dd($activities);
        }
    } else {
        if (request('title') == 1) {
            $activities = Activity::with('activity')->where('user_id', $user->id)->whereHas('thread', function ($query) use ($search, $user) {
                $query->where('threads.title', 'LIKE', '%' . $search . '%');
            })->get();
            dd($activities);
        } else {
            $activities = Activity::with('activity')->whereHas('thread', function ($query) use ($search) {
                $query->where('threads.body', 'LIKE', '%' . $search . '%')
                    ->orWhere('threads.title', 'LIKE', '%' . $search . '%');
            })->orWhereHas('reply', function ($query) use ($search) {
                $query->where('replies.body', 'LIKE', '%' . $search . '%');
            })->get();
        }
    }     

Thank you!

Upvotes: 0

Views: 93

Answers (1)

IGP
IGP

Reputation: 15786

You could use the query builder's when and unless methods as well as define some query scopes in your models so your end result could look something like this

$user = request('name') ? User::where('name', $name)->firstOrFail() : null;
$title = request('title') == 1;

$activities = Activity::with('activity')->search($search, $user, $title)->get();
# Activity model
public function scopeSearch(Builder $query, ?$search = null, ?User $user = null, bool $title = false)
{
    if (!$search)
        return $query;
    else
        return $query->when($user, fn($q) => $q->where('user_id', $user->id))
                     ->whereHas('thread', fn($thread) => $thread->search($search, $user))
                     ->unless($title, fn($q) => $q->orWhereHas('reply', fn($reply) => $reply->search($search, $user)));
}
# Thread model
public function scopeSearch(Builder $query, ?string $search = null, ?User $user = null)
{
    if (!$search)
        return $query;
    else
        return $query->when($user, fn($q) => $q->where('threads.user_id', $user->id))
                     ->where(fn($q) => $q->where('threads.title', 'LIKE', "%$search%")
                                         ->orWhere('threads.body', 'LIKE', "%$search%"));
}
# Reply model
public function scopeSearch(Builder $query, ?string $search = null, ?User $user = null)
{
    if (!$search)
        return $query;
    else
        return $query->when($user, fn($q) => $q->where('replies.user_id', $user->id))
                     ->where('replies.body', 'LIKE', "%$search%");
}

Scopes are basically reusable queries. You can define them at a global level (for all models) or at a local level (this is what I've done here).

Local Query Scopes

With them, I've moved nearly all the query related logic to the models but if you prefer, you could still write it all in the controller.

Using the scopes I defined,

$activities = Activity::with('activity')
    // call Activity Model's search scope
    ->search($search, $user, $title)
    ->get();

translates to

$activities = Activity::with('activity')
    ->when($user, fn($q) => $q->where('user_id', $user->id))
    // call Thread model's search scope in whereHas('thread', ...) closure
    ->whereHas('thread', fn($thread) => $thread->search($search, $user))
    // call Reply model's search scope in whereHas('reply', ...) closure
    ->unless($title, fn($q) => $q->orWhereHas('reply', fn($reply) => $reply->search($search, $user)))
    ->get();

which in turn translates to

$activities = Activity::with('activity')
    ->when($user, fn($q) => $q->where('user_id', $user->id))
    ->whereHas('thread', function ($thread) use ($search, $user) {
        $thread->when($user, fn($q) => $q->where('threads.user_id', $user->id))
               ->where(fn($q) => $q->where('threads.title', 'LIKE', "%$search%")
                                   ->orWhere('threads.body', 'LIKE', "%$search%"));
    })
    ->unless($title, fn($q) => $q->orWhereHas('reply', function ($reply) use ($search, $user) {
        $reply->when($user, fn($q) => $q->where('replies.user_id', $user->id))
              ->where('replies.body', 'LIKE', "%$search%");
    }))
    ->get();

The $title variable could be inlined at this point. ->unless(request('title') == 1, ...)

Upvotes: 1

Related Questions