119

Is there any way to easily clone an Eloquent object, including all of its relationships?

For example, if I had these tables:

users ( id, name, email )
roles ( id, name )
user_roles ( user_id, role_id )

In addition to creating a new row in the users table, with all columns being the same except id, it should also create a new row in the user_roles table, assigning the same role to the new user.

Something like this:

$user = User::find(1);
$new_user = $user->clone();

Where the User model has

class User extends Eloquent {
    public function roles() {
        return $this->hasMany('Role', 'user_roles');
    }
}
andrewtweber
  • 24,520
  • 22
  • 88
  • 110

12 Answers12

115

tested in laravel 4.2 for belongsToMany relationships

if you're in the model:

    //copy attributes
    $new = $this->replicate();

    //save model before you recreate relations (so it has an id)
    $new->push();

    //reset relations on EXISTING MODEL (this way you can control which ones will be loaded
    $this->relations = [];

    //load relations on EXISTING MODEL
    $this->load('relation1','relation2');

    //re-sync everything
    foreach ($this->relations as $relationName => $values){
        $new->{$relationName}()->sync($values);
    }
Sabrina Leggett
  • 9,079
  • 7
  • 47
  • 50
  • 5
    Worked in Laravel 7 – Daniyal Javani Apr 18 '20 at 11:30
  • It also works on previous version Laravel 6. (I guess is expected based on the previous comment :) ) Thanks! – mmmdearte Jul 24 '20 at 09:52
  • Worked in Laravel 7.28.4. I have noticed that code should be different if you are trying to run it outside the model. Thanks – Roman Grinev Oct 21 '20 at 19:51
  • I believe this no longer works as of Laravel 8. The relationships were being reassigned from the old model to the new model in the `$this->replicate` step, so the later step would have no relationships to pull from and the old model would be ruined. I was able to get it working by moving `$this->relations = []` to be the FIRST command. Also, `sync` does not appear to be a method any longer, I was able to use `createMany` to serve the same purpose, but then doesn't work for certain `HasOne` rels, so I had to extend `HasOne` to have the `create` method not call `setForeignAttributesForCreate`. – Kyle Crossman Jan 21 '22 at 22:54
  • for laravel 9 not worked , 1.$homeWork->relations = []; 2. $homeWork->load('medias'); 3. dd($homeWork->relations ) returns null – rijisoft Dec 25 '22 at 19:23
58

You may also try the replicate function provided by eloquent:

http://laravel.com/api/4.2/Illuminate/Database/Eloquent/Model.html#method_replicate

$user = User::find(1);
$new_user = $user->replicate();
$new_user->push();
Tosh
  • 1,789
  • 15
  • 20
Piotr Borek
  • 866
  • 1
  • 6
  • 6
  • 10
    Actually you have to load the relationships you want to replicate as well. The given code will only replicate the base model without its relations. To clone the relationships as well, you can either get the user with its relations: `$user = User::with('roles')->find(1);` or load them after you have the Model: so the first two lines would be `$user = User::find(1); $user->load('roles');` – Alexander Taubenkorb Apr 15 '15 at 08:43
  • 2
    Loading the relationships does not appear to also replicate the relationships, at least not in 4.1. I had to replicate the parent, then loop through the children of the original replicated them and updating them one at a time to point to the new parent. – Rex Schrader May 25 '15 at 17:53
  • `replicate()` will set the relations and `push()` recurses into the relations and saves them. – Matt K Nov 04 '15 at 21:41
  • Also in 5.2 you need to loop through the children and save them after replicating one at a time; inside a foreach: `$new_user->roles()->save($oldRole->replicate)` – d.grassi84 Aug 09 '16 at 17:51
37

For Laravel 5. Tested with hasMany relation.

$model = User::find($id);

$model->load('invoices');

$newModel = $model->replicate();
$newModel->push();


foreach($model->getRelations() as $relation => $items){
    foreach($items as $item){
        unset($item->id);
        $newModel->{$relation}()->create($item->toArray());
    }
}
JIM
  • 500
  • 4
  • 11
31

You may try this (Object Cloning):

$user = User::find(1);
$new_user = clone $user;

Since clone doesn't deep copy so child objects won't be copied if there is any child object available and in this case you need to copy the child object using clone manually. For example:

$user = User::with('role')->find(1);
$new_user = clone $user; // copy the $user
$new_user->role = clone $user->role; // copy the $user->role

In your case roles will be a collection of Role objects so each Role object in the collection needs to be copied manually using clone.

Also, you need to be aware of that, if you don't load the roles using with then those will be not loaded or won't be available in the $user and when you'll call $user->roles then those objects will be loaded at run time after that call of $user->roles and until this, those roles are not loaded.

Update:

This answer was for Larave-4 and now Laravel offers replicate() method, for example:

$user = User::find(1);
$newUser = $user->replicate();
// ...
The Alpha
  • 143,660
  • 29
  • 287
  • 307
  • 2
    Be careful, only a shallow copy, not the sub/child objects :-) – The Alpha May 27 '14 at 18:44
  • 1
    @TheShiftExchange, You may [find it interesting](http://heera.it/taste-javascript-php-part-2-php-5-4#.U4Tdg_mSySo), I did an experiment a long ago. Thanks for the thumbs up :-) – The Alpha May 27 '14 at 18:47
  • 1
    Doesn't this also copy the id of the object? Making it useless for saving? – Tosh Mar 18 '15 at 14:08
  • @Tosh, Yes, exactly and that's why you need to set another id or `null` :-) – The Alpha Mar 18 '15 at 16:56
  • @TheAlpha I found that you cannot set the new id to `NULL` or another id. Laravel will try to update the model you cloned instead of inserting a new one. `replicate()` is the best way to accomplish this as it creates a new static instance first, then omits the id and created/updated dates entirely when copying over the attributes. – Matt K Nov 05 '15 at 21:17
  • No, it will work, I've tested it when therer was no replicate() method, anyways, the null will be replaced by `mySql` when inserting @MattK but thanks for your input :-) – The Alpha Nov 06 '15 at 08:04
  • @TheAlpha I missed the Laravel 4 tag. FYI, it will not work in Laravel 5.1. `$user = User::find(1); $new_user = clone $user; $new_user->id = NULL; $new_user->save();` ... this code throws a `QueryException`, the generated SQL is: "SQL: update `plans` set `id` = , `updated_at` = 2015-11-06 15:20:15 where `id` = 1", it tries to update the original model and not insert a new one. – Matt K Nov 06 '15 at 15:27
8

Here is an updated version of the solution from @sabrina-gelbart that will clone all hasMany relationships instead of just the belongsToMany as she posted:

    //copy attributes from original model
    $newRecord = $original->replicate();
    // Reset any fields needed to connect to another parent, etc
    $newRecord->some_id = $otherParent->id;
    //save model before you recreate relations (so it has an id)
    $newRecord->push();
    //reset relations on EXISTING MODEL (this way you can control which ones will be loaded
    $original->relations = [];
    //load relations on EXISTING MODEL
    $original->load('somerelationship', 'anotherrelationship');
    //re-sync the child relationships
    $relations = $original->getRelations();
    foreach ($relations as $relation) {
        foreach ($relation as $relationRecord) {
            $newRelationship = $relationRecord->replicate();
            $newRelationship->some_parent_id = $newRecord->id;
            $newRelationship->push();
        }
    }
davidethell
  • 11,708
  • 6
  • 43
  • 63
7

This is in laravel 5.8, havent tried in older version

//# this will clone $eloquent and asign all $eloquent->$withoutProperties = null
$cloned = $eloquent->cloneWithout(Array $withoutProperties)

edit, just today 7 April 2019 laravel 5.8.10 launched

can use replicate now

$post = Post::find(1);
$newPost = $post->replicate();
$newPost->save();
david valentino
  • 920
  • 11
  • 18
5

When you fetch an object by any relation you want, and replicate after that, all relations you retrieved are also replicated. for example:

$oldUser = User::with('roles')->find(1);
$newUser = $oldUser->replicate();
elyas.m
  • 1,260
  • 1
  • 13
  • 20
3

Here is a trait that will recursively duplicate all the loaded relationships on an object. You could easily expand this for other relationship types like Sabrina's example for belongsToMany.

trait DuplicateRelations
{
    public static function duplicateRelations($from, $to)
    {
        foreach ($from->relations as $relationName => $object){
            if($object !== null) {
                if ($object instanceof Collection) {
                    foreach ($object as $relation) {
                        self::replication($relationName, $relation, $to);
                    }
                } else {
                    self::replication($relationName, $object, $to);
                }
            }
        }
    }

    private static function replication($name, $relation, $to)
    {
        $newRelation = $relation->replicate();
        $to->{$name}()->create($newRelation->toArray());
        if($relation->relations !== null) {
            self::duplicateRelations($relation, $to->{$name});
        }
    }
}

Usage:

//copy attributes
$new = $this->replicate();

//save model before you recreate relations (so it has an id)
$new->push();

//reset relations on EXISTING MODEL (this way you can control which ones will be loaded
$this->relations = [];

//load relations on EXISTING MODEL
$this->load('relation1','relation2.nested_relation');

// duplication all LOADED relations including nested.
self::duplicateRelations($this, $new);
Sean
  • 460
  • 5
  • 9
  • How to update the above code for replicating pivot relations? – Sonal Apr 26 '21 at 11:38
  • inetead of `foreach ($from->relations as $relationName => $object){...`, in new Laravel versions replace it with `foreach ($from->getRelations() as $relationName => $object){` – oussama benounnas Mar 07 '23 at 16:10
2

If you have a collection named $user, using the code bellow, it creates a new Collection identical from the old one, including all the relations:

$new_user = new \Illuminate\Database\Eloquent\Collection ( $user->all() );

this code is for laravel 5.

Mihai Crăiță
  • 3,328
  • 3
  • 25
  • 37
2

I added this function in BaseModel to duplicate data with relations. It works in Laravel 9.

public function replicateWithRelationsAttributes(): static
{
    $model = clone $this->replicate();
    foreach ($this->getRelations() as $key => $relation) {
       $model->setAttribute($key, clone $relation);
    }

    return $model;
}
Raza
  • 3,147
  • 2
  • 31
  • 35
0

Here's another way to do it if the other solutions don't appease you:

<?php
/** @var \App\Models\Booking $booking */
$booking = Booking::query()->with('segments.stops','billingItems','invoiceItems.applyTo')->findOrFail($id);

$booking->id = null;
$booking->exists = false;
$booking->number = null;
$booking->confirmed_date_utc = null;
$booking->save();

$now = CarbonDate::now($booking->company->timezone);

foreach($booking->segments as $seg) {
    $seg->id = null;
    $seg->exists = false;
    $seg->booking_id = $booking->id;
    $seg->save();

    foreach($seg->stops as $stop) {
        $stop->id = null;
        $stop->exists = false;
        $stop->segment_id = $seg->id;
        $stop->save();
    }
}

foreach($booking->billingItems as $bi) {
    $bi->id = null;
    $bi->exists = false;
    $bi->booking_id = $booking->id;
    $bi->save();
}

$iiMap = [];

foreach($booking->invoiceItems as $ii) {
    $oldId = $ii->id;
    $ii->id = null;
    $ii->exists = false;
    $ii->booking_id = $booking->id;
    $ii->save();
    $iiMap[$oldId] = $ii->id;
}

foreach($booking->invoiceItems as $ii) {
    $newIds = [];
    foreach($ii->applyTo as $at) {
        $newIds[] = $iiMap[$at->id];
    }
    $ii->applyTo()->sync($newIds);
}

The trick is to wipe the id and exists properties so that Laravel will create a new record.

Cloning self-relationships is a little tricky but I've included an example. You just have to create a mapping of old ids to new ids and then re-sync.

mpen
  • 272,448
  • 266
  • 850
  • 1,236
0

In Laravel v5.8.10+ (Currently Laravel v9.x) If you need to work with Laravel replicate() model with relationships this could be a solution. Let's see two simple example.

app/Models/Product.php

<?php
  
namespace App\Models;
  
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
  
class Product extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<string>
     */
    protected $fillable = [
        'name', 'price', 'slug', 'category_id'
    ];
}

app/Models/Category.php

<?php
  
namespace App\Models;
  
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
  
class Category extends Model
{
    use HasFactory;
  
    /**
     * Get all the products for the Category.
     * 
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function products()
    {
        return $this->hasMany(Product::class);
    }
  
    /**
     * Clone the model into a new, non-existing instance with all the products.
     *
     * @return \App\Models\Category
     */
    public function replicateRow()
    {
       $clon = $this->replicate();
       $clon->push();
       
       $this->products->each(
           fn ($product) => $clon->products()->create($product->toArray())
       );

       return $clon;
    }
}

Controller Code

<?php
  
namespace App\Http\Controllers;
  
use App\Models\Category;
  
class ReplicateController extends Controller
{
    /**
     * Handle the incoming request.
     *
     * @param  \App\Models\Category $category
     * @return void
     */
    public function index(Category $category)
    {
        $newCategory = $category->replicateRow();
  
        dd($newCategory);
    }
}
MrEduar
  • 1,784
  • 10
  • 22