frickenate

Tag Archive: doctrine

Custom JSON data type for Doctrine

Existing support for JSON in Doctrine

It turns out Doctrine does ship with a built-in JSON data type. The name of the data type being json_array hints at the potential for problems should one attempt to store values that are valid JSON without being an array. Inspecting the code for Doctrine’s \Doctrine\DBAL\Types\JsonArrayType class, the first thing we note is that Doctrine doesn’t put its nose in the values you store through this data type. It simply uses a call to json_encode() to convert a PHP value to a column’s storage value, and then a call to json_decode() when hydrating your value from the database to userland code. So far so good, it seems that we can probably get away with using json_array for all json data types.

The null problem

Sadly there is one problem: a column defined with the json_array type cannot properly handle the null value. When this data type was conceived, it was apparently imagined that it would only be useful to have support for JSON arrays rather than the JSON format in general — and thus the name json_array.

When converting an Entity’s value to the column’s storage value, Doctrine does in fact abide by null values. As can be seen by the definition of \Doctrine\DBAL\Types\JsonArrayType::convertToDatabaseValue(), if your Entity object contains the null value for a column defined with json_array, Doctrine will happily insert or update your database with null.

Unfortunately, the companion \Doctrine\DBAL\Types\JsonArrayType::convertToPHPValue() method does not follow the same logic. When reading the column’s value out from the database, the null value is converted to an empty array! To be honest I’m a little baffled that this slipped through with the original release of the data type — how can convertToDatabaseValue() and convertToPHPValue() have different logic for null values? It makes very little sense for conversions between userland code and database to differ in this regard.

Will Doctrine be fixing this?

Not anytime soon. Bug report DBAL-446 is still open and unresolved, but a pull request to fix the bug was rejected and closed. Fixes that break backwards compatibility are not permitted before the next major release, which in this case would be 3.0 or later. This is a case of “once a bug, always a bug” due to overly strict versioning requirements.

A custom data type to the rescue

So we’re left implementing a custom data type in Doctrine that does nothing more than replace the return of an empty array with null. I’ve gone ahead and made the data type compatible with Doctrine 2.4, as well as 2.5 where Platforms having a native JSON data type will work as expected.

<?php

namespace Path\To\Custom\Type;

/**
 * Custom Doctrine data type for JSON.
 *
 * Doctrine has a json_array type but, as its name suggests, it was designed with
 * only arrays in mind. This extending type fixes a bug with the json_array type
 * wherein a null value in database gets converted to an empty array.
 *
 * IMPORTANT NOTE: you must register custom types with Doctrine:
 *      \Doctrine\DBAL\Types\Type::addType('json', '\Path\To\Custom\Type\Json');
 *
 * @link https://github.com/doctrine/dbal/issues/1643
 * @link https://github.com/doctrine/dbal/pull/655
 */
class Json extends \Doctrine\DBAL\Types\JsonArrayType
{
    /**
     * Made to be compatible with Doctrine 2.4 and 2.5; 2.5 added getJsonTypeDeclarationSQL().
     *
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $fieldDeclaration, \Doctrine\DBAL\Platforms\AbstractPlatform $platform)
    {
        return method_exists($platform, 'getJsonTypeDeclarationSQL') ? (
            $platform->getJsonTypeDeclarationSQL($fieldDeclaration)
        ) : $platform->getClobTypeDeclarationSQL($fieldDeclaration);
    }

    /**
     * When database value is null, we return null instead of empty array like our parent does.
     *
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, \Doctrine\DBAL\Platforms\AbstractPlatform $platform)
    {
        return $value === null ? null : parent::convertToPHPValue($value, $platform);
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'json';
    }
}