4
votes

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.

1
You want the courses in the selected university. You can drop Univerisity completely from the query if you want. Give this a shot change $query->where('id', $request->input('university_id')); to $query->where('university_id', $request->input('university_id'));mdamia

1 Answers

0
votes

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