Laravel eloquent query using local scope with conditional statement and arguments - php

I've written the following function in my Car model that does the following:
Gets the related reservations based on two dates(pickup/dropoff)
Checks if the amount of these reservations are equal or exceed the quantity of the car
Finally returns a boolean depending on the output
/**
* Custom Functions
*/
public function isAvailableFor($from, $to) {
$reservationsCount = $this->reservations->where('pickup_date', '>=', $from)->where('dropoff_date', '<=', $to)->count();
if($reservationsCount >= $this->quantity) {
return false;
}
return true;
}
The function is working as expected but I want to implement this in a more elegant way using local scopes so I can actually use it efficiently when querying the Car model in my controllers but I can't find the correct way to do it and my code becomes a complete mess.
For example I have the following scope that I am using by just typing Car::active()->get();
/**
* Scopes
*/
public function scopeActive($query)
{
return $query->where('status', 'active');
}
The main problem is the count() function that doesn't let me implement my function in a scope-way or at least I am not that experienced to come up with a solution.
Thanks in advance.
Update
As correctly pointed by OsDev since my function returns a boolean it can not be implemented directly in the scope function. I can alternatively do this in my scope function but I guess it is pretty much an overkill:
public function scopeAvailable($query, $from, $to) {
$excludedId = array();
$cars = Car::whereHas('reservations')->get();
foreach($cars as $car) {
if(!$car->isAvailableFor($from, $to)) {
array_push($excludedId, $car->id);
}
}
return $query->whereNotIn('id', $excludedId);
}

You have to return the $query instead the count result because that way you don't break the Query Builder chain
You can't combine scopes and Model functions because scopes are supposed to return the $query builder object and in that example, your function is returning a boolean.
You can do something like this
/**
* Scopes
*/
public function scopeIsAvailableFor($query,$from,$to)
{
return $query->where('pickup_date', '>=', $from)->where('dropoff_date', '<=', $to);
}
Then you can chain it and call count if you want
$count = Car::active()->isAvailableFor('2020-05-03','2020-05-06')->count();
Maybe you can wrap your new scope into your model method
public function isAvailableFor($from, $to) {
$reservationsCount = $this->reservations->isAvailableFor($from,$to)->count();
return !$reservationsCount >= $this->quantity;
}

Related

Laravel Reuseable Functions

I have a videos table which stores details about a video and another videoStats table which stores how many times a. video has been watched.
I am creating a Service with some custom methods to suit the requirement of the application. I don't know if it is ideal to create a new service for the need or if I can have these methods within the model itself. If there is any other way to do it better, I would appreciate some suggestions.
I have a method mostWatchedVideos which returns a list with watch_count and video_id for every video.
The second method mostWatchedVideosBetween is supposed to return the same thing as mostWatchedVideos but for a certain time period. I can repeat the same code with additional argument for the date ->whereBetween('createdAt', [$from, $till]) and it would do the trick, but what I was thinking of is, since both the methods use the same code, is there a way to reuse the method but with the date filter?
public static function mostWatchedVideosBetween(Carbon $from, Carbon $till)
{
}
public static function mostWatchedVideos($limit = NULL)
{
return VideoStats::select(\DB::raw('count(*) as watch_count, videoId'))
->groupBy('videoId')
->orderBy('watch_count', 'DESC')
->limit($limit)
->get();
}
You could write a method that returns the common query:
public static function getWatchCountQuery() {
return VideoStats::select(\DB::raw('count(*) as watch_count, videoId'))
->groupBy('videoId')
->orderBy('watch_count', 'DESC');
}
And use that in your other methods:
public static function mostWatchedVideosBetween(Carbon $from, Carbon $till) {
return $this->getWatchCountQuery()
->whereBetween('createdAt', [$from, $till])
->get();
}
public static function mostWatchedVideos($limit = NULL) {
return $this->getWatchCountQuery()
->limit($limit)
->get();
}
And in my opinion that logic belongs in your model. Not in a service.
I usually use conditional Conditional Clauses. For ie, if the value/s is null, it will result in false and what is within the condition will not be taken into account:
public static function mostWatchedVideos($limit = NULL, $from = null, $till = null)
{
return VideoStats::select(\DB::raw('count(*) as watch_count, videoId'))
->groupBy('videoId')
->orderBy('watch_count', 'DESC')
->when($limit, function ($query, $limit) {
return $query->limit($limit);
})
->when(($from && $till), function ($query, $from, $till) {
return $query->whereBetween('date_column', [Carbon::parse($from), Carbon::parse($till)]);
})
->get();
}

Laravel 5.7: New Accessor works great but is not recognisable in Repository Class (unknown column) despite of adding $appends to Model

So I have a class Order extends Model.
I created an Accessor called requiresApproval that returns true or false.
public function getRequiresApprovalAttribute(): bool
{
if ($some_physical_column_from_db === 'does not matter') {
return true;
}
return false;
}
When I have my Order model and I call $order->requiresApproval I get my boolean value. Everything works great.
However I need this accessor to appear on the list of attributes because I want to use it in my repository class in where condition within query.
So based on the official documentation, I added:
protected $appends = [
'requires_approval',
];
but when I dd() my Order, this attribute is not on the list of attributes (while $appends property indicates the accessor is there).
Long story short:
When in my repository I call:
public function getOrdersEligibleToBeSendToOptions(): Collection
{
$columns = [
'*',
];
return $this->model
->where('status_uuid', '<>', OrderStatusesInterface::STATUS_COMPLETED)
->where('requiresApproval', '=', true) // this fails
// ->where('requires_approval', '=', true) // this fails as well
->get($columns);
}
I get:
What am I doing wrong? How can I use my accessor within repository class?
OK, this works, but the reason I don't like this solution is the fact that just half of the conditions are on the DB layer, the rest is by filtering what's already fetched.
If the query is going to return (let's say) thousand of records and filter returns just a few of them I personally see this as a huge waste of DB resource.
public function getOrdersEligibleToBeSendToOptions(): Collection
{
$columns = [
'*',
];
$results = $this->model
->where('status_uuid', '<>', OrderStatusesInterface::STATUS_COMPLETED)
->get($columns);
return $results->filter(function ($value, $key) {
return $value->requiresApproval === false;
});
}
Eloquent queries work on the database fields, but you can use your accessor after fetching a colletion from the database like this.
Here is some good article about this:
https://medium.com/#bvipul/laravel-accessors-and-mutators-learn-how-to-use-them-29a1e843ce85
return $this->model
->where('status_uuid', '<>', OrderStatusesInterface::STATUS_COMPLETED)
->get($columns)
->filter(function ($row) {
return $row->requires_approval === true;
});
The model virtual attributes cannot be used within queries. Perhaps a better approach would be to create a scope to enforce this constraint on a query:
class Order extends Model
{
public function scopeRequiresApproval($query)
{
return $query->where('some_column', '>', 100);
}
}
Then
return $this->model
->where('status_uuid', '<>', OrderStatusesInterface::STATUS_COMPLETED)
->requiresApproval()
->get($columns);

symfony 4 count method with doctrine-orm

I am a beginner in symfony 4, and I wanted to count the recording of an object, but it does not work.
this is my code
// Function count() in my repository class
public function count()
{
return $this->createQueryBuilder('d')
->select('count(d.codeMarche) as count')
->getQuery()
->getSingleScalarResult();
}
// in my controller class, i called my function count()
/**
* #Route("/", name="home")
*/
public function hom(MarcheDAORepository $repo){
$count = $repo->count();
return $this->render('index/home.html.twig', ['nbrDAO'=>$count]);
}
i need your help!!
The count method is already defined in the parent class so you can simply omit your implementation and use the default one (that return an int value as the doc suggest) or rename your method if you want to use a custom implementation, as example
public function countCodeMarche()
{
return $this->createQueryBuilder('d')
->select('count(d.codeMarche) as count')
->getQuery()
->getSingleScalarResult();
}
That will return a string and you should cast to int if you need a different type.
You can do that by the following code :
public function hom(MarcheDAORepository $repo){
$items = $repo->findAll();
$count=count($items);
return $this->render('index/home.html.twig', ['nbrDAO'=>$count]);
}

Eloquent scope method gives different results

I'm using Laravel 5.4
Working code
$cityWithEvents = City::with(['events' => function ($q) {
$q->whereDate('start_time', Carbon::today('America/Montreal'))->orwhereBetween('start_time', [Carbon::today('America/Montreal'), Carbon::tomorrow('America/Montreal')->addHours(4)]);
}])->where('active', 1)->get()->keyBy('id');
Not working code
$cityWithEvents = City::with('todayEventsWithAfterHoursIncluded')
->where('active', 1)
->get()
->keyBy('id');
City model
public function events() {
return $this->hasManyThrough('App\Event', 'App\Venue');
}
public function todayEventsWithAfterHoursIncluded () {
return $this->events()
->whereDate('start_time', Carbon::today('America/Montreal'))
->orwhereBetween('start_time', [
Carbon::today('America/Montreal'),
Carbon::tomorrow('America/Montreal')->addHours(4)
]);
}
Questions
When trying to create a scope method the query gives me different result. I can't see why and what should I change
I've only used scopes a few times, but never within a ->with() clause. On your City model, create a new scope:
public function scopeTodayEventsWithAfterHoursIncluded($query){
return $query->with(["events" => function($subQuery){
$subQuery->whereDate('start_time', Carbon::today('America/Montreal'))->orWhereBetween('start_time', [Carbon::today('America/Montreal'), Carbon::tomorrow('America/Montreal')->addHours(4)]);
});
}
Then, on your City query, add it as a scope function:
$cityWithEvents = City->where('active', 1)
->todayEventsWithAfterHoursIncluded()
->get();
I think the way you are using it requires that your Event model has the scope on it, as you're technically calling with("events") on your base query and your scoped one.
Let me know if this changes you results.
If you do the query, you should do it like this:
$cityWithEvents = City::withTodayEventsWithAfterHoursIncluded()
->where('active', 1)
->get()
->keyBy('id');
You scope in you model should look like this:
public function scopeWithTodayEventsWithAfterHoursIncluded ($query)
{
return $query
->with(['events' => function ($q) {$q
->whereDate('start_time', Carbon::today('America/Montreal'))
->orwhereBetween('start_time', [
Carbon::today('America/Montreal'),
Carbon::tomorrow('America/Montreal')->addHours(4)
]);
}]);
}
Now it should be equal.

Unable to assign variable value in model constructor

I am playing with Laravel models and I need one to return a value that is not in the db table but it comes by running a model method. This method runs a query that groups and count grouped results.
The model method works just fine but I don't seem to be able to pre-fill the $quantity variable within the constructor with something different than 0.
So this is an excerpt of the model:
public $quantity;
function __construct($attributes = array(), $exists = false) {
parent::__construct($attributes, $exists);
$this->quantity = $this->quantity();
}
public function quantity()
{
$query = DB::table('carts_shopping')
->select('cart_id', DB::raw('COUNT(*) AS quantity'))
->where('cart_id',$this->cart_id)
->groupBy('cart_id')
->first();
return ($query) ? $query->quantity : 0;
}
While this is how I am trying to retrieve the results from controller:
$cartitems = Auth::user()->cartshopping;
foreach ($cartitems as $cartitem)
{
echo $cartitem->name;
echo $cartitem->quantity;
}
As you may guess 'cartshopping' comes from the user model being related with the model excerpt I pasted.
I also noticed that quantity() method gets called and it returns 0 all the time as if $this->cart_id was empty and, changing $this-cart_id with a real value the query itself doesn't even get executed.
Thanks a lot for any suggestion you guys can share.
Have you tried accessing the properties using $this->attributes?
public $quantity;
function __construct($attributes = array(), $exists = false) {
parent::__construct($attributes, $exists);
$this->quantity = $this->quantity();
}
public function quantity() {
$query = DB::table('carts_shopping')
->select('cart_id', DB::raw('COUNT(*) AS quantity'))
->where('cart_id', $this->attributes['cart_id'])
->groupBy('cart_id')
->first();
return ($query) ? $query->quantity : 0;
}
Failing that, you could try using the Eloquent accessors, which would be the best way to do it. This would make it dynamic as well, which could be useful.
class YourModel {
// Normal model data here
public function getQuantityAttribute() {
$query = DB::table('carts_shopping')
->select('cart_id', DB::raw('COUNT(*) AS quantity'))
->where('cart_id', $this->attributes['cart_id'])
->groupBy('cart_id')
->first();
return ($query) ? $query->quantity : 0;
}
}

Categories