Я считаю, что одним из самых плохих качеств системы является непредсказуемость. Когда программа работает в 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() станет более предсказуемым. Надеюсь, что кому-нибудь - эта статья поможет съкономить немного времени на отладку своего приложения.