Eloquent - apply value from relationship to where in scope - laravel

So I have this need to check if a customer needs to be called. Customers has to be called at intervals depending on a value days_between_calls in a BelongsTo model called SubscriberType. I got it to work but I don't like it, maybe there is a cleaner way.
So I have a model Subscription with relations :
public function subscriberType()
{
return $this->belongsTo(SubscriberType::class);
}
public function calls()
{
return $this->hasMany(Call::class);
}
and a (very simplified) scope :
public function scopeNeedsCall(Builder $query) {
$query->join('subscriber_types', 'subscriber_types.id', '=', 'subscriptions.subscriber_type_id')
->whereDoesntHave('calls', function(Builder $query) {
$query->whereRaw('calls.created_at > DATE_SUB(NOW(), INTERVAL days_between_calls DAY)');
});
}
Is there any cleaner way to use this days_between_calls field's value without manually joining its table and without writing raw sql?
Thanks ahead.

So it looks like there is not much that can be improved, and I do need a rawsql part here. I improved it a little anyway using https://laravel.com/docs/9.x/eloquent-relationships#has-one-of-many but that's about it.

Related

Laravel - What's the benefit of calling query() method on an Eloquent model

In the course "Eloquent Performance Patterns" by Jonathan Reinink I've seen him calling query() method on his Eloquent models before writing the actual query. Let me give an example:
User::query()
->with('company')
->paginate()
As far as I know, he could write:
User::with('company')
->paginate()
I've seen this practice over and over in his course. It raised a question in my head: Is there any benefit in doing so or is it just a personal preference?
There is no difference, you don't have to add query(). It was practical to instantiate the query for the cases similar to this;
$user = User::query();
if (true) { // check some condition and append query condition(s)
$user->with('company');
}
return $user->paginate();
but then when method came and it became easier to do that;
return User::query()
->when(true, function (Builder $builder) {
$builder->with('company');
})
->paginate();

Query Builder filter for multi level deep relationship in Laravel

I have a selection of plots which each belong to a development by a hasManyThrough relationship through housetypes. I want to filter these by development on their overview page. Plots has a housetype_id column and housetypes has a development_id column.
public function plots()
{
return $this->hasManyThrough(Plot::class, Housetype::class);
}
When I use my filter it returns the developments ID number as $development, I then need this to only show plots which are linked to that development.
I have looked into using whereHas or Join methods but have been unable to figure this out. Current filter scope is below. Thanks
public function scopeFilterDevelopment($query)
{
$development = request()->input('filter_development');
if ($development == "") {
return;
}
if(!empty($development)){
$query->where('development_id', $development);
}
}
If I can understand it right you wish to assert a condition on other Model, HasMany will load all the objects to the related model once the query is completed. Eloquent then binds the related model objects to each.
Try joins from Laravel instead. I feel this is what you exactly want: https://laravel.com/docs/5.8/queries#joins
I would use whereHas to filter the relationship:
YourModel::whereHas('plots', function($query) {
$query->filterDevelopment();
})->get();
I would also edit the query scope not to rely on the request global function, but instead pass the development of value as a parameter.
you have make a leftjon and then use when, you dont have to use
if(!empty($development)){
$query->where('development_id', $development);
}
this any more, you can use
->when($development=="" ? false : true, function($query) use ($development){
return $query->where('development_id', $development);
})
this is a full example
$queryBuilder = DB::table('facturas')->
leftJoin('clientes','clientes.id','=','facturas.clientes_id')->
select('facturas.estados_id as estado','facturas.numero as
numero',DB::raw('concat(clientes.nombre," ",clientes.apellido) as cliente'))->
when($estados===null ? false: true,function($query) use ($estados){
return $query->whereIn('facturas.estados_id', $estados);
})
It was a whereHas that solved this in the end! (another developer at work walked me through this)
Relationship -
public function housetype()
{
return $this->belongsTo(Housetype::class);
}
Function -
public function scopeFilterDevelopment($query)
{
if (request()->input('filter_development') == "") {
return;
}else{
$query->whereHas('housetype', function($housetype){
$housetype->where('development_id', request()->input('filter_development'));
});
}
}
This then returns any plot where its housetype has a matching development_id for the filter_development from the request.
Thanks for everyone's input

Best Practice - Laravel Controller Eloquent merge

I have a scope on my Supplier model that returns results where active = true.
This works great when creating new entries, as I only want the user to see active suppliers.
Current entries may have an inactive supplier; When I edit it, I want to see all active Suppliers, plus the current supplier (if it is inactive)
I have this code in my controller:
$suppliers = Supplier::active()->get();
if (!$suppliers->contains('id', $record->supplier->id))
{
$suppliers->add(Supplier::find($record->supplier->id));
}
Two questions: Is this the correct way to do this? Should this code be in my controller or should I have it somewhere else? (perhaps a scope but I wouldn't know how to code that).
Edit:
Thanks for the help guys. I have applied advice from each of the answers and refactored my code into a new scope:
public function scopeActiveIncluding($query, Model $model = null)
{
$query->where('active', 1);
if ($model && !$model->supplier->active)
{
$query->orWhere('id', $model->supplier->id);
}
}
What you've written will work, but the Collection::contains function can potentially be pretty slow if the collection is large.
Since you have the id, I would probably make the following change:
$suppliers = Supplier::active()->get();
$supplier = Supplier::find($record->supplier->id);
if (!$supplier->active) {
$suppliers->add($supplier);
}
Of course, the downside to this is that you may be making an unnecessary query on the database.
So you have to consider:
is the record's supplier more likely to be active or inactive?
is the size of the collection of active suppliers large enough to justify another (potentially wasted) call to the database?
Make the choice that makes the most sense, based on what you know of your application's data.
As for the second question, if you will only need this specific set of suppliers in this one part of your application, then the controller is a good place for this code.
If, however, you will need this particular set of suppliers in other parts of your application, you should probably move this code elsewhere. In that case, it might make sense to create a function on the the related model (whatever type $record is...) that returns that model's suppliers set. Something like:
public function getSuppliers()
{
$suppliers = Supplier::active()->get();
$supplier = $this->supplier;
if (!$supplier->active) {
$suppliers->add($supplier);
}
return $suppliers;
}
I saw #Vince's answer about 1st question, and I'm agree with him.
About 2nd question:
Write scope in Supplier model like this:
public function scopeActive($query){
$query->where('active', 1); // for boolean type
}
For good practice, you need to write the logic parts in services like "App\Services\SupplierService.php". And there write the function you want:
public function activeSuppliersWithCurrent($record) {
$suppliers = Supplier::active()->get();
$supplier = Supplier::find($record->supplier->id);
if (!$supplier->active) {
$suppliers->add($supplier);
}
}
In your SupplierController's constructor inject the instance of that service and use the function, for example:
use App\Servives\SupplierService;
protected $supplierService = null;
public function __construct(SupplierService $supplierService) {
$this->supplierService = $supplierService;
}
public function getActiveSuppliersWithCurrent(...) {
$result = $this->supplierService->activeSuppliersWithCurrent($record);
}
As you can see, later you will not need to change anything in controller. If you'll need to change for example the query of suppliers selection, you will just have to change something only in service. This way will make your code blocks separated and shorter.
Also the sense for this pattern: you don't need to access the models from controller. All logic related with models will implemented in services.
For other projects you can grab only services or only controllers, and implement another part differently. But in that case if you had all codes in controller, that will prevent you to grab the portions of necessary codes, cuz may you don't remember what doing each blocks...
You could add a where clause to the query to also find that id.
$suppliers = Supplier::active()->orWhere('id', $record->supplier->id)->get();
You could potentially slide this into the active scope by passing the 'id' as an argument.
public function scopeActive($query, $id = null)
{
$query->where('active', true);
if ($id) {
$query->orWhere('id', $id);
}
}
Supplier::active($record->supplier->id)->get();
Or make another scope that does this.

Laravel 5 hasMany relation returning incorrect relations

I have a Yard model and Treatment model, I am using the following hasMany relationship to return treatments that are currently active:
public function activeTreatments() {
return $this->hasMany('App\Models\Treatment')
->where(function ($q) {
$q->where('expires_at','>=', Carbon::now('Pacific/Auckland'))
->where('completed_at','>', Carbon::now('Pacific/Auckland'));
})
->orWhere('completed',false);
}
For some reason when I add the ->orWhere('completed',false) the query returns all treatments not just the treatments associated with the specific yard. What am I doing wrong here?
It's hard to say exactly what is going on without inspecting the SQL being generated.
Wherever you are using this code, you could chain a toSql() on the end to see what the query looks like (use this where you would use a get()). Or you could enable the query log to see what is being queried.
Given the symptoms, it is likely that the orWhere() is negating a condition used to filter the models.
Try nest the orWhere() inside the current where() statement:
public function activeTreatments() {
return $this->hasMany('App\Models\Treatment')
->where(function ($q) {
$q->where(function($q), {
$q->where('expires_at','>=', Carbon::now('Pacific/Auckland'))
->where('completed_at','>', Carbon::now('Pacific/Auckland'));
})->orWhere('completed',false);
});
}

Filtering eager-loaded data in Laravel 4

I have the following setup:
Clubs offer Activities, which are of a particular Type, so 3 models with relationships:
Club:
function activities()
{
return $this->hasMany('Activity');
}
Activity:
function club()
{
return $this->belongsTo('Club');
}
function activityType()
{
return $this->hasMany('ActivityType');
}
ActivityType:
function activities()
{
return $this->belongsToMany('Activity');
}
So for example Club Foo might have a single Activity called 'Triathlon' and that Activity has ActivityTypes 'Swimming', 'Running', and 'Cycling'.
This is all fair enough but I need to show a list of ActivityTypes on the Club page - basically just a list. So I need to get the ActivityTypes of all the related Activities.
I can do that like so from a controller method that receives an instance of the Club model:
$data = $this->club->with(array('activities', 'activities.activityTypes'))->find($club->id)
That gets me an object with all the related Activities along with the ActivityTypes related to them. Also fair enough. But I need to apply some more filtering. An Activity might not be in the right status (it could be in the DB as a draft entry or expired), so I need to be able to only get the ActivityTypes of the Activities that are live and in the future.
At this point I'm lost... does anybody have any suggestions for handling this use case?
Thanks
To filter, you can use where() as in the fluent DB queries:
$data = Club::with(array('activities' => function($query)
{
$query->where('activity_start', '>', DB::raw('current_time'));
}))->activityType()->get();
The example which served as inspiration for this is in the laravel docs, check the end of this section: http://laravel.com/docs/eloquent#eager-loading
(the code's not tested, and I've taken some liberties with the property names! :) )
I think if you first constraint your relationship of activities, the activity types related to them will be automatically constrained as well.
So what I would do is
function activities()
{
return $this->belongsToMany('Activity')->where('status', '=', 'active');
}
and then your
$data = $this->club->with(array('activities', 'activities.activityTypes'))->find($club->id)`
query will be working as you would expect.

Resources