Я считаю, что одним из самых плохих качеств системы является непредсказуемость. Когда программа работает в 50% случаев, это хуже, чем если бы она не работала вообще.
В своей работе я использую laravel 5. Мне нравятся многие из подходов, которые в нем применены. Одной из вещей, которую я активно использую - это Attribute Casting. Это небольшой синтаксический сахар, при котором ORM остается в рамках Active Record, но при этом у разработчиков появляется механизм работы со сложными полями.
На днях я занимался синхронизацией кода между несколькими сервисами и заметил, что на один из сервисов приходят неверные данные. Вернее данные были верные, но они были дополнительно экранированы, чего быть не должно было. Небольшое расследование почему так произошло ниже.
В проекте для важнейших объектов используется версионирование, которое сделано за счет дополнительной таблицы в БД. Важно что одно из полей является массивом, который складывается в json при сохранении в БД. Код создающий версии следующий:
/**
* @property array $gateway
*/
class User extends Illuminate\Database\Eloquent\Model {
protected $casts = [
'gateway' => 'array',
];
public static function boot()
{
parent::boot();
static::saving(function (User $user) {
$userHistory = UserHistory::create(
array_merge([
'date_added' => date('Y-m-d H:i:s'),
], $user->getDirty()
)
);
return true;
});
}
/**
* @property array $gateway
*/
class UserHistory extends Illuminate\Database\Eloquent\Model {
protected $casts = [
'gateway' => 'array',
];
}
То есть при сохранении, проверяется какие поля изменились и новые значения записываются в таблицу UserHistory
БД. Кажется, что все верно, но если посмотреть на записи в БД, то будет любопытная картина:
Таблица "User" поле gateway :
#{"email":{"enabled":true,"balance":291165},"sms":{"enabled":true,"balance":10000}}#
Таблица "UserHistory" поле gateway (после вставки):
#"{\"email\":{\"enabled\":true,\"balance\":291165},\"sms\":{\"enabled\":true,\"balance\":10000}}"#
Записи почти идентичны, но первая является json массивом, а вторая строкой. При этом значения остальных полей отображаются корректно.
Пройдясь xdebug по коду замечено, что при получении измененных полей, gateway возвращается, так же как записан в БД - в виде строки. Честно говоря, я сильно смутился этому: если посмотреть на документацию метода getDirty()
:
array getDirty()
Get the attributes that have been changed since last sync.
Добавляя к этому то, что написано в про кастинг атрибутов, более логичным кажется, что getDirty()
должно вернуть уже кастованные аттрибуты. Конечно, это просто недокументированное поведение, но обсуждая это в slack и на форуме я понял, что самое неприятное в этом поведении - это его неочевидность:
/**
* @property array cars
*/
class A extend Model {
protected $casts = [
'cars' => 'array',
];
}
$a = A::first(); // select first record from database
$cars = [
'lada' => 1
]; // value for field
$a->cars =$cars; // let`s set value of object
$dirty = $a->getDirty(); // return array where expected get value that we set
$cars === $dirty['cars'] // FALSE because of $dirty['cars'] - return string
$attributes = $a->getAttributes();
$cars === $attributes['cars'] // TRUE
Мне очень хочется верить, что в одном из следующий релизов поведение метода getDirty() станет более предсказуемым. Надеюсь, что кому-нибудь - эта статья поможет съкономить немного времени на отладку своего приложения.