How to create a three way pivot using Eloquent ORM - php

Very new to Laravel but am loving what I am seeing so far.
I am working on an application where Clients attend Sessions and Sessions can be attended by many Clients. In addition to this, Clients may also complete Assessments in Sessions they attend on an individual basis (i.e. one client = one assessment). Also worth pointing out that they may or may not complete an assessment.
Best way to explain this is...
It's also important to point out that the data must be consistent - a completed assessment must be done in a session where a client was present. i.e. the client must have been present at the session - the same is true of the inverse - a client can't have completed and assessment for a session he or she was not present at.
This is more of trying to wrap my head around the best structure for this and use the Eloquent ORM in Laravel. I know I can make a working solution to this which definitely will be garbled and messy, but I would rather know the correct way.
Some pertinent code:
class Client extends Model
{
public function sessions()
{
return $this->hasMany('App\Session');
}
public function assessments()
{
return $this->hasMany('App\Assessment');
}
}
class Session extends Model
{
public function clients()
{
return $this->hasMany('App\Client');
}
public function assessments()
{
return $this->hasMany('App\Assessment');
}
}
//This is where I am having difficulty...
class Assessment extends Model
{
public function client()
{
return $this->hasOne('App\Client');
}
public function session()
{
return $this->hasOne('App\Session');
}
}
The issue I face is how to create this three way relationship between "Client", "Session" and "Assessment" in such a way that integrity is enforced and access is available.
Few things I have thought about:
Create a "client_session" pivot table and add an additional field to it called "assessment_id" and use this to bind an assessment to a unique combination of client and session. I'm not sure how (or even if it is possible) to define such a relationship to a table without a model.
Use "Assessments" as the pivot table. I don't really like this idea as it is possible that no assessments take place but I still need that relationship between client and session and storing that in "Assessments" feels wrong.
Create a separate table "Attendance" and an associated "pivot class" i.e. extends Pivot not Model - really unsure about this one!
I have managed to structure the data using simple tables and use INNER JOINs away from Laravel but wanted to understand how best to implement this with Eloquent.
I've also seen the belongsToMany() relationship using a Pivot (i.e. extends Pivot rather than Model) or the hasManyThrough() relationship.
I also saw https://github.com/jarektkaczyk/Eloquent-triple-pivot but this hasn't been maintained in 4 years so I assume this can be done with whatever ships with Eloquent/Laravel.
Any help, greatly appreciated!
**** UPDATE ****
Okay, so #barghouthi has help massively with my understanding. Currently my models look like the following:
class Client extends Model
{
public function sessions()
{
return $this->belongsToMany('App\Session', 'attendance')->using('App\Attendance');
}
}
class Session extends Model
{
public function clients()
{
return $this->belongsToMany('App\Client', 'attendance')->using('App\Attendance');
}
}
class Attendance extends Pivot
{
public function client()
{
return $this->belongsTo('App\Client', 'attendance');
}
public function session()
{
return $this->belongsTo('App\Session', 'attendance');
}
public function assessment()
{
return $this->hasMany('App\Assessment','attendance');
}
}
class Assessment extends Model
{
public function attendance()
{
return $this->belongsTo('App\Attendance');
}
public function client()
{
// for now
return $this->attendance()->client();
}
public function session()
{
// for now
return $this->attendance()->session();
}
}
I've purposefully renamed the pivot table so that reflects the Pivot Class.
I've put some dummy data into the attendance table such that:
When I query the client sessions for client ID 1 this returns four entries rather than two. Four is correct in terms of the attendance table but I would like to have only two records returned - i.e. the sessions.
**** UPDATE
Okay - so a lot of learning and I'm not quite there. This is my current solution:
class Client extends Model
{
public function sessions()
{
return $this->belongsToMany('App\Session', 'attendance')->using('App\Attendance')->distinct('session_id');
}
public function assessments()
{
return $this->hasManyThrough('\App\Assessment', '\App\Attendance', 'client_id', 'id');
}
}
class Session extends Model
{
public function clients()
{
return $this->belongsToMany('App\Client', 'attendance')->using('App\Attendance')->distinct('client_id');
}
public function assessments()
{
return $this->hasManyThrough('\App\Assessment', '\App\Attendance', 'session_id', 'id');
}
}
class Assessment extends Model
{
public function client()
{
return $this->belongsToMany('\App\Client', 'attendance', 'id', 'client_id')->using('\App\Attendance');
}
public function session()
{
return $this->belongsToMany('\App\Session', 'attendance', 'id', 'session_id')->using('\App\Attendance');
}
}
The attendance table simply has id, client_id and session_id. I figured there was no need for assessment_id as there is a one to one relationship (i.e. an assessment(s) may or not be carried out by a client at a particualr session)
class Attendance extends Pivot
{
public function clients()
{
return $this->belongsTo('App\Client', 'attendance');
}
public function sessions()
{
return $this->belongsTo('App\Session', 'attendance');
}
public function assessments()
{
return $this->belongsTo('App\Assessment','attendance');
}
}
This seems to to do the trick apart for the most part. If I try and call
$assessment->client->id
I get the follwing:
Property [id] does not exist on this collection instance.
Which means my relationships in Assessment are not quite right if I want to read the properties of related elements.
When I use
$assessment->client
The output is
[{"id":1,"created_at":"2018-10-24 19:15:41","updated_at":"2018-10-24 19:15:41","date_of_birth":"26 Sep 1974","gender":1,"initial_contact_date":"2013-05-01","initial_session_offered_date":"2002-07-06","contact_from":1,"presentation":"Vitae qui et nesciunt iste autem numquam earum. Illo repudiandae deleniti vel nesciunt non iure. Sunt est nemo ut excepturi illum temporibus.","counsellor_id":1,"pivot":{"id":1,"client_id":1}}]
So I am guessing I am close.

so if you have client_session or attendance table with id, client_id, session_id and you use this table as foreign key in assessments table.
The thing is the pivot table can be a model
see Many to Many : Defining Custom Intermediate Table Models
class Client extends Model
{
public function sessions()
{
// using intermediate model
return $this->belongsToMany('App\Session')->using('App\Attendance');
}
}
class Session extends Model
{
public function clients()
{
// using intermediate model
return $this->belongsToMany('App\Session')->using('App\Attendance');
}
}
class Attendance extends Pivot
{
public function clients()
{
return $this->belongsTo('App\Client');
}
public function session()
{
return $this->belongsTo('App\Session');
}
public function Assessments()
{
return $this->hasMany('App\Assessment');
}
}
class Assessment extends Model
{
public function attendance()
{
return $this->belongsTo('App\Attendance');
}
public function client()
{
// for now
return $this->attendance()->client();
}
public function session()
{
// for now
return $this->attendance()->client();
}
}
so this way you can't have an assessment without an attendance and an
attendance can have many assessments.
Assessment would look like:
id, attendance_id: refers to client and sessions

Related

Laravel hasOne relation with where clause

I have a model called RealEstate, this model has a relation with another model called TokenPrice, I needed to access the oldest records of token_prices table using by a simple hasOne relation, So I did it and now my relation method is like following:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
class RealEstate extends Model
{
public function firstTokenPrice(): HasOne
{
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
}
By far it's fine and no complexity. But now, I need to involve another relation into firstTokenPrice.
Let me explain a bit more:
As my project grown, the more complexity was added it, like changing firstTokenPrice using by a third table called opening_prices, so I added a new relation to RealEstate called lastOpeningPrice:
public function lastOpeningPrice(): HasOne
{
return $this->hasOne(OpeningPrice::class)->latestOfMany();
}
So the deal with simplicity of firstTokenPrice relation is now off the table, I want to do something like following every time a RealEstate object calls for its firstTokenPrice:
Check for lastOpeningPrice, if it was exists, then firstTokenPrice must returns a different record of token_price table, otherwise the firstTokenPrice must returns oldestOfMany of TokenPrice model.
I did something like following but it's not working:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
class RealEstate extends Model
{
public function lastOpeningPrice(): HasOne
{
return $this->hasOne(OpeningPrice::class)->latestOfMany();
}
public function firstTokenPrice(): HasOne
{
$lop = $this->lastOpeningPrice;
if ($lop) {
TokenPriceHelper::getOrCreateFirstToken($this, $lop->amount); // this is just a helper function that inserts a new token price into `token_prices` table, if there was none exists already with selected amount
return $this->hasOne(TokenPrice::class)->where('amount', $lop->amount)->oldestOfMany();
}
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
}
I have checked the $this->hasOne(TokenPrice::class)->where('amount', $lop->amount)->oldestOfMany() using by ->toSql() method and it returns something unusual.
I need to return a HasOne object inside of firstTokenPrice method.
You can use ofMany builder for that purpose:
public function firstTokenPrice(): HasOne
{
$lop = $this->lastOpeningPrice;
if ($lop) {
TokenPriceHelper::getOrCreateFirstToken($this, $lop->amount); // this is just a helper function that inserts a new token price into `token_prices` table, if there was none exists already with selected amount
return $this->hasOne(TokenPrice::class)->ofMany([
'id' => 'min',
], function ($query) use ($lop) {
$query->where('amount', $lop->amount);
});
}
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
I used ->oldest() with a custom scope called amounted in TokenPrice model:
class TokenPrice extends Model
{
public function scopeAmounted(Builder $query, OpeningPrice $openingPrice): Builder
{
return $query->where('amount', $openingPrice->amount);
}
/....
}
And then changed my firstTokenPrice
public function firstTokenPrice(): HasOne
{
$lop = $this->lastOpeningPrice;
if ($lop) {
TokenPriceHelper::getOrCreateFirstToken($this, $lop->amount);
return $this->hasOne(TokenPrice::class)->amounted($lop)->oldest();
}
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
It's working, but I don't know if it's the best answer or not

3 Tables Relation In Laravel

I have a little problem,
i can't see how can i make a three table relation on Laravel.
DELETED
If someone can help me, I take notes !
Thanks !
As far as I can see, you need to create a relationship to your pivot table and then, you can create relationships to both table.
class Contact extends Model
{
public function filmJobs()
{
return $this->hasMany(ContactFilm::class);
}
}
class ContactFilm extends Model
{
public function film()
{
return $this->hasOne(Film::class);
}
public function job()
{
return $this->hasOne(Job::class);
}
}
You can now use it like that: $contact->filmJobs[0]->film and $contact->filmJobs[0]->job.

what is the best way to get model data without sending the id in public request?

I'm building online courses website in laravel. I want to display the teachers info in public pages and I don't want the id to be revealed in public requests. should I use UUID or encryption ?
for example :
teacher/1/courses
teacher/1/info
I think uuid is the best approach, it's fairly readable, it is not cryptographical safe but seriously unpractical to break.
Pretty easy to implement using model binding. Imagine a route with this controller.
class CourseController {
public function index(Teacher $teacher) {
...
}
}
Route::get('teacher/{teacher}/courses', 'CourseController#index');
You need a uuid column on the model, do a migration for that. Else overwrite how model binding is resolved on the Teacher.php model.
class Teacher {
public function getRouteKey(): string
{
return $this->uuid;
}
public function resolveRouteBinding($value, $field = null)
{
return self::where('uuid', $value)->firstOrFail();
}
}
From here you just need to set uuid when you create your teacher model, hook into the creating event and it should be fine.
class Teacher {
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->uuid = resolve(UuidFactory::class)->uuid4();
});
}

Laravel get data from pivot table

I have a pivot table of users_operators.
I want to grab the operator_id of the user.
This is how i do this now, but its seems like verbose way.
if (Auth::user()->type === 'operator') {
$user = Auth::user();
// There is a better way to do this?
$operator_id = $user->operator[0]['pivot']['operator_id'];
Session::put('operatorId', $operator_id);
}
class Operator extends \Eloquent
{
public function users() {
return $this->belongsToMany('User');
}
}
class User extends \Eloquent
{
public function operator() {
return $this->belongsToMany('Operator');
}
}
I'm, battling insomnia and not functioning at 100%, but you should be able to get away with $user->operator->id based on what I'm interpreting your models to be (it looks like you had a typo when you copied them into the question).
If that doesn't work, you might want to check out the "Dynamic Properties" section in the Eloquent docs for more info, if you haven't already.

Like or favorite a post with unique user using Laravel Eloquent

I was creating a like system for my website. in this I wanted one user can only like one time for a post. and a post can be liked by many user. Also many user can like many post.
So if I guess it right, It is a many to many reletionship.
in this context,
I create the following table
... users table:
id
name
....
posts table :
id
post
...post_likes table
id
user id
poost_id
Now I am having the following model for
user :
class User extends SentryUserModel {
public function post_likes()
{
return $this->has_many('Post_like', 'id');
}
}
post :
class Post extends Eloquent {
public function post_likes()
{
return $this->has_many('Post_like', 'id');
}
}
post_like :
class Post_like extends Eloquent {
public function posts()
{
return $this->belongs_to('Post', 'post_id');
}
public function users()
{
return $this->belongs_to('User', 'user_id');
}
}
now when I am going to insert into the database (for post_likes table) I am getting an error called
Illuminate \ Database \ Eloquent \ MassAssignmentException
user_id
Also I want to know is there any way to inset into database like
$user->like()->save($user); ?????
Thank you in advance. Happy coding . \m/
I'll start with a basic issue, firstly you might want to make sure all your tables are lower case (still as a snake case as well), it's not required but it's ultimately how it's expected to be with Laravel so it makes life easier to keep with that. Also a note to the wise, like Class names, database tables are typically in the singular so user instead of users
Secondly yes you can do an insert with $user->post_likes()->save($debate); as your post_likes method on the user class returns has_many.
Thirdly, your design of the Post_like class is a bit off, you could be better off make it like so:
class PostLike extends Eloquent { // note that PostLikes is a more standard naming for a class, they should ideally be camel case names but with all capitals for words
protected $table = 'post_like'; // specifies the table the model uses
public function post() // this should be singular, the naming of a belngs_to method is important as Laravel will do some of the work for you if let it
{
return $this->belongs_to('Post'); // by naming the method 'post' you no longer need to specify the id, Laravel will automatically know from the method name and just adding '_id' to it.
}
public function users()
{
return $this->belongs_to('User');
}
}
Fourthly, your other classes could be better as:
class Post extends Eloquent {
public function post_likes()
{
return $this->has_many('PostLike'); // doesn't require you to specify an id at all
}
}
I can't exactly tell you why you're getting that mass assign error, your post is a bit garbled and doesn't look like you've included the code that actually causes the exception? I have a feeling though is that you're trying to do an insert for multiple database rows at one time but haven't defined a fillable array for PostLike such as with here: http://four.laravel.com/docs/eloquent#mass-assignment
According to Laravel 5:
User Model:
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
public function post_likes()
{
return $this->hasMany('App\PostLike');
}
}
Post Model:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model {
public function post_likes()
{
return $this->hasMany('App\PostLike');
}
}
PostLike Model:
namespace App;
use Illuminate\Database\Eloquent\Model;
class PostLike extends Model {
protected $table = 'post_like';
public function posts()
{
return $this->belongsTo('App\Post');
}
public function users()
{
return $this->belongsTo('App\User');
}
}
and if you want to save the post_like data then:
$inputs['post_id'] = 1;
$inputs['user_id'] = 4;
$post_like = PostLike::create($inputs);
Hope this helps!!

Categories