Clonar um objeto Eloquent incluindo todos os relacionamentos?

86

Existe alguma maneira de clonar facilmente um objeto Eloquent, incluindo todos os seus relacionamentos?

Por exemplo, se eu tivesse essas tabelas:

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

Além de criar uma nova linha na userstabela, com todas as colunas iguais id, exceto , também deve criar uma nova linha na user_rolestabela, atribuindo a mesma função ao novo usuário.

Algo assim:

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

Onde o modelo de usuário tem

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

Respostas:

74

testado no laravel 4.2 para relacionamentos belongsToMany

se você estiver no 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
fonte
3
Trabalhou no Laravel 7
Daniyal Javani
Também funciona na versão anterior do Laravel 6. (acho que é esperado com base no comentário anterior :)) Obrigado!
mmmdearte
Trabalhou no Laravel 7.28.4. Percebi que o código deve ser diferente se você estiver tentando executá-lo fora do modelo. Obrigado
Roman Grinev
56

Você também pode tentar a função replicar fornecida 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
fonte
7
Na verdade, você também deve carregar os relacionamentos que deseja replicar. O código fornecido irá apenas replicar o modelo básico sem suas relações. Para clonar os relacionamentos também, você pode obter o usuário com seus relacionamentos: $user = User::with('roles')->find(1);ou carregá-los depois de ter o modelo: portanto, as duas primeiras linhas seriam$user = User::find(1); $user->load('roles');
Alexander Taubenkorb
2
Carregar os relacionamentos não parece replicar também os relacionamentos, pelo menos não em 4.1. Tive de replicar o pai, depois percorrer os filhos do original replicado e atualizá-los um por vez para apontar para o novo pai.
Rex Schrader
replicate()definirá as relações e push()recursará nas relações e as salvará.
Matt K
Também no 5.2, você precisa percorrer os filhos e salvá-los depois de replicar um por vez; dentro de um foreach:$new_user->roles()->save($oldRole->replicate)
d.grassi84
28

Você pode tentar isso ( Clonagem de objeto ):

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

Como clonenão copia profundamente, os objetos filho não serão copiados se houver algum objeto filho disponível e, neste caso, você precisa copiar o objeto filho clonemanualmente. Por exemplo:

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

No seu caso, roleshaverá uma coleção de Roleobjetos, portanto, cada um Role objectna coleção precisa ser copiado manualmente usando clone.

Além disso, você precisa estar ciente de que, se você não carregar o rolesusing with, eles não serão carregados ou não estarão disponíveis no $usere quando você chamar $user->roles, esses objetos serão carregados em tempo de execução após essa chamada de $user->rolese até isso, aqueles rolesnão são carregados.

Atualizar:

Essa resposta era para Larave-4e agora o Laravel oferece o replicate()método, por exemplo:

$user = User::find(1);
$newUser = $user->replicate();
// ...
O alfa
fonte
2
Tenha cuidado, apenas uma cópia superficial, não os objetos sub / filhos :-)
The Alpha
1
@TheShiftExchange, você pode achar interessante , eu fiz um experimento há muito tempo. Obrigado pelos polegares para cima :-)
The Alpha
1
Isso também não copia o id do objeto? Tornando-o inútil para salvar?
Tosh
@Tosh, Sim, exatamente e é por isso que você precisa definir outro id ou null:-)
The Alpha
1
plus1 para revelar o segredo do php: P
Metabólico de
21

Para Laravel 5. Testado com relação 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
fonte
7

Aqui está uma versão atualizada da solução de @ sabrina-gelbart que clonará todos os relacionamentos hasMany em vez de apenas o belongsToMany como ela postou:

    //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
fonte
É complicado se some_parent_idnão for o mesmo para todos os relacionamentos. Porém, isso é útil, obrigado.
Dustin Graham
4

Este é o laravel 5.8, não tentei na versão anterior

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

editar, apenas hoje, 7 de abril de 2019, laravel 5.8.10 lançado

pode usar replicar agora

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

Se você tem uma coleção chamada $ user, usando o código abaixo, ela cria uma nova coleção idêntica à antiga, incluindo todas as relações:

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

este código é para laravel 5.

Mihai Crăiță
fonte
1
Você não poderia simplesmente fazer $new = $old->slice(0)?
fubar
2

Quando você busca um objeto por qualquer relação desejada e replica depois disso, todas as relações recuperadas também são replicadas. por exemplo:

$oldUser = User::with('roles')->find(1);
$newUser = $oldUser->replicate();
elyas.m
fonte
Eu testei no Laravel 5.5
elyas.m
2

Aqui está uma característica que irá duplicar recursivamente todos os relacionamentos carregados em um objeto. Você pode facilmente expandir isso para outros tipos de relacionamento, como o exemplo de Sabrina para 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});
        }
    }
}

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
fonte
0

Esta é outra maneira de fazer isso se as outras soluções não o agradarem:

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

O truque é limpar as propriedades ide existspara que o Laravel crie um novo registro.

Clonar relacionamentos pessoais é um pouco complicado, mas incluí um exemplo. Você só precisa criar um mapeamento de ids antigos para novos ids e, em seguida, sincronizar novamente.

mpen
fonte