Laravel. Аксессоры и мутаторы
11.11.2020
Теги: JSON • Laravel • PHP • Web-разработка • Класс • Массив • Модель • Теория • Фреймворк
Аксессоры (accessors) и мутаторы (mutators) позволяют модифицировать значения атрибутов Eloquent при их чтении или записи в экземпляры моделей.
Аксессоры (accessors)
Допустим, у нас есть таблица базы данных users
и соотвествующая ей модель User
. В таблице есть поля first_name
(имя) и last_name
(фамилия). При регистрации пользователи могут указать имя и фамилию строчными буквами — например, «сергей иванов». Но мы хотим показывать эти данные правильно, как «Сергей Иванов».
namespace App; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; class User extends Model { /** * Возвращает имя пользователя * * @param string $value * @return string */ public function getFirstNameAttribute($value) { return Str::ucfirst($value); } /** * Возвращает фамилию пользователя * * @param string $value * @return string */ public function getLastNameAttribute($value) { return Str::ucfirst($value); } }
Если пользовать при регистрации указал «СЕРГЕЙ ИВАНОВ» — нам это не поможет, так что изменим эти два метода:
class User extends Model { public function getFirstNameAttribute($value) { return Str::ucfirst(Str::lower($value)); } public function getLastNameAttribute($value) { return Str::ucfirst(Str::lower($value)); } }
Как видите, первоначальное значение столбца передается аксессору, позволяя изменять значение и возвращать его. Чтобы получить доступ к значению аксессора, нужно просто обратиться к атрибуту first_name
экземпляра модели.
$user = App\User::find(1); $firstName = $user->first_name; // Сергей $lastName = $user->last_name; // Иванов
Можно использовать аксессор для формирования атрибута, собранного из других аттрибутов:
/** * Возвращает имя и фамилию пользователя * * @return string */ public function getFullNameAttribute() { return $this->first_name . ' ' . $this->last_name; }
Мутаторы (mutators)
Хорошо, а как изначально изменить значение, перед тем как записать в базу данных? Тут на помощь приходят мутаторы, который позволяют модифицировать значение атрибута модели в момент присваивания значения.
namespace App; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; class User extends Model { /** * Установить имя пользователя * * @param string $value * @return string */ public function setFirstNameAttribute($value) { $this->attributes['first_name'] = Str::ucfirst(Str::lower($value)); } /** * Установить фамилию пользователя * * @param string $value * @return string */ public function setLastNameAttribute($value) { $this->attributes['last_name'] = Str::ucfirst(Str::lower($value)); } }
Теперь в контроллере надо установить значение атрибутов first_name
и last_name
:
namespace App\Http\Admin\Controllers; use App\User; use Illuminate\Http\Request; class UserController extends Controller { public function index() { $users = User::all(); return view('admin.user.index', compact('users')); } public function create() { return view('admin.user.create'); } public function store(Request $request) { $user = new User(); // здесь будут вызваны мутаторы $user->first_name = $request->first_name; $user->last_name = $request->last_name; /* ... */ $user->save(); return redirect() ->route('admin.user.index') ->with('success', 'Новый пользователь создан'); } public function edit(User $user) { return view('admin.user.edit', compact('user')); } public function update(Request $request, User $user) { // здесь будут вызваны мутаторы $user->first_name = $request->first_name; $user->last_name = $request->last_name; /* ... */ $user->save(); return redirect() ->route('admin.user.index') ->with('success', 'Данные пользователя исправлены'); } public function destroy(User $user) { $user->delete(); return redirect() ->route('admin.user.index') ->with('success', 'Пользователь успешно удален'); } }
Мутаторы даты
По умолчанию Eloquent преобразует столбцы created_at
и updated_at
в экземпляры класса Carbon
, которые наследуют php-класс DateTime
и предоставляют ряд полезных методов. Какие поля будут автоматически преобразовываться подобным образом, задается с помощью свойства $dates
модели.
namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model { /** * Атрибуты, которые будут преобразованы в экземпляры Carbon * * @var array */ protected $dates = [ 'published_at', ]; }
created_at
и updated_at
, надо установить в false
public-свойство модели $timestamps
.
Преобразуем столбцы created_at
и updated_at
в экземпляры класса Carbon
и изменим временну́ю зону:
class User extends Model { /** * Преобразует дату и время регистрации пользователя из UTC в Europe/Moscow * * @param $value * @return \Carbon\Carbon|false */ public function getCreatedAtAttribute($value) { return Carbon::createFromFormat('Y-m-d H:i:s', $value) ->timezone('Europe/Moscow')->format('d.m.Y H:i'); } /** * Преобразует дату и время обновления пользователя из UTC в Europe/Moscow * * @param $value * @return \Carbon\Carbon|false */ public function getUpdatedAtAttribute($value) { return Carbon::createFromFormat('Y-m-d H:i:s', $value) ->timezone('Europe/Moscow')->format('d.m.Y H:i'); } }
Теперь в шаблоне можем показать дату и время, которые уже отформатированы как d.m.Y H:i
:
<p>Дата и время регистрации: {{ $user->created_at }} (по Москве)</p>
<p>Дата и время регистрации: 11.11.2020 17:43 (по Москве)</p>
Здесь следует помнить, что вызов метода format()
возвращет строку, содержащую дату и время. Поэтому, если в шаблонах планируется вызывать методы Carbon
, то возвращать надо объект, а не строку.
По умолчанию метки времени отформатированы как Y-m-d H:i:s
. Но это можно изменить, если задать значение для свойства $dateFormat
модели. Это свойство определяет, как атрибуты даты хранятся в базе данных.
namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model { /** * Формат хранения столбцов с датами модели * * @var string */ protected $dateFormat = 'U'; // количество секунд, прошедших с начала эпохи Unix }
Тут есть смысл сделать небольшое отступление и сказать о дате и времени. Чтобы не возникало путаницы, дату-время в базе данных надо хранить в UTC. B MySQL за это отвечает настройка default-time-zone
в секции [mysqld]
.
[mysqld] default-time-zone='+00:00'
И только в момент показа времени пользователю, его можно перевести в подходящий часовой пояс. Например, добавить в таблицу БД users
поле timezone
, которое будет хранить часовые пояса — Europe/Moscow
, Asia/Irkutsk
, Asia/Magadan
.
class User extends Model { public function getCreatedAtAttribute($value) { return Carbon::createFromFormat('Y-m-d H:i:s', $value) ->timezone($this->timezone)->format('d.m.Y H:i'); } public function getUpdatedAtAttribute($value) { return Carbon::createFromFormat('Y-m-d H:i:s', $value) ->timezone($this->timezone)->format('d.m.Y H:i'); } }
$timezones = [ 'Europe/Kaliningrad' => 'Калининград, Россия (+02:00)', 'Europe/Moscow' => 'Москва, Россия (+03:00)', 'Europe/Astrakhan' => 'Астрахань, Россия (+04:00)', 'Asia/Yekaterinburg' => 'Екатеринбург, Россия (+05:00)', 'Asia/Omsk' => 'Омск, Россия (+06:00)', 'Asia/Novosibirsk' => 'Новосибирск, Россия (+07:00)', 'Asia/Irkutsk' => 'Иркутск, Россия (+08:00)', 'Asia/Chita' => 'Чита, Россия (+09:00)', 'Asia/Vladivostok' => 'Владивосток, Россия (+10:00)', 'Asia/Magadan' => 'Магадан, Россия (+11:00)', 'Asia/Kamchatka' => 'Петропавловск-Камчатский, Россия (+12:00)' ];
Мутаторы атрибутов
Свойство $casts
модели предоставляет возможность преобразования атрибутов к общим типам данных. Свойство $casts
должно быть массивом, где ключ — название преобразуемого атрибута, а значение — тип, в который нужно преобразовать столбец. Поддерживаемые типы для преобразования: integer
, real
, float
, double
, decimal:n
, string
, boolean
, object
, array
, collection
, date
, datetime
и timestamp
. В варианте decimal
нужно задать число цифр — decimal:2
.
Для примера, преобразуем атрибут is_admin
, который сохранен в базе данных как integer
(0 или 1) к boolean
:
namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * Атрибуты, которые должны быть преобразованы к базовым типам * * @var array */ protected $casts = [ 'is_admin' => 'boolean', ]; }
1. Преобразование в массив и JSON
Тип array
особенно полезен для преобразования при работе со столбцами, которые хранятся в формате JSON. Например, если в базе данных есть поле типа JSON или TEXT, которое содержит сериализированные JSON данные, добавление преобразования в array
к этому атрибуту автоматически десериализует атрибут в php-массив, во время доступа к нему из модели Eloquent.
namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * Атрибуты, которые должны быть преобразованы к базовым типам * * @var array */ protected $casts = [ 'roles' => 'array', ]; }
$user = App\User::find(5); $roles = ['super' => false, 'admin' => false, 'editor' => true]; $user->roles = $roles; $user->save();
2. Преобразование даты
При использовании правил преобразования date
и datetime
можно сразу указать формат, который будет использоваться при сериализации модели в JSON или массив.
namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * Атрибуты, которые должны быть преобразованы к базовым типам * * @var array */ protected $casts = [ 'created_at' => 'datetime:d.m.Y H:iP', 'updated_at' => 'datetime:d.m.Y H:iP', ]; }
Получим всех пользователей и преобразуем в массив:
$items = User::all()->toArray();
Array ( [0] => Array ( [id] => 1 [name] => Сергей Иванов [email] => ivanov@mail.ru [created_at] => 10.11.2020 14:18+00:00 [updated_at] => 11.11.2020 15:29+00:00 ) [1] => Array ( [id] => 2 [name] => Николай Петров [email] => petrov@mail.ru [created_at] => 10.11.2020 14:20+00:00 [updated_at] => 11.11.2020 15:02+00:00 ) [2] => Array ( [id] => 3 [name] => Михаил Николаев [email] => nikolaev@mail.ru [created_at] => 11.11.2020 12:05+00:00 [updated_at] => 11.11.2020 13:05+00:00 ) )
Получим всех пользователей и преобразуем в JSON:
$items = User::all()->toJson();
[ { "id": 1, "name": "Сергей Иванов", "email": "ivanov@mail.ru", "created_at": "10.11.2020 14:18+00:00", "updated_at": "11.11.2020 12:29+00:00" }, { "id": 2, "name": "Николай Петров", "email": "petrov@mail.ru", "created_at": "10.11.2020 14:20+00:00", "updated_at": "11.11.2020 15:02+00:00" }, { "id": 3, "name": "Михаил Николаев", "email": "nikolaev@mail.ru", "created_at": "11.11.2020 12:05+00:00", "updated_at": "11.11.2020 13:05+00:00" } ]
Здесь есть момент, который мне непонятен. При сериализации модели в массив или JSON, объект даты-времени должен быть преобразован в строку. Если мы преобразование делаем сами с помощью методов getCreatedAtAttribute()
и getUpdatedAtAttribute()
, то на вход приходит строка типа «2020-11-13T10:44:28.000000Z
» вместо «2020-11-13 10:44:28
». Почему это происходит — непонятно, а заканчивается все ошибкой Carbon::CreateFromFormat()
.
getCreatedAtAttribute()
и getUpdatedAtAttribute()
только в моей версии Laravel (7.28.3), в более поздних версиях это было исправлено. Когда доберусь до Laravel 8.x — проверю этот момент.
Чтобы избавиться от этой ошибки, мы можем сами указать, в какой формат преобразовать дату-время при сериализации, для этого нужно добавить в модель метод serializeDate()
:
class User extends Model { public function getCreatedAtAttribute($value) { /* * Если формат даты d.m.Y H:i:s, значит вызван метод модели * toArray() или toJson() — и мы возвращаем дату-время в UTC */ if (preg_match('~^\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}:\d{2}$~', $value)) { return Carbon::createFromFormat('d.m.Y H:i:s', $value) ->format('d.m.Y H:i:sP'); } /* * Если формат даты Y-m-d H:i:s — преобразуем дату-время из * UTC в часовой пояс этого пользователя */ return Carbon::createFromFormat('Y-m-d H:i:s', $value) ->timezone($this->timezone) ->format('d.m.Y H:i'); } public function getUpdatedAtAttribute($value) { /* * Если формат даты d.m.Y H:i:s, значит вызван метод модели * toArray() или toJson() — возвращаем дату-время в UTC */ if (preg_match('~^\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}:\d{2}$~', $value)) { return Carbon::createFromFormat('d.m.Y H:i:s', $value) ->format('d.m.Y H:i:sP'); } /* * Если формат даты Y-m-d H:i:s — преобразуем дату-время из * UTC в часовой пояс этого пользователя */ return Carbon::createFromFormat('Y-m-d H:i:s', $value) ->timezone($this->timezone) ->format('d.m.Y H:i'); } protected function serializeDate(\DateTimeInterface $date) { return $date->format('d.m.Y H:i:s'); } }
При сериализации модели на вход методов getCreatedAtAttribute()
и getUpdatedAtAttribute()
будет приходить строка формата «d.m.Y H:i:s
», в противном случае — строка формата «Y-m-d H:i:s
». И уже имея эти данные — можем возвращать разный результат. При этом мы используем поле timezone
таблицы users
, которое хранит часовой пояс пользователя — Europe/Moscow
, Asia/Irkutsk
, Asia/Magadan
.
Поиск: JSON • Laravel • PHP • Web-разработка • Класс • Массив • Модель • Теория • Фреймворк