0

I have created an api where I get offers that have 2 relationships in them:

  • venues belongsToMany offer (manyToMany)
  • days belongsToMany offer (manyToMany)

The offers then have the relationships synced and saved to them and in the returned json you can see the objects for days and venues.

What I am trying to do now is use haversine to get the distance of an offers venue lat/lng with the haversine value. I am currently using this eloquent query to get offers with their respective relationships:

$mytime = Carbon::now('Europe/London');
$time = $mytime->format('H:i');
$timeInt = $this->hourMinToInteger($time);
$today = $mytime->format('m/d/Y');
$day = strtolower($mytime->format('l'));
$tomorrow = strtolower(Carbon::now()->addDay(1)->format('l'));
$offersWithDays = Offer::with(['days','venues'])->get();

$response = [
    'now' => [],
    'later' => [],
    'tomorrow' => [],
    'featured' => []
];

// validOffers are all offers that fall within this stuff
foreach ($offersWithDays as $offerAll) {
    $relation = $offerAll->days()->get();

    $theDays = array();
    foreach ($relation as $theDay) {
      $theDayObj = $theDay->day;
      array_push($theDays,$theDayObj);
    }
    extract($theDays);

    if ($offerAll->is_featured) {
        $response['featured'][] = $offerAll;
    }
    if (in_array($day,$theDays) && $offerAll->offer_start_time < $time) {
        $response['now'][] = $offerAll;
    }
    if (in_array($day,$theDays) && $offerAll->offer_start_time > $time) {
        $response['later'][] = $offerAll;
    }
    if (in_array($tomorrow,$theDays)) {
        $response['tomorrow'][] = $offerAll;
    }
}

This works perfectly and I have no issues. I would now like to get the offers using haversine and the location of the venue attached to the offer but I am struggling to see how this would work in the eloquent query.

Here are the models for each relation:

Day

use Illuminate\Database\Eloquent\Model;

class Day extends Model
{
    /**
     * Get the offer days for the offer.
     */

    protected $fillable = ['start_time','end_time'];

    public function offers()
    {

        return $this->belongsToMany('App\Offer');
    }
}

Venue

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Venue extends Model
{
    //
    use SoftDeletes;

    protected $table = 'venues';

    protected $dates = ['deleted_at'];

    protected $fillable = ['id','address','description','phone_number','website','name', 'headline','lat','lng','days'];

    public static $rules = array(
        'name' => ''
    );

    protected $casts = [
        'lat' => 'float(10,8)',
        'lng' => 'float(11,8)'
    ];

    public function offers()
    {
        return $this->belongsToMany('App\Offer','offer_venue');
    }

}

Offer

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Offer extends Model
{
    /**
     * The database table used by the model.
     *
     * @var string
     */

    use SoftDeletes;

    protected $table = 'offers';

    protected $dates = ['deleted_at'];
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $hidden  = ['map_location', 'regular', 'name', 'distance_to_offer'];
    protected $fillable = ['id','venue_id','type_id','day', 'venue_address','is_featured','repeater_days','image', 'venue_description', 'offer_headline','offer_subheader','offer_terms','venue_phone_number','venue_website', 'venue_name', 'venue_headline','venue_description','venue_latitude','venue_longitude','offer_when','offer_end_time','offer_start_time','offer_start_date','offer_end_date','featured_date','current_time','current_date'];
    public static $rules = array(
        'image' => 'image|mimes:jpeg,jpg,png,bmp,gif,svg'
    );
    protected $casts = [
        'is_featured' => 'boolean',
        'is_regular' => 'boolean',
        'offer_when' => 'array'
    ];
    /**
     * Get the offers for the offer.
    */
    public function days()
    {

        return $this->belongsToMany('App\Day','day_offer', 'offer_id', 'day_id');
    }

    public function types()
    {

        return $this->belongsToMany('App\Type', 'offer_type');
    }

    public function venues()
    {

        return $this->belongsToMany('App\Venue', 'offer_venue', 'offer_id', 'venue_id');
    }


}

Can anyone help me with this and shed some light?

==== edit ====

Here is sample output of json with Eloquent query that I need:

enter image description here

See days and venues. I want this in a raw query which I have started below but I have directly queries on the days table, it would be better to query directly from the offers table and join venues objects and days objects like in the attachment:

$query = DB::table('days AS od')
            ->select('o.*',
                'ot.type',
                'od.day',
                'v.name AS venue_name',
                'v.headline AS venue_headline',
                'v.description AS venue_description',
                'v.phone_number AS venue_phone_number',
                'v.website AS venue_website',
                'v.lat AS venue_latitude',
                'v.lng AS venue_longitude', 'v.address AS venue_address',
                // TODO : remove the 37, -122 lat lng co-ordinate pair that is hardcoded into this string and adjust the radius variable below
                DB::raw('3959 * acos( cos( radians(37) ) * cos( radians( v.lat ) ) * cos( radians( v.lng ) - radians(-122) ) + sin( radians(37) ) * sin( radians( lat ) ) ) as distance'))
            ->join('day_offer AS odp', 'odp.day_id', '=', 'od.id')
            ->join('offers AS o', 'o.id', '=', 'odp.offer_id')
            ->join('offer_type AS otp', 'otp.offer_id', '=', 'o.id')
            ->join('types AS ot', 'ot.id', '=', 'otp.type_id')
            ->join('venues AS v', 'v.id', '=', 'o.venue_id'); 

        $radius = '10000';
        $query->having('distance', '<', $radius);
        $query->orderBy('distance', 'desc');
        $validOffers = $query->get();
M dunbavan
  • 1,157
  • 5
  • 22
  • 57

1 Answers1

0

I assume that in the end you want to be able to find offers based on users coordinates? If you get 10,000 offers or venues in the future – you don't want to go through them one by one and calculate the distance and then display the closest matches – that would be simply unfeasible.

I would suggest geospatial queries (supported by MySQL) – eg. give me 50 offers (or venues) located within 5km radius of latitude X and longitude Y.

Unfortunately, Eloquent doesn't support geospatial queries so you would need to do raw queries using DB facade, but performance will be great – you will get results instantly.

This is the rational way to go, in my opinion. Parsing the whole set of offers/venues, implementing distance calculation as Trait is irrational.

Denis Mysenko
  • 6,366
  • 1
  • 24
  • 33
  • So if I wanted to get the attached venues to each offers object a simple join on the query would work? – M dunbavan Jan 10 '16 at 10:21
  • Yes, of course. Once you get offers based on location – you can simply join the venue data. As a matter of fact, I did something very similar to this in the past. MySQL has special index type for geolocation – 'geometry'. It will give you a list of nearby offers in milliseconds! – Denis Mysenko Jan 10 '16 at 10:59
  • Oh okay then, I will need to get the distance of the offers venue based on the users location, if it's within 10000 in haversine query then we can show them. Can MySQL geometry do this? – M dunbavan Jan 10 '16 at 11:02
  • Yes it definitely can. See an example here: https://www.percona.com/blog/2013/10/21/using-the-new-spatial-functions-in-mysql-5-6-for-geo-enabled-applications/ Also, you will need to add table->engine = 'MyISAM' in your migration file (when offers table is created) because geometry indexes are only supported with ISAM storage. – Denis Mysenko Jan 10 '16 at 13:14
  • Denis Mysenko, I have edited my question so you can look at the example of what the Eloquent plain query is doing. What I need to know is, is it possible to do this in a raw query in laravel? – M dunbavan Jan 11 '16 at 08:48
  • This is not the way to go – it will still calculate ALL distances but on MySQL side instead of PHP side. I will add an example to my answer shortly! – Denis Mysenko Jan 11 '16 at 10:59
  • okay Denis well let me know, maybe we can go on chat in here and figure it out? – M dunbavan Jan 11 '16 at 12:20