DEV Community

KILLALLSKYWALKER
KILLALLSKYWALKER

Posted on

Balance Through Scopes: Enforcing Business Rules with Global and Local Scopes

In any job portal , jobs should expire after a certain period . The common problem jobs keep showing up in search results which resulting frustating candidates and recrutiers .

Any way , when we develop this , we purposely not adding any job expired as we need to ensure our job portal look active (YTJT) .

Easy Fix

The fix is easy , dont think to much , just implement this directly .

public function index()
{
    $jobs = JobAdvertisement::where('expires_at', '>', now())->get();

    return view('jobs.index', compact('jobs'));
}
Enter fullscreen mode Exit fullscreen mode

See easy , but you know , we notice that we have multiple place need to update so we need to update at different place . Till the day that we found out this thing will never ending , we will still getting complaint from recruiter of the company and also from candidate that applied and receive notification from expired job because we miss some place to add the expires_at .

We did something like this also for the context

public function show($id)
{
    $job = JobAdvertisement::findOrFail($id);

    if ($job->expires_at < now()) {
        abort(404); 
    }

    return view('jobs.show', compact('job'));
}
Enter fullscreen mode Exit fullscreen mode

Basically we jut repeating the expiry logic everywhere and in risk missing it . This solve the expiry problem but not easy to maintain and scalable .

A New Hope

At this point , we starting to asking why we keep writing same condition , why not the model itself should know when a job is expired ?

By using global scope , we can make it more clean and easy to maintain as the logic in one place only .

Why global not model scope ? Since global scope will be enforce for each query , we want it automatically applied everywhere since we want to enforce this as one of our business rules in job portal where all job should be display only that are not expired only .

class JobAdvertisement extends Model
{
    protected static function booted()
    {
        static::addGlobalScope('active', function ($builder) {
            $builder->where('expires_at', '>', now());
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

So now everytime you use , it will already use the scope .

JobAdvertisement::all(); 
Enter fullscreen mode Exit fullscreen mode

With this we can guarantee all thing related to job advertisement will always show non expired by default .

BUT , we still got admin or recruiter that might need to access expired one also right ? No worry we can still handle this using this way

class JobAdvertisement extends Model
{
    protected static function booted()
    {
        static::addGlobalScope('active', function ($builder) {
            $builder->where('expires_at', '>', now());
        });
    }

    public function scopeWithExpired($query)
    {
        return $query->withoutGlobalScope('active');
    }

    public function scopeOnlyExpired($query)
    {
        return $query->withoutGlobalScope('active')
                     ->where('expires_at', '<=', now());
    }
}
Enter fullscreen mode Exit fullscreen mode

Now for usage admin and recruiter we can just call like this

$expiredJobAdvertisements = JobAdvertisement::onlyExpired()->get(); 

$jobAdvertisements = JobAdvertisement::withExpired()->get(); 

Enter fullscreen mode Exit fullscreen mode

Closing

Using global scope is really cool , but like any other thing , we should always know when to use it properly .

For me , if anything related to business rules and it need to be apply to all by default.

Don't use it when you want to get it by filtering based on context dependent and also user specific .

Top comments (0)