Skip to content
Advertisement

Laravel update model’s one-to-many relation’s items

I have two Eloquent models:

class User extends Model
{
    public function items()
    {
        return $this->hasMany(Item::class, "userId");
    }
}
class Item extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class, "userId");
    }
}

Item‘s columns are id, userId and name.

When I want to update the user (PUT /users/<id>) I also want to update the items in the same request, meaning:

  1. Skip existing items
  2. Add new items
  3. Remove extra items

Afterwards both DB and the relation should have up-to-date items.

Example of the request data:

{
    "userAttribute1": "Something",
    "userAttribute2": "Something else",
    "items": [
        {
            "name": "Name1"
        },
        {
            "name": "Name2"
        }
    ]
}

I tried to look for a simple way to do this with Laravel, for example with many-to-many relations you can just call attach/detach and sync to automatically update them with ids.

I have made this abomination for my one-to-many relation:

// Controller method for PUT /users/<id>
public function update(User $user, Request $request)
{
    // Update user attributes normal way
    $user->fill($request->validated());
    $user->save();
    
    // Array, e.g. [ { "name": "Name1" }, { "name": "Name2" } ]
    $newItems = $request->validated()["items"];
    
    // Collection of items
    $currentItems = $user->items;
    
    // Array to keep track of which items to delete later
    $removeItemIds = [];
    
    foreach ($currentItems as $i => $currentItem)
    {
        // currentItem is Item model

        $exists = false;
        
        foreach ($newItems as $j => $newItem)
        {
            // newItem is array, e.g. { "name": "Name1" }

            if ($currentItem->name === $newItem["name"])
            {
                $exists = true;
                break;
            }
        }
        
        if ($exists)
        {
            // New item already exists, remove from newItems array
            unset($newItems[$j]);
        }
        else
        {
            // Current item does't exist anymore, remove from currentItems collection and mark as deletable
            $removeItemIds[] = $currentItem->id;
            unset($currentItems[$i]);
        }
    }
    
    // Add remaining new items
    foreach ($newItems as $newItem)
    {
        $item = Item::make($newItem);
        $item->userId = $user->id;
        $item->save();
        
        $currentItems->push($item);
    }
    
    // Delete extra items
    $user->items()->whereIn("id", $removeItemIds)->delete();
    
    // Update relation so the returned data is up-to-date as well
    $user->setRelation("items", $currentItems);
    
    return [
        "user" => new UserResource($user),
    ];
}

This user + items model is just an example – I have multiple similar relations (where there is more than just name column) and copy pasting this code everywhere and slightly modifying it seems a little bit dumb.

Laravel is known for all these fancy shortcuts and easy to use/magic methods so my question here is: is there a simpler and shorter way to do this update?

Advertisement

Answer

You can use Laravel collections.

First, update the user:

$user->update($request->validated());

Then, you can sync user items:

$new = ['item 1', 'item 2']; // Your request items

// Get the current items your user have:
$items = $user->items->pluck('name')->toArray();

// Now, you need to delete the items your user have but are not present in the request items array:
$deleteItems = $user->items->pluck('name', 'id')
  ->reject(function($value, $id) use ($new) {
      return in_array($value, $new);
  })
  ->keys();

Item::whereIn('id', $deleteItems)->delete();

// Last but not least, you need to create the new items and attach it to the user:
collect($new)->each(function($insertData) use ($user) {
  $user->items()->firstOrcreate([
      'name' => $insertData
  ]);
});

For models with more than one field, you need to pass two arrays to the firstOrCreate method. The first array will be used to find the model, and, if not found, it will be created with the merge of the two arrays:

$user->items()->firstOrcreate([
   'name' => $insertData
], [
    'description' => $description,
    'quantity' => $quantity
]);

Since you are using firstOrCreate, it will only create the item if it’s not found by it’s name, and you will not have duplicated items.

User contributions licensed under: CC BY-SA
2 People found this is helpful
Advertisement