Laravel Scopes
Tags: laravel, scopes, php
Published: 2021-08-22 12:01:11

Between internal testing on the App (admin staff, we really need a QA team  but we also really need another developer), I found myself with quite the downtime this week so I decided to work on a feature that we have been talking about for some time.  Nothing super vital but a cool idea / concept that we think could allow for some extra bookings, which is how we keep the lights on.  The idea is for whenever a captain declines a trip, we trigger an email that notifies the customer that the trip has unfortunately been declined but here are some similar trips.  Ideally we'd have everything all looking good in an email but coding emails literally are the worst thing ever so we have a link that takes us to a single page that has the charters displayed very similar to our search results.  

Since this page will be created using livewire, the route itself has model in it and we will gather charters that have similar trips based on the booking ID that was passed in.  
`Route::get('/bookings/{booking}/similar', \App\Http\Livewire\Web\SimilarCharters::class)->name('bookings.declined.similar-listings');` I know I'm not too elegant at writing routes, but it works.  I made out a list of what I thought were the most important factors of a trip to make them considered "similar".  To me the date of the booking is probably most important.  We have a lot of people coming from out of town so if they were planning on fishing on a Saturday they probably are going to need to keep it that way.  I also figured a 25 mile proximity of the previous charters location.  We need the amount of passengers allowed to be either the same or larger, the cost (this one is kinda still up in the air) but I said within plus or minus $100 of the initial booking.  The cost may change based on initial cost of the trip and we can maybe do a percentage but for now I just want to get some results.  And the final one that I'm still kinda on the fence about is duration of the booking.  Plus or minus an hour.  Seems reasonable but I also don't want to get to the point where there are too many conditions and no trips are returned.  

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zo7j2o5dmjt9dfztp85o.png) This is how I like to keep scope organized, but you do whatever works best for you.  For this process I'll be using nothing more than some local scopes, some which are already available from previous features and a few new ones I'll have to add.  The idea of local scope is to define a common set of constraints that can be used throughout the application.  The one thing too that we don't want to do is provide too many options.  You honestly cannot overwhelm the general public so I figured limiting the results to 9 (a nice 3 x 3 grid on computers) would be fitting.  We'll also have some type of button or link below the results that will link back to our Search results but with some prepopulated query parameters. 
```
    public function render()
    {
        $query = Charter::query()->limit(9);
        $query->isWithinMaxDistance($this->lat, $this->lng, 25);
        $query->withoutPrivateBookingsOn($this->date, $this->passengerCount);
        $query->similarDuration($this->duration);
        $query->similarCost($this->cost);
        return view('livewire.web.similar-charters')->with([
            'charters' => $query->get()
        ])->extends('layouts.web-layout')->section('content');
    }
```
That's my render of the livewire component and it's relatively simple.  The only new scopes that I had to create were the similarCost / similarDuration.  There are a couple of other scopes that I may add / use or even just use our main scopeAvailable but for now this seems to be working just fine.  

My two new scopes below and we can go over that real quick.
```
    public function scopeSimilarDuration($query, $duration)
    {
        $min = $duration - 1;
        $max = $duration + 1;
        $query->whereHas('trips', function ($subQuery) use ($min, $max) {
            return $subQuery
                ->whereBetween('duration_hours', [$min, $max]);
        });
    }

    public function scopeSimilarCost($query, $cost)
    {
        $min = $cost - 10000;
        $max = $cost + 10000;
        $query->whereHas('trips', function ($subQuery) use ($min, $max) {
            return $subQuery
                ->whereBetween('cost', [$min, $max]);
        });
    }
```
Since these scopes take in an extra parameter ($cost / $duration) they are considered Dynamic scopes since results will always be changing based on params.  They are both very similar as you can see and I probably could've combined them but the whole single responsibility principle would say no separate them.  I set a min / max value for the whereBetween clause on both of them.  Cost is based on pennies since we are dealing with strip so that's why its 10000 for $100 dollars, and the more I look at it here there more I think it really does just make sense to use a percentage so I'll probably go back and modify that a little.  But the idea is each scope returns as subQuery where the charter has trips that meet the where between clause.  

Then in the classic sense of reuse reuse reuse, I reustings our `_listing` partial to make the nice tiles (identical to the serach results tile) but no filters and yada yada yada.
```
<div class="mt-20 w-11/12 m-auto">
    <h3 class="font-bold text-2xl my-8"></h3>
    <div class="flex">
        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 w-full">
            @foreach($charters as $charter)
            @include('platforms.fishanywhere._partials._listing', ['charter' => $charter, 'route' =>
            route('charters.show', ['slug' => $charter->slug ]), ])
            @endforeach
        </div>
    </div>
</div>
```
Super basic but it works.  I'm sure they'll modify copy / design but this was more or less a proof of concept for them.  Honestly when I showed one of the admins he was super stoked and with the idea that all we have to do is pass in a booking ID to get results, he could set up add campaigns and on completed trips we can trigger follow up emails with similar trips as well, and hopefully this landing page will initiate another couple of bookings a week, which over time add up.