Correct way to pull a relationship from another model? - php

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.

Related

Laravel add condition inside model public function relation

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

Retrieve data from multiple joins using eloquent

I'm developing a project to track deliveries of goods.
My idea would be that one delivery can go to different places, and all those places are connected by single trips.
Here is my Eloquent schema:
class Delivery extends Model
{
public function places()
{
return $this->hasMany(Place::CLASS, 'delivery_id');
}
public function trips()
{
// what should I do here?
}
}
class Place extends Model
{
public function delivery()
{
return $this->belongsTo(Delivery::CLASS, 'delivery_id');
}
}
class Trip extends Model
{
public function departurePlace()
{
return $this->belongsTo(Place::CLASS, 'departure_place_id');
}
public function arrivalPlace()
{
return $this->belongsTo(Place::CLASS, 'arrival_place_id');
}
}
Basically what I am trying to do is to get all the trips that are related to one delivery. Or, as another way of saying it, all the trips that connect all the places that one delivery must go through.
This is the SQL query that achieves the result I want:
select distinct trip.*
from delivery
join place on (place.delivery_id = delivery.id)
join trip on (place.id = trip.arrival_place_id or place.id = trip.departure_place_id)
I would like to have a trips() method on my Delivery model, that returns that result.
However I am quite confused as to how achieve that using Eloquent.
Unfortunately we could have simply used the union method to achieve this, but it seems that is doesn't work for hasManyThroughrelations.
Anyway, I think Eloquent relations are not meant to be used to achieve such a specific query.
Instead, you may use Eloquent scopes to achieve this.
So based on the answer of #Saengdaet, we can write two relations and then combine them with a scope:
(BTW: I don't know why you said that his code gave an error...)
class Delivery extends Model
{
public function places()
{
return $this->hasMany(Place::class);
}
public function outboundTrips()
{
return $this->hasManyThrough(
Trip::class,
Place::class,
"delivery_id", // Foreign key on places table
"departure_place_id", // Foreign key on trips table
);
}
public function inboundTrips()
{
return $this->hasManyThrough(
Trip::class,
Place::class,
"delivery_id", // Foreign key on places table
"arrival_place_id", // Foreign key on trips table
);
}
public function scopeTrips($query)
{
$deliveriesWithTrips = $query->with(['outboundTrips', 'inboundTrips'])->get();
$trips = [];
$deliveriesWithTrips->each(function ($elt) use (&$trips) {
$trips[] = $elt->inboundTrips->merge($elt->outboundTrips);
});
return $trips;
}
}
And now to retrieve all trips for a given delivery you simply write:
Delivery::where('id', $id)->trips();
If you want to get all trips from using Delivery model, you can use `hasManyThrough" relationship that provides a convenient shortcut for accessing distant relations via an intermediate relation.
But you have to choose which FK on your Trip model will you use to relate it
You can refer to "has-many-through" relationship
class Delivery extends Model
{
public function places()
{
return $this->hasMany(Place::CLASS, 'delivery_id');
}
public function trips()
{
return $this->hasManyThrough(
Trip::Class,
Place::Class,
"delivery_id", // Foreign key on places table
"departure_place_id", // Foreign key on trips table
"id", // Local key on deliveries table
"id" // Local key on places table
);
}
}

Laravel Eloquent Polymorphic Scope latest first item

Good day,
I'm a bit stuck here with fetching latest item using Laravel scopes and Eloquent Polymorphic One-to-Many relationship.
Given:
I'm using latest version of Laravel 6.x.
I have two models: Website and Status.
Status model is reusable and can be used with other models.
Each website has multiple statuses.
Every time status is changed a new record is created in DB.
Active website status is the latest one in the DB.
Websites model:
class Website extends Model
{
public function statuses()
{
return $this->morphMany(Statuses::class, 'stateable');
}
public function scopePending($query)
{
return $query->whereHas('statuses', function ($query) {
$query->getByStatusCode('pending');
});
}
}
Statuses model:
class Statuses extends Model
{
public function stateable()
{
return $this->morphTo();
}
public function scopeGetByStatusCode($query, $statusCode)
{
return $query->where('status_code', $statusCode)->latest()->limit(1);
}
}
The problem is that when I call:
Website::pending()->get();
The pending() scope will return all websites that have ever got a pending status assigned to them, and not the websites that have currently active pending status (eg. latest status).
Here is the query that is returned with DB::getQueryLog()
select * from `websites`
where exists
(
select * from `statuses`
where `websites`.`id` = `statuses`.`stateable_id`
and `statuses`.`stateable_type` = "App\\Models\\Website"
and `status_code` = 'pending'
order by `created_at` desc limit 1
)
and `websites`.`deleted_at` is null
What is the right way of obtaining pending websites using scope with polymorphic one-to-many relation?
Similar issue is descried here: https://laracasts.com/discuss/channels/eloquent/polymorphic-relations-and-scope
Thanks.
Okay, after doing some research, I have stumbled across this article: https://nullthoughts.com/development/2019/10/08/dynamic-scope-on-latest-relationship-in-laravel/
Solution turned out to be quite eloquent (sorry for bad pun):
protected function scopePending($query)
{
return $query->whereHas('statuses', function ($query) {
$query->where('id', function ($sub) {
$sub->from('statuses')
->selectRaw('max(id)')
->whereColumn('statuses.stateable_id', 'websites.id');
})->where('status_code', 'pending');
});
}

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

Categories