¿Clonar un objeto Eloquent incluyendo todas las relaciones?

86

¿Hay alguna forma de clonar fácilmente un objeto Eloquent, incluidas todas sus relaciones?

Por ejemplo, si tuviera estas tablas:

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

Además de crear una nueva fila en la userstabla, con todas las columnas iguales excepto id, también debe crear una nueva fila en la user_rolestabla, asignando el mismo rol al nuevo usuario.

Algo como esto:

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

Donde el modelo de usuario tiene

class User extends Eloquent {
    public function roles() {
        return $this->hasMany('Role', 'user_roles');
    }
}
andrewtweber
fuente

Respuestas:

74

probado en laravel 4.2 para las relaciones pertenece a muchas

si estás en el modelo:

    //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
fuente
3
Trabajó en Laravel 7
Daniyal Javani
También funciona en la versión anterior de Laravel 6. (Supongo que se espera según el comentario anterior :)) ¡Gracias!
mmmdearte
Trabajado en Laravel 7.28.4. He notado que el código debería ser diferente si intenta ejecutarlo fuera del modelo. Gracias
Roman Grinev
56

También puede probar la función de replicación proporcionada por 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();
Piotr Borek
fuente
7
En realidad, también debe cargar las relaciones que desea replicar. El código dado solo replicará el modelo base sin sus relaciones. Para clonar las relaciones también, puede obtener el usuario con sus relaciones: $user = User::with('roles')->find(1);o cargarlas después de tener el Modelo: por lo que las dos primeras líneas serían$user = User::find(1); $user->load('roles');
Alexander Taubenkorb
2
Cargar las relaciones no parece replicar también las relaciones, al menos no en 4.1. Tuve que replicar el padre, luego recorrer los elementos secundarios del original, replicarlos y actualizarlos uno a la vez para apuntar al nuevo padre.
Rex Schrader
replicate()establecerá las relaciones y los push()recurrentes en las relaciones y las guardará.
Matt K
También en 5.2 necesita recorrer los niños y guardarlos después de replicar uno a la vez; dentro de un foreach:$new_user->roles()->save($oldRole->replicate)
d.grassi84
28

Puede probar esto ( Clonación de objetos ):

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

Dado cloneque no hace una copia en profundidad, los objetos secundarios no se copiarán si hay algún objeto secundario disponible y, en este caso, debe copiar el objeto secundario clonemanualmente. Por ejemplo:

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

En su caso roles, habrá una colección de Roleobjetos, por lo que cada uno Role objecten la colección debe copiarse manualmente usando clone.

Además, debe tener en cuenta que, si no carga el rolesuso, esos objetos withno se cargarán o no estarán disponibles en el $usery cuando llame $user->roles, esos objetos se cargarán en el tiempo de ejecución después de esa llamada. de $user->rolesy hasta este, esos rolesno se cargan.

Actualizar:

Esta respuesta fue para Larave-4y ahora Laravel ofrece un replicate()método, por ejemplo:

$user = User::find(1);
$newUser = $user->replicate();
// ...
El alfa
fuente
2
Tenga cuidado, solo una copia superficial, no los objetos secundarios / secundarios :-)
The Alpha
1
@TheShiftExchange, puede que le resulte interesante , hice un experimento hace mucho tiempo. Gracias por el visto bueno :-)
The Alpha
1
¿No copia esto también la identificación del objeto? ¿Haciéndolo inútil para ahorrar?
Tosh
@Tosh, sí, exactamente y es por eso que necesita establecer otra identificación o null:-)
The Alpha
1
plus1 para revelar el secreto de php: P
Metabólico
21

Para Laravel 5. Probado con la relación hasMany.

$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
fuente
7

Aquí hay una versión actualizada de la solución de @ sabrina-gelbart que clonará todas las relaciones de hasMany en lugar de solo las pertenece a Many como ella publicó:

    //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
fuente
Difícil si some_parent_idno es lo mismo para todas las relaciones. Sin embargo, esto es útil, gracias.
Dustin Graham
4

Esto está en laravel 5.8, no lo he probado en una versión anterior

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

editar, solo hoy 7 de abril de 2019 se lanzó laravel 5.8.10

puede usar replicar ahora

$post = Post::find(1);
$newPost = $post->replicate();
$newPost->save();
david valentino
fuente
2

Si tiene una colección llamada $ user, usando el código a continuación, crea una nueva colección idéntica a la anterior, incluyendo todas las relaciones:

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

este código es para laravel 5.

Mihai Crăiță
fuente
1
¿No podrías simplemente hacer $new = $old->slice(0)?
fubar
2

Cuando recupera un objeto por cualquier relación que desee, y luego lo replica, todas las relaciones que recupera también se replican. por ejemplo:

$oldUser = User::with('roles')->find(1);
$newUser = $oldUser->replicate();
elyas.m
fuente
Probé
2

Aquí hay un rasgo que duplicará de forma recursiva todas las relaciones cargadas en un objeto. Puede expandir esto fácilmente para otros tipos de relaciones como el ejemplo de Sabrina para pertenece a muchos.

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});
        }
    }
}

Uso:

//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 Berce
fuente
0

Aquí hay otra forma de hacerlo si las otras soluciones no lo apaciguan:

<?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);
}

El truco consiste en borrar las propiedades idy existspara que Laravel cree un nuevo registro.

Clonar las relaciones con uno mismo es un poco complicado, pero he incluido un ejemplo. Solo tiene que crear un mapeo de identificadores antiguos a identificadores nuevos y luego volver a sincronizar.

mpen
fuente