Cant access relations in 'created' Model Event? - laravel

Could someone please clarify this? I've used Model Events a lot of times before but it seems I haven't tried to access any related models on the initial "created" Event.
For example, I have two Models in a M2M relation to each other:
Book() public function authors()
Author() public function books()
I have saved a new Book object, with related authors, elsewhere in my code and by tapping into the "created" or "saved" Model Event (in EventServiceProvider.php), I would like to be able to update some fields in the related objects at the same time like this:
Book::created(function($book) {
$authors = $book->authors;
foreach($authors as $a){
$a->books_authored += 1;
$a->save();
}
});
..but I can't, as $authors call returns no related objects. If this is the usual behaviour (and I haven't done something incorrect here)? Is there a way to get access to these relations on the initial creation/saving Event?
Thanks in advance.

I think the problem you are having is that the created event is firing before you have attached the authors. You haven't attached your code, but I'm assuming:
Book::create(['title' => 'Foo'])->author()->save(new Author['name' => 'Brian']);
This is actually the following:
$book = Book::create(['title' => 'Foo']) // Book created event fired
$relation = $book->author(); // Relation retrieved
$relation->save(new Author['name' => 'Brian']); //related author attached
You should probably manually fire an event when you attach an author in a function on your book model e.g.
class Book extends Eloquent
{
public function saveAuthor($author) {
if($this->save($author)) {
Event::fire(new AuthorWasAttached($this, $author));
}
}
}
Then do your processing in the AuthorWasAttached event class
This may be of help.

You create the book object and then;
$author=new Author(['name'=>'Joo']);
$book->authors()->save($author);

Related

Laravel: Model function to check if relationship exists

I have two models Customer, Contact with the following relationship in the Customer model:
public function latestContact () {
return $this->hasOne(Contact::class)->latest();
}
I already found out here that the optional helper is a possible to way check if the relationship exists when displaying the data. Otherwise I would receive a "Trying to get property of non-object" error.
optional($customer->latestContact)->address
Now I am wondering if there is a way to directly check this inside the model function. I would prefer to only call
$customer->latestContact->address
or something like
$customer->getLatestContactAdress
and return false (or no result) if the relationship does not exists.
Thank you in advance.
You could define an accessor or a function within your parent model.
Something like this in your Customer model:
public function getLatestContactAddress()
{
return optional($this->latestContact)->address;
}
And call it like this:
$customer->getLatestContactAddress();
Try using eager loading
$customer = Customer::with('latestContact')->get();
Let me know if not works

How to eager load a relation with a condition in Eloquent?

I have a relation that can be inherited from a parent if not set for the object itself.
For an example setup let's say we have events that have a venue.
class Event extends Model
{
public function venue()
{
return $this->belongsTo('App\Venue');
}
public function activities()
{
return $this->hasMany('App\Activity');
}
}
And there are activities in the events that mostly take place in the same venue, but sometimes could be elsewhere while still belonging to the same event.
class Activity extends Model
{
public function event()
{
return $this->belongsTo('App\Event');
}
public function venue()
{
if ($this->venue_id)
return $this->belongsTo('App\Venue');
return $this->event->venue();
}
}
If I simply request activities for an event and work with them it is fine. But if I try to eager load the venues for activities, I only get the ones that are set directly on the activity, never requesting one from parent.
$activities = $event->activities;
$activities->load('venue'); // Works correctly without this line
foreach ($activities as $activity)
if ($activity->venue) // Doesn't take venue from the parent (event)
echo $activity->venue->name; //Only shows if venue_id is set on activity
Is there any chance to fix the relations so I could load them in bulk?
By their very nature, eager loaded relationships do not have the relationship method run for each parent model. If they did, you would have the N+1 issue, which is exactly what eager loading is meant to solve.
The relationship method is run once on the model that is used to start the query. This gets the base query to run, and then all of the parent model ids are injected into that query.
In order to do what you want, you need to change things up a little bit. First, your Activity can be directly related to venues, so setup that relationship without any conditions. Next, create an accessor method that will return the proper venue for the Activity.
So, your code would look something like:
class Activity extends Model
{
public function event()
{
return $this->belongsTo('App\Event');
}
public function venue()
{
return $this->belongsTo('App\Venue');
}
public function getActivityVenueAttribute()
{
return $this->venue ?? $this->event->venue ?? null;
}
}
The other option would be to always assign the venue_id on the Activity, even if it is the same as the Event venue_id. Then you don't need to worry about the venue id missing on the activity.

Using Active Record, how can I save child's detail information through its parent?

I'm using parent->child (master->detail) relation in Yii2 Active Record
When I want to create a child, I have to manually fill its parent info like this:
Relation: Client (1) ---> (n) Comments
class ClientController extends \yii\web\Controller
{
public function actionAddComment() {
$comment = new Comment;
if ($comment->load(Yii::$app->request->post())) {
$comment->client = $this->id; // Client id
$comment->save();
}
return $this->render('view', ['comment'=>$comment]);
}
}
I've optimized it, creating a Comment method to do that:
class Comment extends ActiveRecord {
public function newComment($client) {
$comment = new Comment;
$comment->client = $client; // Client id
return $comment;
}
}
And I have gone through beforeSave in the Comment model, but still not sure if there is a better way.
Is there anything like:
$comment = new Comment(Yii::$app->request->post());
$client->save($comment); // Here the parent is writing his information to the child
Or one-liner shortcut:
$client->save(new Comment(Yii::$app->request->post());
Without having to create this logic in beforeSave?
Yes, I recommend to use the built in link() and unlink() methods provided by Active Record which you can use in your controller to relate or unrelate 2 models either they share many-to-many or one-to-many relationship.
It even has an optional $extraColumns attribute for additional column values to be saved into a junction table if using it link( $name, $model, $extraColumns = [] )
So your code may look like this :
$comment = new Comment;
if ($comment->load(Yii::$app->request->post())) {
$comment->link('client', $this);
}
check docs for more info.
Now about where to use this code to relate models, it depend on how your app is structured. I'm not sure if doing that through a triggered event would be a good practice, you need to remember that errors may happens and
you may need to evaluate certain scenarios or logic before throwing exceptions. So in my case, I prefer to use that code into my Controllers.
Sometimes you need to build a specific action like you did actionAddComment(), In certain other cases like when your Post request is meant to update the Parent model and also update its related child models at once, the Parent's Update Action ClientController::actionUpdate() may be a good place to do so, maybe something like this will do the job :
$params = Yii::$app->request->post();
$client->load($this->params, '');
if ($client->save() === false && !$client->hasErrors()) {
throw new ServerErrorHttpException('Failed to update the object for unknown reason.');
}
foreach ($params["comments"] as $comment) {
// We may be sure that both models exists before linking them.
// In this case I'm retrieving the child model from db so I don't
// have to validate it while i just need its id from the Post Request
$comment = Comment::findOne($comment['id']);
if (!$comment) throw new ServerErrorHttpException('Failed to update due to unknown related objects.');
// according to its documentation, link() method will throw an exception if unable to link the two models.
$comment->link('client', $client);
...

Creating a Many-to-many relationship in Laravel with additional data

I have in my database a pivot table that stores extra information. It has 2 foreign keys, and an additional field. Here's what it looks like:
EventTeam
int event_id (fk)
int team_id (fk)
boolean home
The intent here is that an Event may have many teams (in fact, it must have at least 2, but that's not a database constraint), and a team may participate in many events. However, for each event-team relationship, I want to also track whether the team is considered the home team for that event.
How do I define my model with this in mind? Do I have an EventTeam model at all, or do I define a belongsToMany relationship in both the Team and Event models? If I need a separate model, what relationships do I define in it? If I don't, how do I add the boolean field to the pivot table that gets used? I really have no idea how to do this.
You dont need a EventTeam model per se, but it could come in handy for seeders or if you are going to attach models to your EventTeam connection anywhere else in your app. This should work:
Event model:
public function teams()
{
return $this->belongsToMany('Team');
}
Team model:
public function events()
{
return $this->belongsToMany('Event');
}
For the extra boolean you can use ->withPivot().
$this->belongsToMany('Event')->withPivot('is_home');
See http://laravel.com/docs/eloquent#working-with-pivot-tables for more info.
Updated answers:
1) I would put it in both models so you can access the pivot data from both sides without a problem.
2) It should be to column name indeed.
3) Like i said its not really needed for you in this situation, but you could do this:
EventTeam model:
public function event()
{
return $this->belongsTo('Event');
}
public function team()
{
return $this->belongsTo('Team');
}
Add withPivot('home') on your relations definitions, then you can access it like this:
$team->events->first()->pivot->home; // 0/1
$event->teams->first()->pivot->home; // 0/1
first is just an example of getting single related model here.
Now, next thing is adding that value to the relation:
$team = Team::find($id);
$event = Event::find($eventId);
$team->events()->attach($event, ['home' => 1]);
// or
$team->events()->attach($eventId, ['home' => 1]);
// or using sync
$event->teams()->sync([1,5,15], ['home' => 0]);
Another thing is querying that field:
// load first team and related events, that the team hosts
$team = Team::with(['events'=>function ($q) {
$q->wherePivot('home', 1);
}])->first();
// load only teams that are hosts for any event
$hostTeams = Team::whereHas('events', function ($q) {
// wherePivot won't work here!
$q->where('event_team.home', 1);
})->get();
and so on.

Filtering eager-loaded data in Laravel 4

I have the following setup:
Clubs offer Activities, which are of a particular Type, so 3 models with relationships:
Club:
function activities()
{
return $this->hasMany('Activity');
}
Activity:
function club()
{
return $this->belongsTo('Club');
}
function activityType()
{
return $this->hasMany('ActivityType');
}
ActivityType:
function activities()
{
return $this->belongsToMany('Activity');
}
So for example Club Foo might have a single Activity called 'Triathlon' and that Activity has ActivityTypes 'Swimming', 'Running', and 'Cycling'.
This is all fair enough but I need to show a list of ActivityTypes on the Club page - basically just a list. So I need to get the ActivityTypes of all the related Activities.
I can do that like so from a controller method that receives an instance of the Club model:
$data = $this->club->with(array('activities', 'activities.activityTypes'))->find($club->id)
That gets me an object with all the related Activities along with the ActivityTypes related to them. Also fair enough. But I need to apply some more filtering. An Activity might not be in the right status (it could be in the DB as a draft entry or expired), so I need to be able to only get the ActivityTypes of the Activities that are live and in the future.
At this point I'm lost... does anybody have any suggestions for handling this use case?
Thanks
To filter, you can use where() as in the fluent DB queries:
$data = Club::with(array('activities' => function($query)
{
$query->where('activity_start', '>', DB::raw('current_time'));
}))->activityType()->get();
The example which served as inspiration for this is in the laravel docs, check the end of this section: http://laravel.com/docs/eloquent#eager-loading
(the code's not tested, and I've taken some liberties with the property names! :) )
I think if you first constraint your relationship of activities, the activity types related to them will be automatically constrained as well.
So what I would do is
function activities()
{
return $this->belongsToMany('Activity')->where('status', '=', 'active');
}
and then your
$data = $this->club->with(array('activities', 'activities.activityTypes'))->find($club->id)`
query will be working as you would expect.

Resources