Reputation: 16968
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:
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
}
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
}
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;
}
}
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…
Assuming the above makes sense, please see my questions below:
->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
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.
The above is the better approach. You should use whereHas()
for relational queries.
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