Laravel where with Carbon addMinutes not working - php

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)
});
}

Related

Add to existing total using increment

I have a small fundraiser portion of my site and I need to keep adding to the total amount on every transaction and how many sponsors I get.
I have my transaction set as a POST request.
Route::post('/fundraiser/checkout', 'FundController#fundCheckout')->name('fundraiser-checkout');
In my controller I'm doing the following to increment the sponsors_received and funding_received.
Note the below $subscription->quantity = the amount given. $45 * 100 = 4500 cents for stripe.
$funds = DB::table('funds')->where('cin', $cin)
->increment('funding_received', $subscription->quantity)
->increment('sponsors_received');
Want to keep adding to funding_received total and sponsors_received.
This actually does add to my funding received, but fails on sponsors_received with an odd error.
Symfony\Component\Debug\Exception\FatalThrowableError Call to a member
function increment() on integer
If I remove the funding_received query everything works fine. (Can I not use increment twice in a query?)
$funds = DB::table('funds')->where('cin', $cin)
->increment('sponsors_received');
Is there another way I can add to funding_received other than using increment?
Using Laravel 6+
The increment method from the Query Builder is basically a custom update call that handles the increment process. And because it is an update, it returns the same result as an update call, which is the number of updated entries. That's why you get the error:
Call to a member function increment() on integer
So you can't chain the next increment statement because the first one returns a number (integer) instead of a Query Builder instance. You can however do something like this:
$query = DB::table('funds')->where('cin', $cin);
$query->increment('funding_received', $subscription->quantity)
$query->increment('sponsors_received');
This will create a base query with your given condition and run each increment individually.
You're correct. The return type of public function increment() is an int https://laravel.com/api/6.x/Illuminate/Database/Eloquent/Builder.html#method_increment, so chaining calls to increment() is not allowed.
int increment(string $column, float|int $amount = 1, array $extra = [])
Increment a column's value by a given amount.
Parameters
string $column
float|int $amount
array $extra
Return Value
int
A simple way to update this is using the save() method:
$funds = Funds::where('cin', $cin)->firstOrFail();
$funds->funding_received += $subscription->quantity;
$funds->sponsors_received++;
$funds->save();
Note: This assumes you have a model for your funds table.

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.

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;
}

Exceeded maximum time error when overriding the newQuery on Laravel 4.0

So, I was trying to implement this answer for my other question on the same subject... and it keeps givin me the exceeded time error. Any clues?
This is on my product model. It inherits from Eloquent.
public function newQuery($excludeDeleted = true)
{
$user_permission = Auth::user()->permissions;
if( $user_permission->master )
return parent::newQuery();
else if( $user_permission->web_service )
{
$allowed_ids = array();
foreach( $user_permission->allowed_products()->get() as $allowed)
$allowed_ids[] = $allowed->id;
return parent::newQuery()->whereIn('id', $allowed_ids);
}
return parent::newQuery();
}
If the user is master there is no need to query scope on the request. But, if it isn't then I need to filter by the logged user's permissions.
UPDATE:
I tried the following code in a controller and it works alright:
$user_permission = Auth::user()->permissions;
echo "<PRE>"; print_r($user_permission->allowed_products()->get()); exit;
UPDATE 2:
Guys, I just found out that the problem was in this peace of code:
$allowed = Auth::user()->permissions()->first()->allowed_products()->get()->list('id');
It somehow give me an Maximum execution time of 30 seconds exceeded. If I put the exact same code in a controller, works like a charm, though! I also tried to put it in a scope, also worked. This it's really grinding my gears!
Elloquent has a function called newQuery. Controller does not. When you implement this function in a Model you are overriding the one in Elloquent. If you then invoke Elloquent methods that need a new query for your model before they can return, like ->allowed_products()->get(). Then you are calling your own newQuery() method recursively. Since the user permissions have not changed, this results in infinite recursion. The only outcome can be a timeout because it will keep on trying to determine a filtered product list which causes your newQuery() method to be called, which tries to determine the filtered product list before returning the query, and so on.
When you put the method into a Controller, it is not overriding the Elloquent newQuery method so there is no infinite recursion when trying to get the allowed_product list.
It would be more efficient to apply the filter to the product query based on whether the id is in the user's allowed_products() list using ->whereExists() and build up the same query as allowed_products() except now add condition that id from the query you are filtering is equal to the product id in the allowed products query. That way the filtering is done in the database instead of PHP and all is done in the same query so there is no recursion.
I don't see how your update code works. Illuminate\Database\Eloquent\Collection does not have any magic methods to call the relation functions, you should get an undefined method error trying to do that.
Can you try something like
public function newQuery($excludeDeleted = true)
{
// Returns `Illuminate\Database\Eloquent\Collection`
$user_permission = Auth::user()->permissions;
if ($user_permission->master)
{
return parent::newQuery();
}
else if ($user_permission->web_service)
{
// If here you was to call $user_permission->allowed_products()->get() not much is going to happen, besides probably getting an undefined method error.
$allowed_ids = Auth::user()->permissions()->allowed_products()->get()->lists('id');
return parent::newQuery()->whereIn('id', $allowed_ids);
}
return parent::newQuery();
}
Update: as per comments below I believe the problem is due to newQuery() being called multiple times as the code works just fine when called once in a controller. When this is applied to every query there is no need to collect all the IDs over and over again (assuming they're not going to change each time you call for them). Something such as the below will allow you to store these and only process them once per request rather than every time a query is run.
private $allowed_ids_cache = null;
public function newQuery($excludeDeleted = true)
{
$user_permission = Auth::user()->permissions;
if ($user_permission->master)
{
return parent::newQuery();
}
else if ($user_permission->web_service)
{
if ($this->allowed_ids_cache === null)
{
$this->allowed_ids_cache = Auth::user()->permissions()->allowed_products()->get()->lists('id');
}
return parent::newQuery()->whereIn('id', $this->allowed_ids_cache);
}
return parent::newQuery();
}

Implementing date filter on laravel queries

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();
}

Categories