E. Barney
E. Barney

Reputation: 383

Laravel scope using calculated value

I have the following problem in Laravel 5.4: There´s a user table and a membership table, where a user can have many memberships, and a membership belongs to a user. The memberships are of annual duration, but a member will get an additional free day for each friend they recommend that registers on my site, thus the number of free days is constantly changing; this in turn changes the membership´s expiration date. So the question is: How to scope the active memberships for a given user, if the expiration date is a variable? I will need to do something like this:

First the expiration date in Membership.php:

This calculates the total days for each membership: Note that the friendsDays are calculated per user in User.php

public function getTotalDaysAttribute() {
        $days = $this->paidDays + $this->user->friendsDaysRemaining;
        return $days;
    }

This calculates the expiration date for each membership:

public function getExpirationDateAttribute() {
    $date = $this->startDay->addDays($this->TotalDays);
    return $date;
}

So far so good... Now, this is where I´m stuck (pseudo code):

public function scopeActive($query, $dateToCheck = Null) {
   $query->where($dateToCheck >= $this->expirationDate);
}

How to code this properly to get:

dump($user->membership()->active()->get());

Thanks in advance.

Upvotes: 0

Views: 1462

Answers (4)

E. Barney
E. Barney

Reputation: 383

Thank you guys for your prompt answers. I figured it out using a different approach, based on your ideas. Since I cannot use calculated fields in the query, I went back to a field that do exists in the DB, this is the renewalDueDate, that´s a year from the payment date; both are known and fixed dates. Then, in the query I pass the $user and the $dateToCheck as parameters, substract the remaining friends days and compare to that value, like this:

public function scopeActive($query, $user, $dateToCheck = Null) {
    // If no date is passed, use today()
    $dateToCheck = is_null($dateToCheck) ? Carbon::today() : Carbon::parse($dateToCheck);

    //Substract the friendsDaysRemaining from the dateToCheck
    $AdjustedEndDate = $DateToCheck->copy()->subDays($user->friendsDaysRemaining);

    //Build the query 
    $query  ->where('paid', 1) //its been paid for
            ->where('startDay', '<=', $DateToCheck) //It has started
            ->where('renewalDueDate', '>=', $AdjustedEndDate); //It has not expired

    return $query;
}

Although is cumbersome to have to pass the user to get the remaining friends days, this is now working fine:

$dateToCheck= '2018-09-01';
dump($user->membership()->active($user, $dateToCheck)->pluck('id'));

Result:

Collection {#299 ▼ #items: array:2 [▼ 0 => 83 1 => 6 ] }

Of course you could also pass the $friendsDaysRemaining instead of the $user, but is also far from elegant.

Thanks again.

Upvotes: 0

Tom Headifen
Tom Headifen

Reputation: 1996

You have two problems:

  1. You are trying to use model values in your scope. Scopes happen before the model values have been set. This is because in order to get the values in the model to be set the query which fetches the data must first be executed.

  2. Unfortunately due to your database design you won't be able to create a scope to get the answer you want. This is because you are using values on a different table to calculate your TotalDays value.

I suggest you change expirationDate value in the database and call it when a friend is invited.

Something like:

function addDaysToExpiration(User $user) {
    $user->expirationDate = date('Y-m-d h:m:s', strtotime('2008-10-05' . '+1 day'));
    $user->save();
}

Upvotes: 1

Andrius Rimkus
Andrius Rimkus

Reputation: 653

With the API you defined for yourself:

$user->membership()->active()->get();

Your method scopeActive won't be able to see related User and friendsDaysRemaining variable that you need for calculating the expiration date. You can try it for yourself:

public function scopeActive($query) {
   var_dump($this->id); // null
   var_dump($this->user); // null, this part will try to do the query: select * from users where id = null
}

In your position, I would probably go with a persisted expiration_date column on the memberships table and update it whenever needed. This would then allow you to do smth like:

public function scopeActive($query) {
    return $query->where('expiration_date', '>', Carbon::now());
}

Upvotes: 0

Marcin Nabiałek
Marcin Nabiałek

Reputation: 111889

You can pass variable to scope, so for example you can define scope like this:

public function scopeActive($query, \Carbon\Carbon $dateToCheck = Null) 
{
   $query->where('some_date_field',  '>=' ($expirationDate ?? now())->toDateTimeString());
}

and then you can do:

$dateToCheck = now()->addDays(30);

dump($user->membership()->active($dateToCheck)->get());

You can also pass only number of days to scope instead of Carbon instance if it's more convienient to use in your case.

Upvotes: 0

Related Questions