Laravel. Аксессоры и мутаторы

11.11.2020

Теги: JSONLaravelPHPWeb-разработкаКлассМассивМодельТеорияФреймворк

Аксессоры (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-разработка • Класс • Массив • Модель • Теория • Фреймворк

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.