На днях понадобилось немного изменить структуру БД в проекте, так чтобы уже созданные модели не пропали. То есть скинуть все данные моделей куда-нибудь, потом сделать изменения, потом данные восстановить. Стал, в общем, писать небольшой бэкапчик, без особых изысков, просто чтоб работал.
В Ларавеле с моделями работать удобно и получить их данные в целом довольно просто.
$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()
.