Laravel, простенький backup/restore моделей

На днях понадобилось немного изменить структуру БД в проекте, так чтобы уже созданные модели не пропали. То есть скинуть все данные моделей куда-нибудь, потом сделать изменения, потом данные восстановить. Стал, в общем, писать небольшой бэкапчик, без особых изысков, просто чтоб работал.

В Ларавеле с моделями работать удобно и получить их данные в целом довольно просто.

$user = User::find(1);
$data = $user->toArray();

И все, у нас в массиве есть данные модели, можно их склеить в строку, например, и скинуть в файл. Но есть немного нюансов.

Например, в массиве не будут содержаться поля, которые перечислены в $hidden. Для модели пользователя это поле пароля. А мы бы хотели, конечно, сохранить хэши паролей в бэкапе. Чтобы скрытые поля появились, нужно их специально показать:

$user = User::find(1);
$user->makeVisible(['password']);
$data = $user->toArray();

Кроме того, у модели могут быть отношения, перечисленные в свойстве $with. Эти отношения всегда будут загружаться вместе с моделью и тоже попадут в $data (как вложенные массивы). Но отношения мы будем бэкапить отдельно, поэтому хотелось бы их исключить. Для этого с помощью фасада Schema получим список тех полей, которые действительно хранятся в таблице моделей.

$table = $user->getTable();
$fields = Schema::getColumnListing($table);

Теперь можно просто убрать лишнее из $data:

$user = User::find(1);
$user->makeVisible(['password']);
$data = $user->toArray();
$table = $user->getTable();
$fields = Schema::getColumnListing($table);
$filtered = [];
// filter out relationships
foreach ($fields as $key => $field) {
    if (!in_array($key, $fields)) {
        // relationship
        continue;
    } else {
        $filtered[] = $field;
    }
}

То, что осталось в массиве $filtered — можно склеить в строку и вывести куда-то. Осталось только забэкапить все модели и сделать код независимым от класса модели (чтобы не повторяться):

function exportAllModels($model_class, $make_visible = []) {
  $fields = getFieldNames($model_class);
  foreach ($model_class::all() as $model) {
    $data = exportModel($model, $make_visible);
    // данные в массиве $data можно склеить и куда-нибудь вывести
  }
}

function getFieldNames($model_class) {
    $table = (new $model_class)->getTable();
    return Schema::getColumnListing($table);
}

function exportModel($model, $fields, $make_visible = []) {
    if (!empty($make_visible) {
        $model->makeVisible($make_visible);
    }
    $filtered = [];
    // filter out relationships
    foreach ($fields as $key => $field) {
        if (!in_array($key, $fields)) {
            // relationship
            continue;
        } else {
            $filtered[] = $field;
        }
    }
    return $filtered;
}

В $make_visible будем передавать массив скрытых полей, которые должны попасть в экспорт (например, ['password']). Если данных очень много, можно еще ::all() заменить на ::chunk(). Пример вызова:

exportAllModels(App\User::class, ['password']);

Теперь про обратную операцию. Мы вытащили данные из файла, разобрали, и у нас снова есть массив $data с полями модели. Создать модель можно вот так:

User::create($data);

Но так не удастся сохранить, например, id моделей, потому что Laravel будет учитывать только поля, перечисленные в свойстве $fillable класса модели. А сохранить id хочется — например, чтобы не заморачиваться с зависимыми от модели отношениями и восстановить их с теми же id родителей. Для этого можно временно отключить $fillable, вот так:

User::unguard();
User::create($data);
User::reguard();

Есть и еще одна проблема. Допустим, у модели User есть отношение: одному пользователю положено иметь одну модель UserSettings с какими-нибудь настройками. И мы решили (почему-то), что надо сразу с созданием пользователя по соответствующему событию автоматически создавать и эту модель. Тогда при восстановлении пользователей из бэкапов через User::create(), зависимые модели будут созданы автоматически, и для них уже UserSettings::create() не будет работать, т.к. модели-то уже есть, причем с неправильными id.

Можно еще раз почистить таблицы этих зависимых моделей, прежде чем восстанавливаться из бэкапа. Ну или можно восстанавливать модели не через ::create() а с помощью простой вставки в базу данных через фасад DB. Тогда независимый от класса модели код восстановления будет выглядеть вот так:

function createModel($model_class, $data) {
    $table = (new $model_class)->getTable();
    $fields = getFieldNames($model_class);
    $model = array_combine($fields, $data);
    DB::table($table)->insert($model);
}

В принципе, все довольно просто. В рассмотренном примере не хватает только бэкапа промежуточных таблиц для отношений «много-много», но их можно пустить через обычные DB::select() и DB::insert().

Комментарии