Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 65 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
## Overview

This class gives your application a RESTful API. All you have to do is set the `api_access` configuration option to `true`
on the appropriate DataObjects. You will need to ensure that all of your data manipulation and security is defined in
your model layer (ie, the DataObject classes) and not in your Controllers. This is the recommended design for SilverStripe
on the appropriate `DataObject`. You will need to ensure that all of your data manipulation and security is defined in
your model layer (ie, the `DataObject` classes) and not in your Controllers. This is the recommended design for SilverStripe
applications.

## Requirements
Expand All @@ -20,7 +20,7 @@ For a SilverStripe 3.x compatible version of this module, please see the [1.0 br

## Configuration

Example DataObject with simple API access, giving full access to all object properties and relations,
Example `DataObject` with simple API access, giving full access to all object properties and relations,
unless explicitly controlled through model permissions.

```php
Expand All @@ -39,7 +39,7 @@ class Article extends DataObject {
}
```

Example DataObject with advanced API access, limiting viewing and editing to Title attribute only:
Example `DataObject` with advanced API access, limiting viewing and editing to the "Title" attribute only:

```php
namespace Vendor\Project;
Expand All @@ -60,7 +60,7 @@ class Article extends DataObject {
}
```

Example DataObject field mapping, allows aliasing fields so that public requests and responses display different field names:
Example `DataObject` field mapping, allows aliasing fields so that public requests and responses display different field names:

```php
namespace Vendor\Project;
Expand All @@ -83,7 +83,66 @@ class Article extends DataObject {
];
}
```
Given a dataobject with values:

Example `DataObject` `HasMany` and `ManyMany` field-display handling. Only available on `JSONDataFormatter`. Declaring a `getApiFields` method in your `DataObject` (or an `Extension` subclass) allows additional fields to be shown on those relations, in addition to "id", "className" and "href":

```php
namespace Vendor\Project;

use SilverStripe\ORM\DataObject;

class Article extends DataObject {

private static $db = [
'Title'=>'Text',
'Published'=>'Boolean'
];

private static $api_access = true;

/**
* @param array $baseFields
* @return array
*/
public function getApiFields($baseFields)
{
return [
'Title' => $this->Title,
];
}
}
```

Example `DataObject` `HasMany` and `ManyMany` field-display handling. Only available on `JSONDataFormatter`. Declaring a `getApiFields` method in your `DataObject` (or an `Extension` subclass) allows existing fields that the formatter returns (like "id", "className" and "href"), to be overloaded:

```php
namespace Vendor\Project;

use SilverStripe\ORM\DataObject;

class Article extends DataObject {

private static $db = [
'Title'=>'Text',
'Published'=>'Boolean'
];

private static $api_access = true;

/**
* @param array $baseFields
* @return array
*/
public function getApiFields($baseFields)
{
return [
'href' => $this->myHrefOverrideMethod($baseFields['href']),
];
}
}
```

Given a `DataObject` with values:
```yml
ID: 12
Title: Title Value
Expand Down
18 changes: 15 additions & 3 deletions src/DataFormatter/JSONDataFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use SilverStripe\Control\Director;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\FieldType;
use SilverStripe\Core\ClassInfo;
use SilverStripe\ORM\DataObject;

/**
* Formats a DataObject's member fields into a JSON string
Expand Down Expand Up @@ -118,10 +120,16 @@ public function convertDataObjectToJSONObject(DataObjectInterface $obj, $fields
? $this->sanitiseClassName($relClass) . '/' . $obj->$fieldName
: $this->sanitiseClassName($className) . "/$id/$relName";
$href = Director::absoluteURL($rel);
$serobj->$relName = ArrayData::array_to_object(array(
$component = $obj->getField($relName);
$baseFields = [
"className" => $relClass,
"href" => "$href.json",
"id" => self::cast($obj->obj($fieldName))
"id" => self::cast($obj->obj($fieldName)),
];

$serobj->$relName = ArrayData::array_to_object(array_replace(
$baseFields,
ClassInfo::hasMethod($component, 'getApiFields') ? (array) $component->getApiFields($baseFields) : []
));
}

Expand Down Expand Up @@ -152,10 +160,14 @@ public function convertDataObjectToJSONObject(DataObjectInterface $obj, $fields
}
$rel = $this->config()->api_base . $this->sanitiseClassName($relClass) . "/$item->ID";
$href = Director::absoluteURL($rel);
$innerParts[] = ArrayData::array_to_object(array(
$baseFields = [
"className" => $relClass,
"href" => "$href.json",
"id" => $item->ID
];
$innerParts[] = ArrayData::array_to_object(array_replace(
$baseFields,
ClassInfo::hasMethod($item, 'getApiFields') ? (array) $item->getApiFields($baseFields) : []
));
}
$serobj->$relName = $innerParts;
Expand Down
43 changes: 38 additions & 5 deletions tests/unit/JSONDataFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@
namespace SilverStripe\RestfulServer\Tests;

use SilverStripe\RestfulServer\RestfulServer;
use SilverStripe\ORM\DataObject;
use SilverStripe\RestfulServer\Tests\Stubs\JSONDataFormatterTypeTestObject;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\RestfulServer\DataFormatter\JSONDataFormatter;

/**
*
* @todo Test Relation getters
* @todo Test filter and limit through GET params
* @todo Test DELETE verb
*
* Tests improvements made to JsonTypes,
* calls method which appends more fields
*/
class JSONDataFormatterTest extends SapphireTest
{
Expand All @@ -22,11 +21,20 @@ class JSONDataFormatterTest extends SapphireTest
JSONDataFormatterTypeTestObject::class,
];

protected $usesDatabase = true;

public function testJSONTypes()
{

// Needed as private static $api_access = true; doesn't seem to work on the stub file
Config::inst()->update(JSONDataFormatterTypeTestObject::class, 'api_access', true);

// Grab test object
$formatter = new JSONDataFormatter();

$parent = $this->objFromFixture(JSONDataFormatterTypeTestObject::class, 'parent');
$json = $formatter->convertDataObject($parent);

$this->assertRegexp('/"ID":\d+/', $json, 'PK casted to integer');
$this->assertRegexp('/"Created":"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}"/', $json, 'Datetime casted to string');
$this->assertContains('"Name":"Parent"', $json, 'String casted to string');
Expand All @@ -37,10 +45,35 @@ public function testJSONTypes()

$child3 = $this->objFromFixture(JSONDataFormatterTypeTestObject::class, 'child3');
$json = $formatter->convertDataObject($child3);

$this->assertContains('"Name":null', $json, 'Empty string is null');
$this->assertContains('"Active":false', $json, 'Empty boolean is false');
$this->assertContains('"Sort":0', $json, 'Empty integer is 0');
$this->assertContains('"Average":0', $json, 'Empty float is 0');
$this->assertRegexp('/"ParentID":\d+/', $json, 'FK casted to integer');

$original = $this->objFromFixture(JSONDataFormatterTypeTestObject::class, 'original');
$json = json_decode($formatter->convertDataObject($original));

// Returns valid array and isn't null
$this->assertNotEmpty($json, 'Array is empty');

// Test that original fields still exist, ie id, href, and className
$standard_id = $json->Children[0]->id;
$standard_className = $json->Children[0]->className;
$standard_href = $json->Children[0]->href;

$this->assertEquals(8, $standard_id, "Standard id field not equal");
$this->assertEquals('SilverStripe\RestfulServer\Tests\Stubs\JSONDataFormatterTypeTestObject', $standard_className, "Standard className does not equal");
$this->assertEquals('http://localhost/api/v1/SilverStripe-RestfulServer-Tests-Stubs-JSONDataFormatterTypeTestObject/8.json', $standard_href, "Standard href field not equal");

// Test method improvements, more fields rather than just id, href, className
$this->assertEquals(9, $json->ID, "ID not equal");
$this->assertEquals("SilverStripe\\RestfulServer\\Tests\\Stubs\\JSONDataFormatterTypeTestObject", $json->ClassName, "Class not equal");
$this->assertEquals("Test Object", $json->Name, "Name not equal");
$this->assertEquals(false, $json->Active, "Active not equal to false");
$this->assertEquals(0, $json->Sort, "Sort not equal to 0");
$this->assertEquals(0, $json->Average, "Average not equal to 0");
$this->assertEquals(0, $json->ParentID, "ParentID not equal to 0");
}
}
7 changes: 7 additions & 0 deletions tests/unit/JSONDataFormatterTest.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
SilverStripe\RestfulServer\Tests\Stubs\JSONDataFormatterTypeTestObject:
foo:
ID: 8
Name: Test Object 1
original:
ID: 9
Name: Test Object
Children: =>SilverStripe\RestfulServer\Tests\Stubs\JSONDataFormatterTypeTestObject.foo
parent:
Name: Parent
Active: true
Expand Down