Laravel add condition inside model public function relation - php

I am developing a booking system which used Laravel 7.0
so in this system I created a table called 'bookings'
and I created its model called Booking.php
inside this Booking.php I put in relation function to join its model to transaction table like below
public function transaction()
{
return $this->belongsTo('App\Models\Transaction', 'id', 'booking_id');
}
suddenly a new requirement coming in, and this booking will have its child
so to differentiate between the booking parent and its child i am adding a new column to the booking table called
'parent_booking_id'
I don't want to change the code function too much so I am thinking on making condition to make sure the relation of the booking points to the correct transaction
I am thinking of code like this to be written
public function transaction()
{
return $this->belongsTo('App\Models\Transaction', 'parent_booking_id' ?? 'id', 'booking_id');
}
but it is not working as intended
the logic is
If the parent_booking_id exists then join the 'booking_id' column at table transaction to 'parent_booking_id' column at table bookings
else
it will join the 'booking_id' column at table transaction to 'id' column at table bookings

I am not really sure if I understood everything.
I not using that kind of logic inside models, but if you really want it. I'm not sure, but I think this could work.
public function parent_booking()
{
$this->belongsTo('App\Models\Booking', 'parent_booking_id');
}
public function transaction()
{
if ($this->has('parent_booking')) {
return $this->hasOne('App\Models\Transaction', 'parent_booking_id', 'booking_id');
} else {
return $this->hasOne('App\Models\Transaction', 'booking_id');
}
}
If ywhat you try to achieve is this:
When a booking have a parent_booking, attach the transaction to the parent_booking, else, attach it to the booking.
To do this, my suggestion is to do things more like this.
public function parent_booking()
{
$this->belongsTo('App\Models\Booking', 'parent_booking_id');
}
public function childs_booking()
{
$this->hasMany('App\Models\Booking', 'parent_booking_id');
}
Then, you can do this in controller:
$booking = Booking::with('parent_booking')->find($id);
if ($booking->has('parent_booking')) {
//do some stuff.
$booking->parent_booking->transaction->associate($transaction->id);
} else {
//do other stuff
$booking->transaction->associate($transaction->id);
}
And for sure, you will want to retrieve the right transaction. There is an example on a retrieve of every bookings.
$bookings = Booking::with(array('parent_booking', function($query) {
return $query->with('transaction');
}))->with('transaction')->get();
foreach($bookings as $booking) {
if ($booking->has('parent_booking')) {
$transaction_to_use = $booking->parent_booking->transaction;
} else {
$transaction_to_use = $booking->transaction;
}
}
You can also perform something like this:
$transactions = Transaction::with(array('booking', function($query) {
return $query->with('childs_booking');
}))->get();
This way you get every transactions with their booking. You also have every child booking.

Finally found the solution to this problem
the proper way to fix this is by
making 2 relation
and then altering code using where condition to make sure it pass the correct relation based on booking value
BUT because this way of fixing require to much code changes i try to find other ways
and the other ways is to instead of creating 'parent_booking_id' i create a column called 'primary_booking_id'
this 'primary_booking_id' will always contain the booking_id of the first booking being made
so on first time create the model I will update so that it gains the booking_id
$booking->update(['primary_booking_id' => $booking->id]);
and then during creation of its child I will put into the primary_booking_id into the creation
$input['primary_booking_id'] => $previous_booking->primary_booking_id;
$booking::create($input);
so the way this is handled require to make just minor changes to booking transaction relation function as below
public function transaction()
{
return $this->hasOne('App\Models\Transaction', 'booking_id', 'primary_booking_id');
}
voila!
it works and no major changes involve

Related

I'm getting an error when trying to save a new model with 2 fields referencing different ids in the same table

I'm new to laravel, and I've picked up the basic workflow of creating, updating and deleting database entries using migrations, models and controllers. But now I'm trying to do the same with a subscriptions table that has a subscriberId and a followeeId in it. Both of these fields reference different ids of the same table (users). This kind of task seem to require some finetuning. And I'm stuck.
Here's my code with some comments.
Subscriptions Table
Schema::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('subscriberId');
$table->unsignedBigInteger('followeeId');
$table->foreign('subscriberId')->references('id')->on('users');
$table->foreign('followeeId')->references('id')->on('users');
});
Previously, I've used another approach to foreign ids, namely the one with the $table->foreignId('user_id')->constrained() pattern, but in this particular case I need to make sure that the two foreign ids reference different users, so I went for a more verbose option.
User Model
public function subscriptions()
{
return $this->hasMany(Subscription::class, 'subscriberId');
}
Here I've added the second parameter. This seems to work.
Subscription Model
class Subscription extends Model
{
use HasFactory;
protected $fillable = [
'subscriberId',
'followeeId'
];
public function subscriberId()
{
return $this->belongsTo(User::class, 'id', 'subscriberId');
}
public function followeeId()
{
return $this->belongsTo(User::class, 'id', 'followeeId');
}
}
Here I pass additional parameters, too, although in this case I'm not so sure if these are the correct ones. But this is my best guess. If I'm not mistaken, the second parameter of the belongsTo relation is inferred from the model that is being passed in, not the model of the parent class as is the case with the hasMany relation. So in this case that would be 'id' of the users table, which would be the default here anyway, but I need the third parameter, so I explicitly state the second parameter as well. Again, I'm not sure about this combination, but that's what I was able to make of the docs. I've also used other combinations of additional parameters, and even tried getting rid of these two public functions altogether, but that won't work either.
Now, here's the controller. If I do this:
$user->subscriptions()->get();
I do get the subscriptions I want. But if I do this instead:
$user->subscriptions()->create([
'subscriberId' => 1,
'followeeId' => 2
]);
I get the 500 error. I've also tried another approach:
$newSub = new Subscription;
$newSub->subscriberId = 1;
$newSub->followeeId = 2;
$newSub->save();
return $newSub;
But still no success. I still get the 500 error when I try to save()
Please help me out.
Solution
I should have used
public $timestamps = false
in the Subscription model, and I also misunderstood the docs. The correct combo is
User Model
public function subscriptions()
{
return $this->hasMany(Subscription::class, 'subscriberId');
}
and
Subscription Model
public function subscriberId()
{
return $this->belongsTo(User::class, 'subscriberId');
}
public function followeeId()
{
return $this->belongsTo(User::class, 'followeeId');
}

Laravel polymorphic relation with custom key

In my project I'm working on multiple databases and one central one.
I'm using spatie's activity log package to log actions done form control panel to all of that databases.
I have table Items in each of the databases (except for the central) with auto incremented primary key, and another index called hash, which is kind of uuid. Hash is always unique.
Now, when I want to log actions, I can encounter problem as it will save ID of Item, so... in my activity tables I will get two records for subject_id = 1, while one activity happend to Item on one db and another on another, and so on.
How can I change set morphing to use my uuid column instead of id without changing $primaryKey on related model?
Item model relation:
public function activities(): MorphMany
{
$this->morphMany(Activity::class, 'subject', 'subject_id', 'hash');
}
Activity model relation:
public function subject(): MorphTo
{
if (config('activitylog.subject_returns_soft_deleted_models')) {
return $this->morphTo()->withTrashed();
}
return $this->morphTo('activity_log', 'subject_type', 'subject_id', 'hash');
}
Also, I found in ActivityLogger:
public function performedOn(Model $model)
{
$this->getActivity()->subject()->associate($model);
return $this;
}
I ended up with temporary hack.
First of all, I've added a public method to my model:
public function setPrimaryKey(string $columnName)
{
$this->primaryKey = $columnName;
$this->keyType = 'string';
}
Later on I extended ActivityLogger class and implemented my own perfomedOn() method.
public function performedOn(Model $model)
{
if($model instanceof Item::class) {
$model->setPrimaryKey('hash');
}
return parent::performedOn($model);
}
I am aware it is not the best solution but kind of works for now.

Laravel -- Flatten data appended via `with`

I've got two models, User and Seminar. In English, the basic idea is that a bunch of users attend any number of seminars. Additionally, exactly one user may volunteer to speak at each of the seminars.
My implementation consists of a users table, a seminars table, and a seminar_user pivot table.
The seminar_user table has a structure like this:
seminar_id | user_id | speaking
-------------|-----------|---------
int | int | bool
The relationships are defined as follows:
/** On the Seminar model */
public function members()
{
return $this->belongsToMany(User::class);
}
/** On the User model */
public function seminars()
{
return $this->belongsToMany(Seminar::class);
}
I am struggling to figure out how to set up a "relationship" which will help me get a Seminar's speaker. I have currently defined a method like this:
public function speaker()
{
return $this->members()->where('speaking', true);
}
The reason I'd like this is because ultimately, I'd like my API call to look something like this:
public function index()
{
return Seminar::active()
->with(['speaker' => function ($query) {
$query->select('name');
}])
->get()
->toJson();
}
The problem is that since the members relationship is actually a belongsToMany, even though I know there is only to ever be a single User where speaking is true, an array of User's will always be returned.
One workaround would be to post-format the response before sending it off, by first setting a temp $seminars variable, then going through a foreach and setting each $seminar['speaker'] = $seminar['speaker'][0] but that really stinks and I feel like there should be a way to achieve this through Eloquent itself.
How can I flatten the data that is added via the with call? (Or rewrite my relationship methods)
Try changing your speaker function to this
public function speaker()
{
return $this->members()->where('speaking', true)->first();
}
This will always give you an Item as opposed to a Collection that you currently receive.
You can define a new relation on Seminar model as:
public function speaker()
{
return $this->belongsToMany(User::class)->wherePivot('speaking', true);
}
And your query will be as:
Seminar::active()
->with(['speaker' => function ($query) {
$query->select('name');
}])
->get()
->toJson();
Docs scroll down to Filtering Relationships Via Intermediate Table Columns

Correct way to pull a relationship from another model?

We have a Payments table that links to Sales and Sales links to Clients. What is the correct way to link to the Clients relationship from the Payments table?
Payments.sale_id -> Sales.id
Sales.client_id -> Client.id
I tried:
class Payment extends Model {
public function sale() {
return $this->belongsTo('\\App\\Models\\Sale', 'sale_id');
}
public function client() {
return $this->sale->client();
}
}
Which works fine when the sale_id is filled. However, if the sale_id is NULL, this breaks (obviously because $this->sale is null in that case).
I would like a Laravel solution that still allows access through the $client property/attribute.
A temporary solution to avoid the fatal error is to use the get attribute mutator instead of client():
public function getClientAttribute() {
if ($this->sale) {
return $this->sale->client;
}
return null;
}
We'll see if a better solution comes up.

Condition on the relational model Laravel 5

Hello and thanks in advance for your time and help. I have 3 very simple tables. A user table with a user_id, a games table with a game_id as well as some other fields (scheduled date/time) and a GamesAttendee table that just has user_id and game_id field. I am trying to select all games that user is connected to and only return ones that are scheduled for the future/past.
What I ended up going with is:
$cur = GamesAttendee::where('user_id',$user_id)->pluck('game_id')->all();
$cur = Game::whereIn('id', $cur)->where('scheduled','>=',$now)->get();
But I feel like there has to be a more efficient way of doing this. I have looked around and tried various things like eager loading and just messing with my models and nothing seems to work. I feel like this a very simple and essential case that is extremely common and I am wondering how this is actually supposed to be done in laravel.
I have tried:
$cur = Game::with(['attendees'=>function($q) use ($user_id){
return $q->where('user_id',$user_id);
}])->where('scheduled','>=',$now)->get();
But that was not what I wanted. I am basically trying to do:
SELECT * FROM GameAttendees
JOIN `games` on games.id = GameAttendees.game_id
WHERE GameAttendees.user_id = 'x' AND games.scheduled >= '2016/05/01' ;
I quickly jotted that mysql code so just ignore any mistakes. Any ideas?
Thank you.
UPDATE:
Resolved by adding the following into my user model:
public function future_games()
{
$now = gmdate('Y-m-d H:i:s',strtotime('+4 hours'));
return $this->belongsToMany('App\Game','games_attendees')->where('scheduled','>=',$now);
}
then in my controller I was able to do:
$future_games = User::with('future_games')->get();
First define many-to-many relation in your Game and User models:
class Game extends Model {
public function users() {
return $this->belongsToMany(User::class, 'GameAttendees');
}
}
class User extends Model {
public function games() {
return $this->belongsToMany(Game::class, 'GameAttendees');
}
}
With that in place you should be able to get all games given user is attending with:
$games = $user->games;
If you want to add some additional conditions, do the following:
$futureGames = $user->games()->where('scheduled','>=',$now)->get();
Or just create another relation in your User model:
class User extends Model {
public function futureGames() {
return $this->belongsToMany(Game::class, 'GameAttendees')->where('scheduled','>=',$now);
}
}
and access them by:
$futureGames = $user->futureGames;

Categories