Laravel's MySQL JSON Manager


So, do you have a JSON data type column in your MySQL database and you're using Laravel framework? Well, this tutorial might help you manage your JSON column in more easy way.

Although Laravel allows you to cast JSON to array in order for you to manipulate the data easily:
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'options' => 'array',
    ];
}
$user = App\User::find(1);

$options = $user->options;

$options['key'] = 'value';

$user->options = $options;

$user->save();

Would it be nice if we have an API that can simplify the process of adding, updating and deleting data in the JSON column like so?
$user = App\User::find(1);

$user->options()->add('key', 'value');
$user->options()->add(['key1' => 'value1', 'key2' => 'value2']);
$user->options()->update('key', 'value');
$user->options()->update(['key1' => 'value1', 'key2' => 'value2']);
$user->options()->delete('key');
$user->options()->delete(['key1', 'key2']);
$user->options()->get('key');
$user->options()->key;
$user->options()->all();

Let's try it out. First let's create a unit test for it:
php artisan make:test MySQLJSONManagerTest --unit
class MySQLJSONManagerTest extends TestCase
{
    use DatabaseMigrations;

    /** @test */
    public function can_add_an_item_to_the_json_column()
    {
        $user = factory('App\User')->create();

        $user->options()->add('key1', '123456');

        $this->assertDatabaseHas('users', ['options' => '{"key1":"123456"}']);
    }

    /** @test */
    public function the_add_method_can_work_with_array()
    {
        $user = factory('App\User')->create();

        $user->options()->add(['key1' => '123456', 'key2' => '654321']);

        $this->assertDatabaseHas('users', ['options' => '{"key1":"123456","key2":"654321"}']);
    }

    /** @test */
    public function can_update_an_item_in_the_json_column()
    {
        $user = factory('App\User')->create();

        $user->options()->add(['key1' => '123456', 'key2' => '654321']);
        $user->options()->update('key1', 'updated');

        $this->assertDatabaseHas('users', ['options' => '{"key1":"updated","key2":"654321"}']);
    }

    /** @test */
    public function the_update_method_can_work_with_array()
    {
        $user = factory('App\User')->create();

        $user->options()->add(['key1' => '123456', 'key2' => '654321']);
        $user->options()->update(['key1' => 'updated', 'key2' => 'updated']);

        $this->assertDatabaseHas('users', ['options' => '{"key1":"updated","key2":"updated"}']);
    }

    /** @test */
    public function can_delete_an_item_in_the_json_column()
    {
        $user = factory('App\User')->create();

        $user->options()->add(['key1' => '123456', 'key2' => '654321']);
        $user->options()->delete('key1');

        $this->assertDatabaseHas('users', ['options' => '{"key2":"654321"}']);
    }

    /** @test */
    public function the_delete_method_can_work_with_array()
    {
        $user = factory('App\User')->create();

        $user->options()->add(['key1' => '123456', 'key2' => '654321']);
        $user->options()->delete(['key1', 'key2']);

        $this->assertDatabaseHas('users', ['options' => '[]']);
    }

    /** @test */
    public function can_get_specific_value_by_key()
    {
        $user = factory('App\User')->create();
        $key1 = '123456';

        $user->options()->add(['key1' => $key1, 'key2' => '654321']);        

        $this->assertEquals($key1, $user->options()->get('key1'));
    }

    /** @test */
    public function can_use_dynamic_property()
    {
        $user = factory('App\User')->create();
        $key1 = '123456';

        $user->options()->add(['key1' => $key1, 'key2' => '654321']);        

        $this->assertEquals($key1, $user->options()->key1);
    }

    /** @test */
    public function can_get_all_data_using_all_method()
    {
        $user = factory('App\User')->create();
        $options = ['key1' => '123456', 'key2' => '654321'];

        $user->options()->add($options);        

        $this->assertEquals($options, $user->options()->all());
    }
}
phpunit tests/Unit/MySQLJSONManagerTest
There were 2 errors:

1) Tests\Unit\MySQLJsonManagerTest::can_add_an_item_to_the_json_column
BadMethodCallException: Call to undefined method Illuminate\Database\Query\Builder::options()

Next, let's make sure we have the options column in our users table and must be fillable and cast to array:
$table->json('options')->nullable();
protected $fillable = ['options'];
protected $casts = ['options' => 'array'];

Next, let's create a trait called app/Traits/MySQLJSONColumnManager.php:
<?php

namespace App\Traits;

use App\Managers\MySQLJSONColumnManager as BaseManager;

trait MySQLJSONColumnManager
{
    public function __call($method, $arguments)
    {
        if (property_exists($this, 'casts')) {
            if (array_key_exists($method, $this->casts) && in_array($this->casts[$method], ['array', 'json'])) {
                return new BaseManager($this, $method);
            }
        }

        return parent::__call($method, $arguments);
    }
}
What we're doing here is we use the PHP __call magic method. What it does is it creates a method dynamically if a method does not exist in the class, which exactly what we want in this case, it is also known as method overloading in PHP. Then we check if the given model has a casts property and the method name is one of the key in the casts array and check if it's cast to array or json, then we return a new instance of App\Managers\MySQLJSONColumnManager class and passing the model instance and the method name in the constructor. Otherwise, let's just return the parent __call magic method.

Next, let's use the trait in the model that has a JSON column in it. In this case let's use it in the User model:
use App\Traits\MySQLJSONColumnManager;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable, MySQLJSONColumnManager;
}

Next, let's create the App\Managers\MySQLJSONColumnManager class:
<?php

namespace App\Managers;

use Illuminate\Database\Eloquent\Model;

class MySQLJSONColumnManager
{
    /**
     *  The model instance
     * 
     *  @var mixed
     */
    protected $model;

    /**
     *  The list of data
     * 
     *  @var array
     */
    protected $data;

    /**
     *  The the model attribute name.
     * 
     *  @var string
     */
    protected $attribute;

    /**
     *  Create a new data instance
     * 
     *  @param \Illuminate\Database\Eloquent\Model $model
     *  @param  string $attribute
     */
    public function __construct(Model $model, string $attribute)
    {
        $this->model = $model;        
        $this->attribute = $attribute;

        $this->data = $model->$attribute ?: [];               
    }
}

Next, let's create the add method:
/**
 *  Add a new item in the json column.
 * 
 *  @param array|string $key
 *  @param string $value
 */
public function add($key, $value = null)
{
    if (is_array($key)) {
        foreach ($key as $key => $value) {
            $this->data[$key] = $value;
        }
    } else {
        $this->data[$key] = $value;
    }

    $this->persist();
}

Next, let's create the persist method:
/**
 *  Persist the data
 * 
 *  @return mixed
 */
protected function persist()
{
    return $this->model->update([$this->attribute => $this->data]);
}

Next, let's create the update method:
/**
 *  Update an item in the json column.
 * 
 *  @param  array|string $key
 *  @param  string $value
 *  @return mixed
 */
public function update($key, $value = null)
{
    if (! is_array($key)) {
        $key = [$key => $value];
    }

    $this->data = array_merge(
        $this->data,
        array_only($key, array_keys($this->data))
    );

    $this->persist();
}

Next, let's create the delete method:
/**
 *  Remove item from the data.
 * 
 *  @param  array|string $key
 *  @return mixed
 */
public function delete($key)
{
    $exist = false;

    if (is_array($key)) {            
        foreach ($key as $value) {
            if (isset($this->data[$value])) {
                unset($this->data[$value]);
                $exist = true;
            }
        }
    }

    if (is_string($key) && isset($this->data[$key])) {
        unset($this->data[$key]);
        $exist = true;
    }

    if ($exist) {
        return $this->persist(); 
    }
}

Next, let's create the get method:
/**
 *  Retrieve a given setting
 * 
 *  @param  string $key
 *  @return string
 */
public function get($key)
{
    if ($this->has($key)) {
        return array_get($this->data, $key);
    }
}

Next, let's create the has method:
/**
 *  Determine if the given setting exists.
 * 
 *  @param  string  $key
 *  @return boolean
 */
protected function has($key)
{
    return array_key_exists($key, $this->data);
}

Next, let's add a __get magic method for the ability to access dynamic properties:
/**
 *  Magic property access for data
 * 
 *  @param  string $key
 *  @return string
 */
public function __get($key)
{
    return $this->get($key);              
}

Lastly, let's create the all method:
/**
 *  Retrieve an array of all data
 * 
 *  @return array
 */
public function all()
{
    return $this->data;
}

That's all there is to it. We have now the API that we can use to manipulate a JSON column in our MySQL database table. As always, if you have any questions please write it down in the comment below. See you in the next lesson. Thanks!

View the source code for this lesson on GitHub.