Sklonować elokwentny obiekt zawierający wszystkie relacje?

86

Czy istnieje sposób na łatwe sklonowanie elokwentnego obiektu, w tym wszystkich jego relacji?

Na przykład, gdybym miał takie tabele:

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

Oprócz utworzenia nowego wiersza w userstabeli, z takimi samymi kolumnami, z wyjątkiem id, należy również utworzyć nowy wiersz w user_rolestabeli, przypisując tę ​​samą rolę nowemu użytkownikowi.

Coś takiego:

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

Gdzie model użytkownika ma

class User extends Eloquent {
    public function roles() {
        return $this->hasMany('Role', 'user_roles');
    }
}
andrewtweber
źródło

Odpowiedzi:

74

przetestowany w laravel 4.2 pod kątem relacji shouldToMany

jeśli jesteś w modelu:

    //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
źródło
3
Pracował w Laravel 7
Daniyal Javani
Działa również na poprzedniej wersji Laravel 6. (myślę, że oczekuje się tego na podstawie poprzedniego komentarza :)) Dzięki!
mmmdearte
Pracował w Laravel 7.28.4. Zauważyłem, że kod powinien być inny, jeśli próbujesz uruchomić go poza modelem. Dzięki
Roman Grinev
56

Możesz także wypróbować funkcję replikacji dostarczoną przez 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
źródło
7
W rzeczywistości musisz również załadować relacje, które chcesz replikować. Podany kod będzie replikował tylko model podstawowy bez jego relacji. Aby sklonować również relacje, możesz albo pobrać użytkownika z jego relacjami: $user = User::with('roles')->find(1);lub załadować je po uzyskaniu modelu: więc pierwsze dwie linie będą wyglądały tak$user = User::find(1); $user->load('roles');
Alexander Taubenkorb
2
Ładowanie relacji nie wydaje się również replikować relacji, przynajmniej nie w 4.1. Musiałem zreplikować rodzica, a następnie przejrzeć w pętli elementy potomne oryginału, powielając je i aktualizując pojedynczo, aby wskazywały na nowego rodzica.
Rex Schrader
replicate()ustawi relacje i push()powróci do relacji i zapisze je.
Matt K
Również w 5.2 musisz przejrzeć elementy potomne i zapisać je po powieleniu pojedynczo; inside a foreach:$new_user->roles()->save($oldRole->replicate)
d.grassi84
28

Możesz spróbować tego ( klonowanie obiektów ):

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

Ponieważ clonenie kopiuje głęboko, więc obiekty podrzędne nie zostaną skopiowane, jeśli jest dostępny jakikolwiek obiekt podrzędny, aw tym przypadku musisz skopiować obiekt podrzędny cloneręcznie. Na przykład:

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

W twoim przypadku rolesbędzie to zbiór Roleobiektów więc każdy Role objectw kolekcji musi być skopiowany ręcznie za pomocą clone.

Ponadto musisz być tego świadomy, jeśli nie załadujesz pliku rolesusing, withwtedy te nie zostaną załadowane lub nie będą dostępne w $userwywołaniu, a kiedy zadzwonisz, $user->roleste obiekty zostaną załadowane w czasie wykonywania po tym wywołaniu od $user->rolesa do tego te rolesnie są ładowane.

Aktualizacja:

Ta odpowiedź była dla Larave-4i teraz Laravel oferuje replicate()metodę, na przykład:

$user = User::find(1);
$newUser = $user->replicate();
// ...
Alpha
źródło
2
Uważaj, tylko płytka kopia, a nie obiekty podrzędne / podrzędne :-)
Alpha
1
@TheShiftExchange, może Cię to zainteresować , dawno temu przeprowadziłem eksperyment. Dzięki za kciuki do góry :-)
Alpha
1
Czy to również nie kopiuje identyfikatora obiektu? Bezużyteczne do oszczędzania?
Tosh
@Tosh, tak, dokładnie i dlatego trzeba ustawić inny id lub null:-)
Alpha
1
plus 1 za ujawnienie sekretu php: P
Metabolic
21

Dla Laravel 5. Testowane z relacją 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
źródło
7

Oto zaktualizowana wersja rozwiązania z @ sabrina-gelbart, która sklonuje wszystko hasMany, a nie tylko nalezy do przynależności, jak napisała:

    //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
źródło
Trudne, jeśli some_parent_idnie jest takie samo dla wszystkich relacji. Jest to jednak przydatne, dzięki.
Dustin Graham
5

To jest laravel 5.8, którego nie próbowałem w starszej wersji

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

edytuj, właśnie dzisiaj 7 kwietnia 2019 ruszył laravel 5.8.10

może teraz użyć replicate

$post = Post::find(1);
$newPost = $post->replicate();
$newPost->save();
david valentino
źródło
2

Jeśli masz kolekcję o nazwie $ user, używając poniższego kodu, tworzy nową kolekcję identyczną ze starą, zawierającą wszystkie relacje:

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

ten kod dotyczy laravel 5.

Mihai Crăiță
źródło
1
Czy nie mógłbyś po prostu tego zrobić $new = $old->slice(0)?
fubar
2

Kiedy pobierasz obiekt za pomocą dowolnej relacji, którą chcesz, a następnie replikujesz, wszystkie pobrane relacje również są replikowane. na przykład:

$oldUser = User::with('roles')->find(1);
$newUser = $oldUser->replicate();
elyas.m
źródło
Testowałem w Laravel 5.5
elyas.m
2

Oto cecha, która rekurencyjnie powiela wszystkie załadowane relacje na obiekcie. Możesz łatwo rozszerzyć to na inne typy relacji, takie jak przykład Sabriny dla nalezy do Mnie.

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

Stosowanie:

//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
źródło
0

Oto inny sposób, aby to zrobić, jeśli inne rozwiązania Cię nie uspokajają:

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

Sztuczka polega na wyczyszczeniu właściwości idi exists, aby Laravel utworzył nowy rekord.

Klonowanie relacji między sobą jest trochę skomplikowane, ale podałem przykład. Musisz tylko utworzyć mapowanie starych identyfikatorów na nowe, a następnie ponownie zsynchronizować.

mpen
źródło