Клонировать объект Eloquent, включая все отношения?


86

Есть ли способ легко клонировать объект Eloquent, включая все его отношения?

Например, если бы у меня были эти таблицы:

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

В дополнение к созданию новой строки в usersтаблице со всеми одинаковыми столбцами, кроме того id, он должен также создать новую строку в user_rolesтаблице, назначая ту же роль новому пользователю.

Что-то вроде этого:

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

Где модель User имеет

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

Ответы:


74

протестирован в laravel 4.2 для отношений ownToMany

если вы в модели:

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

3
Работал в Laravel 7
Даниал Джавани

Он также работает с предыдущей версией Laravel 6. (я предполагаю, что это ожидается на основе предыдущего комментария :)) Спасибо!
mmmdearte

Работал в Laravel 7.28.4. Я заметил, что код должен отличаться, если вы пытаетесь запустить его вне модели. Спасибо
Роман Гринев

56

Вы также можете попробовать функцию репликации, предоставляемую 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();

7
На самом деле вам также нужно загрузить отношения, которые вы хотите воспроизвести. Данный код будет копировать только базовую модель без ее связей. Для клонирования отношений , а также, вы можете получить пользователь с его отношениями: $user = User::with('roles')->find(1);или загрузить их после того, как у вас есть модель: так что первые две строки будут$user = User::find(1); $user->load('roles');
Александр Taubenkorb

2
Загрузка отношений, похоже, также не воспроизводит отношения, по крайней мере, в 4.1. Мне пришлось реплицировать родительский элемент, затем перебрать дочерние элементы оригинального реплицированного и обновить их по одному, чтобы указать на нового родителя.
Рекс Шрадер

replicate()установит отношения и push()рекурсивно перейдет в отношения и сохранит их.
Matt K

Также в 5.2 вам нужно перебрать потомков и сохранить их после репликации по одному; внутри foreach:$new_user->roles()->save($oldRole->replicate)
d.grassi84 09

28

Вы можете попробовать это ( клонирование объектов ):

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

Поскольку cloneне выполняется глубокое копирование, дочерние объекты не будут скопированы, если доступен какой-либо дочерний объект, и в этом случае вам нужно скопировать дочерний объект cloneвручную. Например:

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

В вашем случае rolesэто будет набор Roleобъектов, поэтому каждый Role objectиз них необходимо скопировать вручную с помощью clone.

Кроме того, вам нужно знать, что если вы не загружаете rolesиспользование, withони не будут загружены или не будут доступны в, $userи когда вы вызовете, $user->rolesэти объекты будут загружены во время выполнения после этого вызова. от $user->rolesи до этого, тем rolesне не загружены.

Обновить:

Этот ответ был для Larave-4и теперь Laravel предлагает replicate()метод, например:

$user = User::find(1);
$newUser = $user->replicate();
// ...

2
Будьте осторожны, только мелкая копия, а не дочерние / дочерние объекты :-)
Альфа

1
@TheShiftExchange, может вас заинтересует , давно проводил эксперимент. Спасибо за поднятие большого пальца :-)
Альфа

1
Разве это не копирует также идентификатор объекта? Делая это бесполезным для экономии?
Tosh

@Tosh, да, именно поэтому и нужно ставить другой id или null:-)
Альфа

1
plus1 за раскрытие секрета php: P
Metabolic

21

Для Laravel 5. Протестировано с отношением 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());
    }
}

7

Вот обновленная версия решения от @ sabrina-gelbart, которая будет клонировать все отношения hasMany, а не только ownToMany, как она опубликовала:

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

Сложно, если some_parent_idне одинаково для всех отношений. Хотя это полезно, спасибо.
Дастин Грэм

5

Это в laravel 5.8, не пробовал в более старой версии

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

edit, только сегодня, 7 апреля 2019 года, запущен laravel 5.8.10

теперь можно использовать репликацию

$post = Post::find(1);
$newPost = $post->replicate();
$newPost->save();

2

Если у вас есть коллекция с именем $ user, используя приведенный ниже код, она создает новую коллекцию, идентичную старой, включая все отношения:

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

этот код предназначен для laravel 5.


1
Не могли бы вы просто сделать $new = $old->slice(0)?
fubar

2

Когда вы выбираете объект по любому желаемому отношению, а затем реплицируете его, все полученные вами отношения также реплицируются. например:

$oldUser = User::with('roles')->find(1);
$newUser = $oldUser->replicate();

Я тестировал в Laravel 5.5
elyas.m

2

Вот черта, которая рекурсивно дублирует все загруженные отношения объекта. Вы можете легко расширить это для других типов отношений, таких как пример Сабрины для ownToMany.

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

Применение:

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

0

Вот еще один способ сделать это, если другие решения вас не устраивают:

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

Хитрость заключается в том, чтобы стереть idиexists свойства так , что Laravel создаст новый рекорд.

Клонирование самоотношений немного сложно, но я привел пример. Вам просто нужно создать сопоставление старых идентификаторов с новыми идентификаторами, а затем выполнить повторную синхронизацию.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.