Implementing date filter on laravel queries - php

I am implementing a date filter on my orders table e.g. return all orders that where 'created_at' today, last week, last month, on a specific date or between two dates.
I know that laravel has dedicated where methods such as "whereDay", "whereMonth" and others.
So far I have written a big switch statement for each condition like his:
switch($date_filter){
case 'today':
$orders = Order::whereDay('created_at','=',date('d')->get();
break;
// case for each date filter type
}
My question is whether there is generate the date filter part of the query separately, so I could do something like this:
$orders = Order::whereRaw($this->getDateFilterAsSql($date_filter)->get();
so that I would not have to repeat the entire query for each date_filter type?

You can create multiple local scopes and use them:
$this->order->$date_filter()->get();
An example of a local scope:
public function scopeToday($query)
{
return $query->whereDay('created_at', date('d'));
}
You also can use a model as a repository and do this:
$this->order->$date_filter();
An example of a method in Order model:
public function today()
{
return $this->whereDay('created_at', date('d'))->get();
}

Related

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.
You have two problems:
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.
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();
}
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.
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());
}
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.

Dynamic model filter in Laravel's Eloquent

I'm looking for a way to make a dynamic & global model filter in Laravel.
I'm imagining a function like the following in my User.php model:
public function filter() {
return ($someVariable === true);
}
Whenever I do a query using Eloquent's query builder, I only want users to show up in the collection when the filter above returns true. I would have thought a feature like that existed, but a quick look at the documentation suggests otherwise. Or did I miss it?
I believe what you're looking for is Query Scopes.
They are methods that may be defined in a global or local context, that mutate the current query for a given model.
https://laravel.com/docs/5.5/eloquent#query-scopes
For example:
Lets say I have a database table called "Teams" and it has a column on it called "Wins." If I wanted to retrieve all Teams that had a number of Wins between Aand B I could write the following Local scope method on the teams model:
public function scopeWinsBetween($query, int $min, int $max)
{
return $query->whereBetween('wins', $min, $max);
}
And it could be invoked as such:
$teams = Teams::winsBetween(50, 100)->get();
I think you could use Collection macro but you will need to suffix all your eloquent get(); to get()->userDynamicFilter();
Collection::macro('userDynamicFilter', function () {
//$expected = ...
return $this->filter(function ($value) use($expected) {
return $value == $expected;
});
});
Thanks. For now I've simply added a post filter option to the models using the following code:
// Apply a post filter on the model collection
$data = $data->filter(function($modelObject) {
return (method_exists($modelObject, 'postFilter')) ? $modelObject->postFilter($modelObject) : true;
});
in Illuminate/Database/Eloquent/Builder.php's get() function, after creating the collection. This allows me to add a function postFilter($model) into my model which returns either true or false.
Probably not the cleanest solution but a working one for now.

How to count specific objects in an array collection based on a getter that return a string

I have an array of student objects. Each student has subjects as an array collection. Each subject has a function getStatus() that is calculated based on different arguments etc, thus there is no real property for status in the subject entity.
How can I count the number of subjects that are completed, in progress, and pending and display it per student in a table?
I could retrieve my students in my controller like this:
$students = $em->getRepository(Student::class)->findAll();
and then perhaps with loops count it somehow, but I don't get how.
I thought of creating a function that implements the filter on the array collection like seen in this answer, but I don't understand how to use that to filter on getStatus().
I also thought to implement ->matching with a criteria like this:
public function getSubjectsByStatus(string $status): ?Collection
{
$subjects = $this->subjects->matching(
Criteria::create()->where(Criteria::expr()->eq($this->getStatus(), $status))
);
return $subjects ?? null;
}
and then do a count on the returned collection, but the first parameter of eq() should be a string and I don't have a status property in the subject entity that can be used as a string, and adding a property now is not a good idea.
How can I count all subjects, pending subjects, completed subjects, and in progess subjects the best way?
You probably should consider making your status value an actual field in your database, since your problem would be easy to solve with a SQL/DQL query.
Without that being the case, here is how you could implement your getSubjectsByStatus method:
public function getSubjectsByStatus(string $status): ?Collection
{
return $this->tl1Configs->filter(function ($element) use ($status) {
return $element->getStatus() == $status;
});
}
But if you call that method three times to just count the amount of all status values, you are looping over your collection three times as well.
A "better" solution would probably be to make a specialized method to explicitly get these counts. This is just one way of achieving what you want though. A method to return an array of sub-collections instead of just status counts could is another solution if you want to actual work with the sub-collections (all depends on your actual usecase).
public function getSubjectStatusCounts(): array
{
$statusCounts = [];
foreach ($this->tl1Configs as $subject) {
$statusCounts[$subject->getStatus()] = ($statusCounts[$subject->getStatus()] ?? 0) + 1;
}
return $statusCounts;
}

Laravel where with Carbon addMinutes not working

I have a table representing events, each of which has a notice period, e.g. you can't book the event if it's currently less than 24 hours before the event.
I'm trying to create a 'bookable' scope for this, but am failing. Specifically, in the below, 'time' represents the time of the event (timestamp), and 'notice' the notice period, in minutes (integer), both of which are columns in the Events model. What I've found is that Laravel is not reading the 'notice' variable, i.e. treating it as 0. Any guidance would be appreciated, thanks.
public function scopeBookable($q) {
$q->where('time','>',Carbon::now()->addMinutes('notice'))->orderBy('time','ASC')->get();
}
The addMinutes() method expects an integer not a string.
Scope Option
You can pass the notice time through to the scope.
// Controller
$notice = 60;
Events::bookable($notice);
// Model
public function scopeBookable($q, $notice=0) {
$q->where('time','>',Carbon::now()->addMinutes($notice))->orderBy('time','ASC')-get();
}
Collection Option
You can always execute a self-join in SQL and check the value of notice in a subquery. Another option is to return a filtered eloquent collection.
public function scopeBookable() {
return Events::all()->filter(function($event) {
return $event->time > Carbon::now()->addMinutes($event->notice)
});
}

How to filter an Eloquent collection within a certain number of days?

I have an Eloquent model, Day, that contains an activate_date field which holds a Carbon object. I would like to return all of the days whose activate_date is within, for example, 7 days of today. I was trying to do it with a scope, but I can't seem to access the activate_date field within the scope function. Is there an easy way to do this?
You can do this easily:
$dateRef = new \Carbon::Carbon()
$dateRef->addDays(7);
// or $dateRef->subDays(7);
Day::where('activate_date', '<', $dateRef)->get();
with query scope:
public function scopeLast7Days($query)
{
$dateRef = new \Carbon::Carbon()
$dateRef->subDays(7);
return $query->whereBetween('activate_date', [$dateRef, new \Carbon::Carbon()])
}
// use like this:
$days = Day::last7Days()->active()->orderBy('created_at')->get();

Categories