Ben Carey
Ben Carey

Reputation: 16968

Scoping Queries within Trait on a Laravel Model

A Little Background…

I have two models within my application that are 'locatable'. I could very easily write a query in a repository to return what I need, however, I am sure this can be done much better with the help of Laravel Scopes and Traits etc (I may be wrong).

So, I have come up with the following solution:

Example Abstract Model

abstract class AbstractModel extends Model implements SomeOtherStuff
    
{
    public function scopeNearby(Builder $query)
    
    {
        return $query->selectRaw('
                (
                    6373000 * acos(
                        cos( radians( ' . $this->location->latitude . ' ) )
    
                        * cos( radians( X(' . $this->location->table . '.location) ) )
    
                        * cos( radians( Y(' . $this->location->table . '.location) ) - radians( ' . $this->location->longitude . ' ) )
    
                        + sin( radians( ' . $this->location->latitude . ' ) )
                        * sin( radians( X(' . $this->location->table . '.location) ) )
                    )
                ) AS distance
           ')
            // I wanted to use ->with('location') below instead of ->join() but it didn’t work
            ->join($this->location->table, $this->location->primaryKey, '=', $this->primaryKey);
    }

    // All other Abstract stuff here
}

Example User Model

class User extends AbstractModel implements SomeOtherStuff
{
    
    /**
     * Get the location of the user.
    
     */
    public function location()
    {
    
        return $this->hasOne(‘App\Models\User\Location', 'user_id');
    
    }

    // All other model stuff here
}

Example User Controller

class UserController extends AbstractController
    
{
    /**
     * Return the nearby users.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    
    public function nearby()
    {
        $users = $this->shield->user()->nearby()->toSql();

        // Print out the query to ensure it is structured correctly
        echo $users;
    
    }
}

My Issues

The above solution works, however, it is very wrong (in my opinion)! The nearby() scope should only be available to models that are ‘locatable’. Naturally, I thought the best solution was to implement a Trait like so:

trait Locatable
{
    /**
        
     * Scope a query to include the distance from the current model.
        
     *
     * @param   \Illuminate\Database\Eloquent\Builder $query
     * @return  \Illuminate\Database\Eloquent\Builder
        
     */
        
    public function scopeNearby(Builder $query)
        
    {
        // Same as above implementation
    
    }
    
}

The problem with this is that the properties of the model that I need are protected and therefore not accessible via a Trait…

My Question(s)

Assuming the above makes sense, please see my questions below:

  1. Is there a better way to implement this desired functionality and if so, please can someone point me in the right direction?
  2. This is lower priority but why does ->with('location') not work? Am I using it wrong? I expected it to add an inner join to the query but it didn’t…

Upvotes: 4

Views: 4433

Answers (1)

prateekkathal
prateekkathal

Reputation: 3572

This is how it should be done actually

trait Locatable {

  /**
   * Scope a query to include the distance from the current model.
   *
   * @param   \Illuminate\Database\Eloquent\Builder $query
   * @return \Illuminate\Database\Eloquent\Builder
   */
  public function scopeNearby($query)
  {
    if(!$this->location) {
      return $query;
    }

    return $query->whereHas('location',
      function($query) {
        $query->selectRaw('
                    //Your query for distance (Use the getTable() function wherever required...)
                  ) AS distance
                ')
              ->having('distance', '<', 25);
      }
    );
  }
}

Answer to your questions.

  1. The above is the better approach. You should use whereHas() for relational queries.

  2. With query will not work because with doesn't join the tables, it only does a WHERE EXISTS () query and then later adds it as a relation to the model returned. To know more... What you can do is....

(Write this wherever you want in your controller... just for the purpose of testing)

dd($user->with('locations')->toSql());

Let me know if you face any issues... :)

Upvotes: 1

Related Questions