Laravel Nested Eager Loading with Constraints - php

I have a Question Eloquent Model, a Course Eloquent Model, a University Eloquent Model. A One to Many relationship exists between the University and the Course. A Many to Many relationship exists between the Question and the Course. The Three models are shown below:
Question Model
namespace App;
use Illuminate\Database\Eloquent\Model;
class Question extends Model
{
/**
* The database table that the Model uses
* #var string
*/
protected $table = "questions";
/**
* The fields that are mass assignable
* #var array
*/
protected $fillable = ['title','body','images','quality_score','deactivated','creator_id','reviewer_id'];
/**
* Images is stored as serialized json.
* So we cast it to a PHP array.
* See: http://laravel.com/docs/5.1/eloquent-mutators#attribute-casting
*/
protected $casts = [
'images' => 'array',
];
public function courses(){
return $this->belongsToMany('App\Course');
}
}
Course Model
namespace App;
use Illuminate\Database\Eloquent\Model;
class Course extends Model
{
/**
* The database table used by the model
* #var string
*/
protected $table = "courses";
/**
* The fields that can be mass assigned
* #var array
*/
protected $fillable = ['name', 'instructor', 'acronym', 'university_id', 'creator_id', 'reviewer_id'];
/**
* There exists a many to one relationship between the Course and User
* This user is the creator of the course
*
* #method void
*
*/
public function creator(){
return $this->hasOne('App\User','creator_id');
}
/**
* There exists a many to one relationship between the Course and User
* This user is the reviewer of the course
* The reviewer of the Course will always be an admin
* If an Admin is the creator, then the reviewer is also the same admin
*
* #method void
*/
public function reviewer(){
return $this->hasOne('App\User','reviewer_id');
}
/**
* There exists a one to many relationship between the University and the Course
* This university is where the course is held
* Courses may float i.e. not be associated to any university
*
* #method void
*/
public function university(){
return $this->belongsTo('App\University');
}
/**
* This method is an accessor. It automatically changes the acronym to be all capitals
* regardless of how it is stored in the database.
* See: http://laravel.com/docs/5.1/eloquent-mutators#accessors-and-mutators
* #param $value (String from Database)
* #return string (Capitalized String)
*/
public function getAcronymAttribute($value){
return strtoupper($value);
}
}
University Model
namespace App;
use Illuminate\Database\Eloquent\Model;
class University extends Model
{
/**
* The database table used by the model
* #var string
*/
protected $table = "universities";
/**
* The fields that can be mass assigned
* name = Name of the University (Example: University of Illinois at Urbana Champaign)
* acronym = Acronym of the University (Example: UIUC)
* creator_id = Id of User that created the University
* reviewer_id = Id of User that reviewed and approved the University
*
* Universities will not be displayed to users without admin role unless they have been reviewed.
*
* #var array
*/
protected $fillable = ['name','acronym','creator_id','reviewer_id'];
/**
* This method is an accessor. It automatically changes the acronym to be all capitals
* regardless of how it is stored in the database.
* See: http://laravel.com/docs/5.1/eloquent-mutators#accessors-and-mutators
* #param $value (String from Database)
* #return string (Capitalized String)
*/
public function getAcronymAttribute($value){
return strtoupper($value);
}
}
On my home page I am showing a list of questions and allow filters for course and university. The controller method is shown here:
public function getHome(Request $request){
/**
* Eager Load with Course and University
*/
$questions = Question::with('courses.university')->get();
/*
* Filter Questions to remove unwanted entries based on course id
*/
if($request->has('course_id') && $request->input('course_id') != -1){
$questions = $questions->filter(function($question) use ($request){
foreach($question->courses as $course){
if ($course->id == $request->input('course_id')){
return true;
}
}
});
}
/*
* Filter Questions to remove unwanted entries based on university id
*/
if($request->has('university_id') && $request->input('university_id') != -1){
$questions = $questions->filter(function($question) use ($request){
foreach($question->courses as $course){
if ($course->university->id == $request->input('university_id')){
return true;
}
}
});
}
/*
* Return the Welcome View with Pagination on the Questions Displayed
* List of Courses and List of Universities
*/
return view('welcome',[
'questions' => $questions,
'courses' => Course::all(),
'universities' => University::all(),
'selected_university_id' => $request->input('university_id',-1),
'selected_course_id' => $request->input('course_id',-1)
]);
}
What I am doing above is returning all the questions from the database and them combing them through to remove all the ones that don't match the filters. This is obviously quite inefficient. I want to use Nested Eager Loading with constraints except I am having a lot of trouble defining what the constraint would look like. Further, I want to use server side paginate to make the client experience better on lower speed internet connections.
Here is one of my attempts:
$questions = Question::with(['courses.university' => function($query) use ($request){
if($request->has('university_id') && $request->input('university_id') != -1) {
$query->where('id', $request->input('university_id'));
}
if($request->has('course_id') && $request->input('course_id') != -1){
$query->where('courses.id',$request->input('course_id'));
}
}])->paginate(10);
This works fine when I don't have any filters.
When I do have a university_id defined, I get the error: Trying to get property of non-object (View: /var/www/testing.com/resources/views/welcome.blade.php)
When I do have a course_id defined, I get the error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'courses.id' in 'where clause' (SQL: select * from universities where universities.id in (1, 2) and courses.id = 1)
I expected the error when I have course_id defined (because I took a blind stab at the first argument of the $query->where method.
I am looking for help in defining the Nested Eager Loading Constraints.

I found the solution in a medium article. The solution works for later versions of laravel as it uses whereHas.
// If you want to put the constraint on the second relation
$questions = Question::with(['courses' => function($query) use($request){
return $query->whereHas('university', function($inner_query) use($request){
return $inner_query->where('id', $request->input('university_id'));
});
}, 'courses.university'])->paginate(10);
For your case, a simple whereHas ought to do the trick.
$questions = Question::whereHas('courses', function($query) use ($request){
return $query->where('university_id', $request->input('university_id'));
})->with(['courses.university'])->paginate(10);
I would also recommend using when clauses to reduce the amount of code.
$questions = Question::when(($request->has('course_id') && $request->input('course_id') != -1), function ($query) use($request){
return $query->where('course_id', $request->input('course_id'));
})->when($request->has('university_id') && $request->input('university_id') != -1, function ($outer_query) use($request){
return $outer_query->whereHas('courses', function($query) use($request){
return $query->where('university_id', $request->input('university_id'));
})->with(['courses.university']);
})->with(['courses.university'])->paginate(10);

Related

Laravel 9 sub-query target linked model columns in where clause

I'm working inside a Laravel 9 project and am trying to perform a query and return my Monitor model where the User role of the monitor is a customer (eventually adding more fields).
My current query attempt throws an error:
Property [user] does not exist on the Eloquent builder instance.
What am I missing?
/**
* Get inactive users
*
* #return array
*/
protected function getInactiveUsersWithMonitors()
{
$monitors = Monitor::with('user')->where(function ($query) {
$query->user->where('role', 'customer');
})->get();
return $monitors;
}
If you have already defined the user function in the Monitor model. Here is what you should do.
/**
* Get inactive users
*
* #return array
*/
protected function getInactiveUsersWithMonitors()
{
/* $monitors = Monitor::with('user')->where(function ($query) {
$query->user->where('role', 'customer');
})->get(); */
$monitors = Monitor::whereHas('user', function($query)
{
$query->where('role', 'customer');
})->get();
return $monitors;
}
This will get all the details from the monitors table as well as the corresponding details from users table.

Laravel nested resource retrieve model by slug instead of id

I'm working on a Laravel 8 project, it's being used as an API for a frontend. My API contains Brands and Forms, a brand is created by a user and a brand can have a form.
My brand schema contains a slug column, it's this column that's present in my front-end URLs, the slug column is unique, e.g:
/account/brands/my-brand/forms/
/account/brands/my-brand/forms/create/
A form has a brand_id column, this is later used as part of Laralve's hasOne and hasMany relationship for automatic joining since it's easier this way and means I don't have to have an ugly URL.
The trouble I'm having is when I want to show the user a list of their forms for the brand they're on I don't have access to the brand_id in the request, only the slug as this is part of the URL, whereas to retrieve my forms I need the brand_id.
How could I easily (without having another function) obtain this whilst still ensuring my relationships in my model work correctly?
Retrieving forms for a brand that belong to a user
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index($brand)
{
// $brand -> this is the slug, unsure how to utilise this on the joined brand
$forms = Form::where('user_id', Auth::id())->with('brand')->get();
return response()->json([
'forms' => $forms
], 200);
}
My Brand model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Brand extends Model
{
use HasFactory, SoftDeletes;
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'brands';
/**
* The attributes that are mass assignable.
*
* #var string[]
*/
protected $fillable = [
'uuid',
'brand',
'url',
'telephone',
'link_terms',
'link_privacy',
'seo_description',
'base64_logo',
'base64_favicon',
'text_marketing',
'text_promos',
'text_broker',
'text_footer_1',
'text_footer_2',
'text_credit_disclaimer',
'analytics_internal_affiliate'
];
/**
* The relationships that should always be loaded.
*
* #var array
*/
protected $with = [
'form'
];
/**
* Get the form associated with the user.
*/
public function form()
{
return $this->hasOne(Form::class);
}
/**
* Get the brand that owns the comment.
*/
public function brand()
{
return $this->belongsTo(User::class);
}
}
My Form model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Form extends Model
{
use HasFactory, SoftDeletes;
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'forms';
/**
* The attributes that are mass assignable.
*
* #var string[]
*/
protected $fillable = [
'name',
'loan_amount',
'loan_min_amount',
'loan_max_amount',
'loan_term',
'loan_min_term',
'loan_max_term'
];
/**
* Get the brand that owns the comment.
*/
public function brand()
{
return $this->belongsTo(Brand::class);
}
}
The path of least resistance would be to harness the whereRelationship() method on your query.
I believe that would look like this:
$forms = Form::where('user_id', Auth::id())
->whereRelationship('brand', '=', $brand)
->with('brand')
->get();
https://laravel.com/docs/master/eloquent-relationships
An alternative which would be more work to set up but would likely make your life easier long term would be to use route model binding. https://laravel.com/docs/master/routing#route-model-binding
You can ask Laravel's IoC container to resolve the full record for you by typehinting it and naming the variable correctly in the method signature.
Since you are using a column other than id to identify it, you need to inform Laravel of this. There are a couple of different ways to do this, discussed at https://laravel.com/docs/master/routing#customizing-the-default-key-name
Then, you can ask Laravel's IoC container to build an instance of the model by searching in the database for you right in the method signature. You simply need to typehint the parameter and ensure it's name matches that in the route definition.

Laravel Pagination / Count - Not Accurate AFTER Filters Applied

When I retrieve the count for my results via laravel's paginator ($var->total() function) OR adding on ->count() to my DB query, it returns the incorrect count. Even when there is ZERO items on the page after a filter is applied, it still shows the incorrect number. This number stays the same, it doesn't change - it's the total # of rows in my parent model. However, again - it shouldn't show the total # of rows in my parent model - it should show the rows that are available after a filter is applied.
Filtered Query in Question:
$orders = list_formstack::with(['order' => function($query) use ($checktv, $dstart, $dend, $thestatus, $pharmacy, $searchid, $searchsubid, $searchrefill, $searchgrams, $filterrep) {
$query->whereNotNull('subid');
if($checktv == "No") {
$query->whereNull('intake');
}
elseif($checktv == "Yes") {
$query->whereNotNull('intake');
}
if($dstart && $dend)
$query->whereBetween('orders.created_at', [$dstart, $dend]);
if($thestatus)
$query->where('orders.status', $thestatus);
if($pharmacy)
$query->where('pharmacy', $pharmacy);
if($searchid)
$query->where('orders.id', $searchid);
if($searchsubid)
$query->where('orders.subid', $searchsubid);
if($searchrefill)
$query->where('refill', $searchrefill);
if($searchgrams)
$query->where('grams', $searchgrams);
if($filterrep)
$query->where('salesrep', $filterrep);
}])->where(function($query) use ($prodgroup, $state, $search, $columns, $searchpatient, $searchcarrier) {
if($state)
$query->where('state', $state);
if($searchpatient)
$query->where('first', 'like', $searchpatient)->orWhere('last', $searchpatient)->orWhere('phone', $searchpatient);
if($searchcarrier)
$query->where('insurancecarrier', 'like', $searchcarrier);
})->orderBy('created_at', 'desc')->paginate(50);
Parent Model:
class list_formstack extends Model
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'formstack';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['parentid','first','last','state','insurancecarrier'];
/**
* Get the form for an order
*/
public function order()
{
return $this->hasMany('App\list_orders', 'formid', 'id')->groupBy('id');
}
}
Child / Relationship Model:
class list_orders extends Model
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'orders';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['id','status','subid','formid','salesrep','billedamt','copayamt','completed','refillcount','gramsleft','billedamt','copayamt','salesrep','intake','created_at'];
public function formstack()
{
return $this->belongsTo('App\list_formstack', 'orderid');
}
}
So as you can see in my Eloquent query, I filter out results based on whether a user chooses that filter or not. So if I filter out "thestatus" to one specific status, it shows the correct results for only that status HOWEVER the count DOES NOT change.
I even tried to throw the results into a collection:
$count = collect($orders);
$count->count();
No luck - same incorrect number.
Any insight on this? What am I doing wrong? Thanks in advance!
EDIT: I should note that using Laravel's Query Builder (NO Eloquent ORM Models) results in the pagination count working correctly.
Parent Model DB Table
Parent Model Table Structure
Child Model DB Table
Child Model Table Structure

Laravel 5.2: Retrieving list of unique values in a many to many relationship

So I have a pension type:
class Type extends Base
{
use RecordStatusActiveTrait;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'pn_pension_type';
/**
* The actual primary key for the model.
*
* #var string
*/
protected $primaryKey = 'pension_type_id';
// Belongs To -----
public function pensionClass() {
return $this->belongsTo('App\Models\Control\PensionClass', 'pension_class_id');
}
// Belongs To Many -----
public function qualifiers()
{
return $this->belongsToMany('App\Models\Pension\Qualifier', 'pn_pension_type_qualifier', 'type_id', 'qualifier_id');
}
}
And a pension Qualifier:
class Qualifier extends Base
{
use RecordStatusActiveTrait;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'pn_pension_qualifier';
/**
* The attributes that should be casted to native types.
*
* #var array
*/
protected $casts = [
'rules' => 'array',
];
// Belongs To Many -----
public function types()
{
return $this->belongsToMany('App\Models\Pension\Type', 'pn_pension_type_qualifier', 'qualifier_id', 'type_id');
}
}
They have a many to many relationship with a table in between as normal. I would like to grab a set of pension types and find the unique Qualifiers between all of them. I COULD just foreach through each of their qualifiers and end up with a list of unique qualifiers but I was wondering if their was an "Eloquent" way.
Using whereHas will allow you to restrict the results based on the relation. In regards to what you want, you'd want to query from your Qualifiers like so.
Qualifiers::whereHas('pensions', function($query) use ($pensionIds) {
$query->whereIn('id', $pensionIds);
})->distinct()->get();
Adding on ->distinct() will then get you the unique entries.

How to implement a two-level relationship in Laravel 4?

I try to get the city name from an entity. I get my entity like this :
$entity = Entity::find(1);
And I would like to get the city name like this :
$entity->addresses->cities->name
But it doesn't work ? I tried multiple ways but impossible to get the city corresponding to the entity.
Here is my model :
Entity :
class Entity extends Eloquent {
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'entities';
public function addresses ()
{
return $this->hasMany('Address');
}
Address :
class Address extends Eloquent {
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'addresses';
public $timestamps = false;
public function cities ()
{
return $this->belongsTo('City');
}
public function entities ()
{
return $this->belongsTo('Entity');
}
City :
class City extends Eloquent {
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'cities';
public function addresses ()
{
return $this->hasMany('City');
}
Do I have to do an intermediate manipulation ? Or do I have to add something in my model ?
I have this error :
Undefined property: Illuminate\Database\Eloquent\Collection::$cities (View: /Applications/MAMP/htdocs/project/app/views/administrator/general.blade.php)
Thank you for your help
In your City model, I believe the addresses() function should return $this->hasMany('Address');
Also, there is some errors in how you are traversing the results of your find query.
Remember one Entity has many addresses, so if you are trying to do $entity->address->city, it has no idea which address you are talking about. You will have to loop through them.
Another thing I like to do is not pluralize all your functions. Just make the ones that will return many things plural. For example, one address will only ever have one city (how can one address belong to multiple cities?) It would be most helpful to change the function name to city() then. One city can have many addresses, so you'd want to have an addresses() function in your City model.
With that in mind, the code that should work for you is...
foreach($entity->addresses as $address) {
echo $address->city->name;
}
Since every entity can have multiple addresses and $entity->addresses() returns a Collection-object, you cant directly get all cities. You have to loop through all addresses and get the corresponding cities.
$cities = array();
foreach( $entity->addresses() as $address ) {
$cities[] = $address->city;
}

Categories