Since I got no replies and couldn't quite find anything I have created a Trait called EloquentJSON (below) which allows you to define attributes on the entity which are json documents. By doing so when the model is read in it will parse the JSON document and set up getters and setters for each top level attribute making it easy to interact with the various attributes within the document. For example we could create a Photos model like this:
class Photo extends Eloquent
{
use AMD\Traits\EloquentJSON;
protected $jsonColumns = ['json_data'];
}
And then this:
$attr = json_decode($photo->json_data);
$attr->key = $value;
$photo->json_data = json_encode($attr);
becomes this:
$photo->key = value;
Also when calling the toArray() method the attributes are moved to the top level and the 'json_attributes' column is hidden. This essentially hides away the fact that you're using the json datatype and makes it look like we're working with attributes directly.
You can also establish relationships on a model like this:
public function user()
{
return $this->hasOne( 'User', 'id', "json_data->>'user_id'" );
}
The one caveat is when you're setting an attribute not previously set already in the JSON document then no setter is created and so you would need to call:
$referral->setJsonAttribute();
This thing is far from complete and freshly minted code so something may/will break - hopefully this saves the next guy/gal some work, and if there's interest in this as a package let me know and I'll see about setting a more formal repo (packagist, etc...)
<?php
trait EloquentJSON
{
/**
* List of known PSQL JSON operators
*
* @var array
*/
public static $jsonOperators = [
'->',
'->>',
'#>',
'#>>' ];
/**
* @var array
*/
private $jsonAttributes = [];
/**
* Boot the EloquentJSON trait for a model.
*
* @return void
*/
public static function bootEloquentJSON()
{
self::loading( function($obj)
{
$obj->inspectJsonColumns();
} );
}
/**
* Decodes each of the declared JSON attributes and records the attributes
* on each
*
* @return void
*/
public function inspectJsonColumns()
{
foreach ( $this->jsonColumns as $col )
{
$this->hidden[] = $col;
$obj = json_decode( $this->$col );
if( is_object($obj) )
{
foreach ( $obj as $key => $value )
{
$this->flagJsonAttribute( $key, $col );
$this->appends[] = $key;
}
}
}
}
/**
* Record that a given JSON element is found on a particular column
*
* @param string $key
* @param string $col
*
* @return void
*/
public function flagJsonAttribute( $key, $col )
{
$this->jsonAttributes[$key] = $col;
}
/**
* Include JSON column in the list of attributes that have a get mutator.
*
* @param string $key
* @return bool
*/
public function hasGetMutator( $key )
{
$jsonPattern = '/' . implode( '|', self::$jsonOperators ) . '/' ;
if ( array_key_exists($key, $this->jsonAttributes) !== false )
{
return true;
}
// In some cases the key specified may not be a simple key but rather a
// JSON expression (e.g. "jsonField->'some_key'). A common case would
// be when specifying a relation key. As such we test for JSON
// operators and expect a mutator if this is a JSON expression
elseif ( preg_match($jsonPattern, $key) != false )
{
return true;
}
return parent::hasGetMutator( $key );
}
/**
* Include the JSON attributes in the list of mutated attributes for a
* given instance.
*
* @return array
*/
public function getMutatedAttributes()
{
$attributes = parent::getMutatedAttributes();
$jsonAttributes = array_keys( $this->jsonAttributes );
return array_merge( $attributes, $jsonAttributes );
}
/**
* Check if the key is a known json attribute and return that value
*
* @param string $key
* @param mixed $value
* @return mixed
*/
protected function mutateAttribute( $key, $value )
{
$jsonPattern = '/' . implode( '|', self::$jsonOperators ) . '/' ;
// Test for JSON operators and reduce to end element
/* TODO: This only really works for 1-level deep. Should it be more? */
$isJson = false;
if ( preg_match($jsonPattern, $key) )
{
$elems = preg_split( $jsonPattern, $key );
$key = end( $elems );
$key = str_replace( [">","'"] , "", $key );
$isJson = true;
}
if ( array_key_exists($key, $this->jsonAttributes) != false )
{
$obj = json_decode( $this->{$this->jsonAttributes[$key]} );
return $obj->$key;
}
elseif ( $isJson )
{
return null;
}
return parent::mutateAttribute( $key, $value );
}
/**
* Set a given attribute on the known JSON elements.
*
* @param string $key
* @param mixed $value
* @return void
*/
public function setAttribute( $key, $value )
{
if ( array_key_exists($key, $this->jsonAttributes) !== false )
{
$this->setJsonAttribute( $this->jsonAttributes[$key], $key, $value );
return;
}
parent::setAttribute( $key, $value );
}
/**
* Set a given attribute on the known JSON elements.
*
* @param string $attribute
* @param string $key
* @param mixed $value
* @return void
*/
public function setJsonAttribute( $attribute, $key, $value )
{
$obj = json_decode( $this->{$attribute} );
$obj->$key = $value;
$this->{$attribute} = json_encode($obj);
return;
}
}
I am hugely interested as I found this looking for PostgreSQL jsonb support.
Its a good start for the model. I would also like migration support and query support for querying by json value.
I also need postgis geography data types and have adopted the lamb/postgis adaptor but it is pretty much abandoned and lacks some features. I would be open to a collaboration to add better PostgreSQL support to eloquent as I feel like what we have is very vanilla and I chose PostgreSQL specifically for its features that can't be found elsewhere like GIS capabilities and json fields.
I am also very interested in this for an upcoming project, specifically any advance on querying by the json. Please let me know if you have plans to release a formal repo/package.
Wow - responses... guess I should check this more often... OK, I will set it up in github and packagist and whatnot in the next day or two... I'll post back here with info once it's set up.
So since there was some interest I have created a new package on packagist:
https://packagist.org/packages/darrylkuhn/dialect
Please feel free to contribute/comment/etc...
I keep coming across this thread when googling for JSONB, Postgres, and Eloquent.
Darryl, thanks for your library, I've found it helpful!
For anyone following: an easy way to get JSON and JSONB fields to automatically cast to an array or an object element in Eloquent is to use this feature: http://laravel.com/docs/master/eloquent-mutators#attribute-casting
class Photo extends Model
{
protected $casts = [
'json_data' => 'array',
'other_json_data' => 'object',
];
....
}
...
$photo = Photo::find(100);
$photo->json_data['some_field'] = 'some_value';
$photo->other_json_data->some_other_field = 'some_other_value';
$photo->save();
Sign in to participate in this thread!
The Laravel portal for problem solving, knowledge sharing and community building.
The community