Search code examples
laravelone-to-oneobservers

Laravel Model Observers & Relations


I have a Member, Person and Address model. They are connected with One to One relationships. Member has one Person, Person has one Address, Address belongs to Person, and Person belongs to Member etc.

Now, my Address can also belongTo a Location... Resulting in 2 nullable colums, person_id & location_id

I have created an observer on the Address model. When I am deleting a Member, I want the address to be kept, if the location_id is set.

public function deleting(Address $address)
{
    if($address->location_id != null) {
        $address->person_id = null;
        $address->save();
        return false;
    } else {
        return true;
    }
}

Weird thing is when I use ... ->address()->delete(); ... It will remove the address with the person, and the Observerfunction won't fire. I have to delete the address first separately $address->delete();, and now the Observerfunction will fire. Another problem I was thinking about, what if it is the other way around? Deleting a Location with the 'person_id' set?

Is there a better way to handle deleting a model that (can) belongsTo 2 different models? (Only OneToOne relationships).

I was thinking maybe I have to make 2 Observers, for Member and Location, and drop the one on the Address model? Or not use observers at all

EDIT:

So the situation can occur that a person and a location can have the same Address. In this case both 'person_id' & 'location_id' are != null on the Address model.

So far I have all the functions on the MemberController working. In my case public function destroy( $id ) will only softdelete a member, and keep the Person and Address untouched.

All I need to fix now is forceDeleting, I've been able to come up with some stuff, but I am not sure what is a good way. Something smells :P When I delete a Member like this

$member->person()->delete();
$member->forceDelete();

This would work. But I will have to do 3 deletes? (including the address). Also deleting an address should depend on the presence of a location_id, because when an address also belongs to a Location, I would like the address to be kept, and instead set the person_id on null.

I have tried this by making an observer on the address model, but with this Im facing the problem of not knowing wether a Member or a Location is being deleted, and so I am not sure which '_id' should be checked for being null or not.

Another problem I am facing is the observer function wont fire when I am deleting the Member->relation()-> way. It only seems to work when I am deleting the address separately $address->delete(); because of this Im doubting if observer is even a good way, and maybe I should just do it in the Member/Location controller.

/**
 * Permanently remove the specified resource from storage.
 *
 * @param int $id
 * @return Response
 */
public function remove( $id )
{
    $member = $this->model->onlyTrashed()->find( $id );

    $address = $member->person->address;
    if($address->location_id != null) {
        $address->person_id == null;
        $address->save();
    } else {
        $address->delete();
    }

    $member->person()->delete();
    $member->forceDelete();

This would work, maybe even a case of keeping it stupid simple? But I am not sure if doing this Address stuff here is a good way, just trying to learn.

Member | hasOne - belongsTo | Person | hasOne - belongsTo | Address | belongsTo - hasOne | Location


Solution

  • The reason you're not able to get your observers to fire when running something like $member->relation()->delete(); is because $member->relation() will just return an instance of Builder. When you call delete() on Builder it will just execute the query and won't actually new-up a model which in turn will mean an event never gets fired. To get the events to fire you can simply do $member->relation->delete();

    If I'm understanding you're issue correctly then you could add a listener/observer to the Person model that will delete the address that doesn't have a location or make the person_id null if it does:

    protected static function boot()
    {
        parent::boot();
    
        static::deleting(function (self $person) {
    
            if (is_null($person->address->location_id)) {
                $person->address->delete();
            } else {
                $person->address->person_id = null;
                $person->address->save();
            }
    
        });
    }
    

    Then in your Member model you could add another deleting listener that also checks if the model is being forceDeleted:

    protected static function boot()
    {
        parent::boot();
    
        static::deleting(function (self $member) {
    
            if ($member->forceDeleting) {
                $member->person->delete();
            }
        });
    }
    

    Using the above $member->delete() will not remove the person or the address, however, $member->forceDelete() will cause both the person and address to be deleted (but the address will only be deleted if it doesn't also belong to a location).

    Hope this helps!