[Из песочницы] Решение типовых проблем с json_encode (PHP)

?v=1

Это краткая статья о наиболее вероятных проблемах с json_encode и их решениях. Иногда при кодировании данных в json, с помощью json_encode в php, мы получаем не тот результат который ожидаем. Я выделил три наиболее частые проблемы с которыми сталкиваются программисты:


  • доступ к полям
  • кодировка текстовых значений
  • цифровые значения


Доступ к полям

Проблема заключается в том что json_encode имеет доступ только к публичным полям объекта. Например если у вас есть класс

class Example {
    public $publicProperty;
    protected $protectedProperty;
    private $privateProperty;

    public function __construct($public, $protected, $private)
    {
        $this->publicProperty = $public;
        $this->protectedProperty = $protected;
        $this->privateProperty = $private;
    }
}

то результатом выполнения следующего кода будет:

$obj = new Example("some", "value", "here"); 
echo json_encode($obj);

// {"publicProperty":"some"}

как видно в результирующий json были включены только публичные поля.
Что же делать если нужны все поля?


Решение

Для php < 5.4:
нам необходимо будет реализовать в классе метод который будет возвращать готовый json. Т.к. внутри класса есть доступ к всем полям можно сформировать правильное представление объекта для json_encode

class Example {
    public $publicProperty;
    protected $protectedProperty;
    private $privateProperty;

    public function __construct($public, $protected, $private)
    {
        $this->publicProperty = $public;
        $this->protectedProperty = $protected;
        $this->privateProperty = $private;
    }

    public function toJson()
    {
        return json_encode([
            'publicProperty' => $this->publicProperty,
            'protectedProperty' => $this->protectedProperty,
            'privateProperty' => $this->privateProperty,
        ]);
    }
}

Для получение json-a c объекта теперь нужно пользоваться методом toJson, а не прямым применением json_encode к объекту

$obj = new Example("some", "value", "here"); 
echo $obj->toJson();

Для php >= 5.4:
достаточно будет реализовать интерфейс JsonSerializable для нашего класса, что подразумевает добавление метода jsonSerialize который будет возвращать структуру представляющую объект для json_encode

class Example implements JsonSerializable
{
    public $publicProperty;
    protected $protectedProperty;
    private $privateProperty;

    public function __construct($public, $protected, $private)
    {
        $this->publicProperty = $public;
        $this->protectedProperty = $protected;
        $this->privateProperty = $private;
    }

    public function jsonSerialize() 
    {
        return [
            'publicProperty' => $this->publicProperty,
            'protectedProperty' => $this->protectedProperty,
            'privateProperty' => $this->privateProperty,
        ];
    }
}

Теперь мы можем использовать json_encode как и раньше

$obj = new Example("some", "value", "here"); 
echo json_encode($obj);

// {"publicProperty":"some","protectedProperty":"value","privateProperty":"here"}


Почему не стоит использовать подход с toJson методом?

Многие наверно заметили что подход с созданием метода возвращающего json может быть использован и в версиях php >= 5.4. Так почему же не воспользоваться им? Все дело в том что ваш класс может быть использован как часть иной структуры данных

echo json_encode([
    'status' => true,
    'message' => 'some message',
    'data' => new Example("some", "value", "here"),
]);

и результат уже будет совсем другой.
Также класс может использоваться другими программистами, для которых такой тип получение json-а с объекта может быть не совсем очевиден.

Что если у меня очень много полей в класcе?

В таком случае можно воспользоваться функцией get_object_vars

class Example implements JsonSerializable
{
    public $publicProperty;
    protected $protectedProperty;
    private $privateProperty;
    protected $someProp1;
...
    protected $someProp100500;

    public function __construct($public, $protected, $private)
    {
        $this->publicProperty = $public;
        $this->protectedProperty = $protected;
        $this->privateProperty = $private;
    }

    public function jsonSerialize() 
    {
        $fields = get_object_vars($this);
        // что-то делаем ...
        return $fields;
    }
}


А если нужно private поля с класса который нет возможности редактировать?

Может получиться ситуация когда нужно получить private поля (именно private, т.к. доступ к protected полям можно получить через наследование) в json-е. В таком случае необходимо будет воспользоваться рефлексией:

class Example
{
    public $publicProperty = "someValue";
    protected $protectedProperty;
    private $privateProperty1;
    private $privateProperty2;
    private $privateProperty3;

    public function __construct($privateProperty1, $privateProperty2, $privateProperty3, $protectedProperty)
    {
        $this->protectedProperty = $protectedProperty;
        $this->privateProperty1 = $privateProperty1;
        $this->privateProperty2 = $privateProperty2;
        $this->privateProperty3 = $privateProperty3;
    }
}

$obj = new Example("value1", 12, "21E021", false);
$reflection = new ReflectionClass($obj);

$public = [];

foreach ($reflection->getProperties() as $property) {
    $property->setAccessible(true);
    $public[$property->getName()] = $property->getValue($obj);
}

echo json_encode($public);

//{"publicProperty":"someValue","protectedProperty":false,"privateProperty1":"value1","privateProperty2":12,"privateProperty3":"21E021"}


Кодировка текстовых значений


Кириллица и другие знаки в UTF8

Второй тип распространённых проблем с json_encode это проблемы с кодировкой. Часто текстовые значения которые нужно кодировать в json имеют в себе символы в UTF8 (в том числе кириллица) в результате эти символы будут представлены в виде кодов:

echo json_encode("кирилица or ₳ ƒ 元 ﷼ ₨ ௹ ¥ ₴ £ ฿ $");

// "\u043a\u0438\u0440\u0438\u043b\u0438\u0446\u0430 or \u20b3 \u0192 \u5143 \ufdfc \u20a8 \u0bf9 \uffe5 \u20b4 \uffe1 \u0e3f \uff04"

Отображение таких символов лечиться очень просто — добавлением флага JSON_UNESCAPED_UNICODE вторым аргументом к функции json_encode:

echo json_encode("кирилица or ₳ ƒ 元 ﷼ ₨ ௹ ¥ ₴ £ ฿ $", JSON_UNESCAPED_UNICODE);
// "кирилица or ₳ ƒ 元 ﷼ ₨ ௹ ¥ ₴ £ ฿ $"


Символы в других кодировках

Функция json_encode воспринимает строковые значения как строки в UTF8, что может вызвать ошибку, если кодеровка другая. Рассмотрим маленький кусочек кода (данный пример кода максимально упрощен для демонстрации проблемной ситуации)

echo json_encode(["p" => $_GET['p']]);

На первый взгляд ничего не предвещает проблем, да и что здесь может пойти не так? Я тоже так думал. В подавляющем большинстве случаев все будет работать, и по этой причине поиск проблемы занял у меня несколько больше времени, когда я впервые столкнулся с тем что результатом json_encode было false.

Для воссоздания такой ситуации предположим что p=%EF%F2%E8%F6%E0 (на пример: localhost?=%EF%F2%E8%F6%E0).
*Переменные в суперглобальных массивах $_GET и $_REQUEST уже декодированы.

$decoded = urldecode("%EF%F2%E8%F6%E0");

var_dump(json_encode($decoded));
// bool(false)

var_dump(json_last_error_msg());
// string(56) "Malformed UTF-8 characters, possibly incorrectly encoded"

Как можно увидеть из ошибки: проблема с кодировкой переданной строки (это не UTF8). Решение проблемы очевидное — привести значение в UTF8

$decoded = urldecode("%EF%F2%E8%F6%E0");
$utf8 = utf8_encode($decoded);

echo json_encode($utf8);
// "ïòèöà"


Цифровые значения

Последняя типовая ошибка связана с кодированием числовых значений.

Например:

echo json_encode(["string_float" => "3.0"]);
// {"string_float":"3.0"}

Как известно php не строго типизированный язык и позволяет использовать числа записаны в виде строки, в большинстве случаев это не приводит к ошибкам внутри php приложения. Но так как json очень часто используется для передачи сообщений между приложениями, такой формат записи числа может вызвать проблемы в другом приложении. Желательно использовать флаг JSON_NUMERIC_CHECK:

echo json_encode(["string_float" => "3.0"], JSON_NUMERIC_CHECK);
// {"string_float":3}

Уже лучше. Но как видим »3.0» превратилось в 3, что в большинстве случаев будет интерпретировано как int. Используем еще один флаг JSON_PRESERVE_ZERO_FRACTION для корректного преобразования в float:

echo json_encode(["string_float" => "3.0"], JSON_NUMERIC_CHECK | JSON_PRESERVE_ZERO_FRACTION);
// {"string_float":3.0}

Прошу так же обратить внимание на следующий фрагмент кода, что иллюстрирует ряд возможных проблем с json_encode и числовыми значениями:

$data = [
    "0000021", // нули слева
    6.12345678910111213, // много знаков после точки (будет округленно)
    "+81011321515", // телефон
    "21E021", // экспоненциальная запись
];

echo json_encode($data, JSON_NUMERIC_CHECK);
//[
//    21,
//    6.1234567891011,
//    81011321515,
//    2.1e+22
// ]

Спасибо за прочтение.

Буду рад увидеть в комментариях описание проблем,  с которыми вы сталкивались, что не были упомянуты в статье

 

© Habrahabr.ru