Skip to content

Commit d7b6e4d

Browse files
author
Darryl Kuhn
committed
allows callers to show/hide json columns and attributes, adds tests for json hinting, updates readme
1 parent 853b310 commit d7b6e4d

File tree

3 files changed

+228
-35
lines changed

3 files changed

+228
-35
lines changed

README.md

+37-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Require this package in your `composer.json` file:
1313
...then run `composer update` to download the package to your vendor directory.
1414

1515
## Usage
16+
### The Basics
1617

1718
The feature is exposed through a trait called which allows you to define attributes on the model which are of the json datatype. 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:
1819

@@ -34,15 +35,48 @@ becomes this:
3435
$photo->key = value;
3536
```
3637
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.
37-
38+
39+
### Relations
3840
You can also establish relationships on a model like this (only supported in PostgreSQL):
3941
```php
4042
public function user()
4143
{
4244
return $this->hasOne( 'User', 'id', "json_data->>'user_id'" );
4345
}
4446
```
45-
The one caveat is when you're setting an attribute not previously set already in the JSON document then no setter is created. For example if you defined a json column called "additional_details" which had a value of "{'foo':'bar'}" and wanted to set 'fizz' so you would need to call:
47+
48+
### Structure Hinting
49+
Sometimes you may have an empty or partially populated record in which case the trait cannot automatically detect and create getters/setters, etc... When getting or setting an attribute not previously set in the JSON document you'll get an exception. You have two choices to deal with this. You can hint at the full structure as in the example below:
50+
```php
51+
class Photo extends Eloquent
52+
{
53+
use Eloquent\Dialect\Json;
54+
protected $jsonColumns = ['json_data'];
55+
56+
public function __construct()
57+
{
58+
parent::__construct();
59+
$this->hintJsonStructure( 'json_data', '{"foo":null}' );
60+
}
61+
}
62+
```
63+
Once you create a hint you will be able to make calls to get and set json attributes e.g. `$photo->foo = 'bar';` regardless of whether or not they are already defined in the underlying db record. Alternatly if you prefer not to hint structures then you may call `setJsonAttribute()`. For example if you defined a json column called "json_data" and wanted to set an attribute called 'fizz' so you could call:
64+
```php
65+
$referral->setJsonAttribute( 'json_data', 'fizz', 'buzz' );
66+
```
67+
### Showing/Hiding Attributes
68+
One of the aims of the project is to make json attributes "first class" citizens of the model. This means by default we add the attributes to the models appends array so that when you call `$model->toArray()` or `$model->toJson()` the attribute shows up as a part of the structure like a normal attribute. By default we also hide away the json column holding the underlying data. Both of these settings can be changed using the `showJsonColumns()` and `showJsonAttributes()` as shown below:
4669
```php
47-
$referral->setJsonAttribute( 'additional_details', 'fizz', 'buzz' );
70+
class Photo extends Eloquent
71+
{
72+
use Eloquent\Dialect\Json;
73+
protected $jsonColumns = ['json_data'];
74+
75+
public function __construct()
76+
{
77+
parent::__construct();
78+
$this->showJsonColumns(true);
79+
$this->showJsonAttributes(false);
80+
}
81+
}
4882
```

src/Dialect/Json.php

+92-12
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
trait Json
44
{
55
/**
6-
* List of known PSQL JSON operators
6+
* List of known PSQL JSON operators. This is used when determining if
7+
* a column reference matches that of a JSON pattern (e.g.
8+
* test_column->>'value')
79
*
810
* @var array
911
*/
@@ -14,15 +16,39 @@ trait Json
1416
'#>>' ];
1517

1618
/**
19+
* Holds the map of attributes and the JSON colums they are stored in.
20+
*
1721
* @var array
1822
*/
1923
private $jsonAttributes = [];
2024

2125
/**
26+
* Holds a list of column names and the structure they *may* contain (e.g.
27+
* ['json_column' => "{'foo':null}"]
28+
*
2229
* @var array
2330
*/
2431
private $hintedJsonAttributes = [];
2532

33+
/**
34+
* By default this trait will hide the json columns when rendering the
35+
* model using toArray() or toJson() only exposing the underlying JSON
36+
* parameters as top level paremters on the model. Set this parameter to
37+
* true if you want to change that behavior.
38+
*
39+
* @var boolean
40+
*/
41+
private $showJsonColumns = false;
42+
43+
/**
44+
* By default this trait will append the json attributes when rendering the
45+
* model using toArray() or toJson(). Set this parameter to false if you
46+
* want to change that behavior.
47+
*
48+
* @var boolean
49+
*/
50+
private $showJsonAttributes = true;
51+
2652
/**
2753
* Create a new model instance that is existing.
2854
* Overrides parent to set Json columns.
@@ -48,13 +74,17 @@ public function newFromBuilder($attributes = array(), $connection = null)
4874
public function inspectJsonColumns()
4975
{
5076
foreach ($this->jsonColumns as $col) {
51-
$this->hidden[] = $col;
77+
if ( !$this->showJsonColumns ) {
78+
$this->hidden[] = $col;
79+
}
5280
$obj = json_decode($this->$col);
5381

5482
if (is_object($obj)) {
5583
foreach ($obj as $key => $value) {
5684
$this->flagJsonAttribute($key, $col);
57-
$this->appends[] = $key;
85+
if ( $this->showJsonAttributes ) {
86+
$this->appends[] = $key;
87+
}
5888
}
5989
}
6090
}
@@ -70,7 +100,9 @@ public function inspectJsonColumns()
70100
public function addHintedAttributes()
71101
{
72102
foreach ($this->hintedJsonAttributes as $col => $structure) {
73-
$this->hidden[] = $col;
103+
if ( !$this->showJsonColumns ) {
104+
$this->hidden[] = $col;
105+
}
74106

75107
if (json_decode($structure) === null) {
76108
throw new InvalidJsonException;
@@ -81,7 +113,9 @@ public function addHintedAttributes()
81113
if (is_object($obj)) {
82114
foreach ($obj as $key => $value) {
83115
$this->flagJsonAttribute($key, $col);
84-
$this->appends[] = $key;
116+
if ( $this->showJsonAttributes ) {
117+
$this->appends[] = $key;
118+
}
85119
}
86120
}
87121
}
@@ -156,9 +190,10 @@ public function getMutatedAttributes()
156190
/**
157191
* Check if the key is a known json attribute and return that value
158192
*
159-
* @param string $key
160-
* @param mixed $value
193+
* @param string $key
194+
* @param mixed $value
161195
* @return mixed
196+
* @throws InvalidJsonException
162197
*/
163198
protected function mutateAttribute($key, $value)
164199
{
@@ -176,15 +211,34 @@ protected function mutateAttribute($key, $value)
176211
}
177212

178213
if (array_key_exists($key, $this->jsonAttributes) != false) {
214+
215+
// Get the content of the column associated with this JSON
216+
// attribute and parse it into an object
179217
$value = $this->{$this->jsonAttributes[$key]};
180-
if ( $value === null ) {
181-
return null;
218+
$obj = json_decode($this->{$this->jsonAttributes[$key]});
219+
220+
// Make sure we were able to parse the json. It's possible here
221+
// that we've only hinted at an attribute and the column that will
222+
// hold that attribute is actually null. This isn't really a parse
223+
// error though the json_encode method will return null (just like)
224+
// a parse error. To distenguish the two states see if the original
225+
// value was null (indicating there was nothing there to parse in
226+
// the first place)
227+
if ( $value !== null && $obj === null ) {
228+
throw new InvalidJsonException;
229+
}
230+
231+
// Again it's possible the key will be in the jsonAttributes array
232+
// (having been hinted) but not present on the actual record.
233+
// Therefore test that the key is set before returning.
234+
if ( isset($obj->$key) ) {
235+
return $obj->$key;
182236
}
183237
else {
184-
$obj = json_decode($this->{$this->jsonAttributes[$key]});
238+
return null;
185239
}
186-
return $obj->$key;
187-
} elseif ($isJson) {
240+
}
241+
elseif ($isJson) {
188242
return null;
189243
}
190244

@@ -234,4 +288,30 @@ public function setJsonAttribute($attribute, $key, $value)
234288
$this->{$attribute} = json_encode($obj);
235289
return;
236290
}
291+
292+
/**
293+
* Allows you to specify if the actual JSON column housing the attributes
294+
* should be shown on toArray() and toJson() calls. Set this value in the
295+
* models constructor (to make sure it is set before newFromBuilder() is
296+
* called). This is false by default
297+
*
298+
* @param boolean $show
299+
* @return boolean
300+
*/
301+
public function showJsonColumns( $show ) {
302+
return $this->showJsonColumns = $show;
303+
}
304+
305+
/**
306+
* Allows you to specify if the attributes within various json columns
307+
* should be shown on toArray() and toJson() calls. Set this value in the
308+
* models constructor (to make sure it is set before newFromBuilder() is
309+
* called). This is true by default
310+
*
311+
* @param boolean show
312+
* @return boolean
313+
*/
314+
public function showJsonAttributes( $show ) {
315+
return $this->showJsonAttributes = $show;
316+
}
237317
}

0 commit comments

Comments
 (0)