Skip to content
This repository was archived by the owner on Jan 2, 2023. It is now read-only.

New resource-based API #119

Open
wants to merge 45 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
3d61e47
New resource-based API
tobyzerner Mar 9, 2017
6494584
Merge branch 'master' into resource-interface
tobyzerner Mar 9, 2017
f395b01
Apply fixes from StyleCI
tobyzerner Mar 9, 2017
1795c83
Merge pull request #120 from tobscure/analysis-qJbw7o
tobyzerner Mar 9, 2017
ba8a379
Only pull a resource out of the map if it exists
tobyzerner Mar 26, 2017
d25a3f4
Remove some methods from public API
tobyzerner Mar 26, 2017
4e80ea8
Decode pagination link query string (no reason for it to be encoded)
tobyzerner Mar 26, 2017
5430fba
Update ResourceInterface docblocks
tobyzerner Mar 26, 2017
2ab0ac2
Rewrite tests
tobyzerner Mar 26, 2017
946cc45
Apply fixes from StyleCI
tobyzerner Mar 26, 2017
820e516
Merge pull request #122 from tobscure/analysis-XNwgj1
tobyzerner Mar 26, 2017
6f9cf92
Drop support for PHPUnit 5.0 for now
tobyzerner Mar 26, 2017
24f092d
Update README
tobyzerner Mar 27, 2017
a350d25
Add some missing docs
tobyzerner Mar 27, 2017
0d66e3b
Formatting
tobyzerner Mar 27, 2017
ea20221
Add sparse fieldsets to example
tobyzerner Mar 27, 2017
2a3c32a
Tweak wording
tobyzerner Mar 27, 2017
cb7702e
More tweaks
tobyzerner Mar 27, 2017
c7b8a32
Revert "Decode pagination link query string (no reason for it to be e…
tobyzerner Mar 27, 2017
880a28d
Reverse non-encoding of pagination URLs (should've read the spec more…
tobyzerner Mar 27, 2017
ac96c04
Support page[size] in pagination links
tobyzerner Mar 27, 2017
a071a76
Improve README; add references to JSON-API spec
tobyzerner Mar 27, 2017
44ed154
New error handling API in README (not implemented yet)
tobyzerner Mar 27, 2017
4ad47d1
README: Make link methods more explicit, add Link objects (not implem…
tobyzerner Mar 27, 2017
3321513
README: Simplify new error handling API: less magic, less surface area
tobyzerner Mar 27, 2017
27a25b5
Formatting
tobyzerner Mar 27, 2017
afa7d3f
Formatting
tobyzerner Mar 27, 2017
2f15ded
Formatting
tobyzerner Mar 27, 2017
8c2a260
Tweaks
tobyzerner Mar 27, 2017
b9923e3
Add default error response for ease
tobyzerner Mar 27, 2017
ffa2bf8
Big changes
tobyzerner Mar 28, 2017
9cf8336
Apply fixes from StyleCI
tobyzerner Mar 28, 2017
ed3d163
Merge pull request #123 from tobscure/analysis-qJ4m9Q
tobyzerner Mar 28, 2017
d1a008f
Relationship static constructors
tobyzerner Mar 28, 2017
a39459d
Apply fixes from StyleCI
tobyzerner Mar 28, 2017
1c93c27
Merge pull request #124 from tobscure/analysis-XaLlmW
tobyzerner Mar 28, 2017
d50ba8a
More big changes, hopefully the last lot
tobyzerner Mar 29, 2017
e940b9f
Apply fixes from StyleCI
tobyzerner Mar 29, 2017
6878c9c
Merge pull request #125 from tobscure/analysis-Xl91rQ
tobyzerner Mar 29, 2017
6bf5873
Fix failing test
tobyzerner Mar 29, 2017
de73055
Fix Relationship constructors
tobyzerner Mar 30, 2017
10adcf3
DRY-ed up tests
f3ath Sep 14, 2017
47e8d6d
Merge pull request #134 from f3ath/dry-up-tests
tobyzerner Sep 14, 2017
50ffec7
Added a benchmark
f3ath Mar 3, 2018
a0ddfe4
Merge pull request #141 from f3ath/benchmarks
tobyzerner Mar 3, 2018
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
230 changes: 140 additions & 90 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,145 +22,178 @@ composer require tobscure/json-api

```php
use Tobscure\JsonApi\Document;
use Tobscure\JsonApi\Collection;

// Create a new collection of posts, and specify relationships to be included.
$collection = (new Collection($posts, new PostSerializer))
->with(['author', 'comments']);
$resource = new PostResource($post);

// Create a new JSON-API document with that collection as the data.
$document = new Document($collection);
$document = Document::fromData($resource);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm proposing to name method more verbose (because we will pass Resource there):

$document = Document::fromResource($resource);

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also pass in an array of resources. fromData matches with the top-level data key in the resulting document.


// Add metadata and links.
$document->addMeta('total', count($posts));
$document->addLink('self', 'http://example.com/api/posts');
$document->setInclude(['author', 'comments']);
$document->setFields(['posts' => ['title', 'body']]);

// Output the document as JSON.
$document->setMetaItem('total', count($posts));
$document->setSelfLink('http://example.com/api/posts/1');

header('Content-Type: ' . $document::MEDIA_TYPE);
echo json_encode($document);
```

### Elements
### Resources

The JSON-API spec describes *resource objects* as objects containing information about a single resource, and *collection objects* as objects containing information about many resources. In this package:
Resources are used to create JSON-API [resource objects](http://jsonapi.org/format/#document-resource-objects). They must implement `Tobscure\JsonApi\ResourceInterface`. An `AbstractResource` class is provided with some basic functionality. Subclasses must specify the resource `$type` and implement the `getId()` method:

- `Tobscure\JsonApi\Resource` represents a *resource object*
- `Tobscure\JsonApi\Collection` represents a *collection object*
```php
use Tobscure\JsonApi\AbstractResource;

Both Resources and Collections are termed as *Elements*. In conceptually the same way that the JSON-API spec describes, a Resource may have **relationships** with any number of other Elements (Resource for has-one relationships, Collection for has-many). Similarly, a Collection may contain many Resources.
class PostResource extends AbstractResource
{
protected $type = 'posts';

A JSON-API Document may contain one primary Element. The primary Element will be recursively parsed for relationships with other Elements; these Elements will be added to the Document as **included** resources.
protected $post;

#### Sparse Fieldsets
public function __construct(Post $post)
{
$this->post = $post;
}

You can specify which fields (attributes and relationships) are to be included on an Element using the `fields` method. You must provide a multidimensional array organized by resource type:
public function getId()
{
return $this->post->id;
}
}
```

A JSON-API document can then be created from an instantiated resource:

```php
$collection->fields(['posts' => ['title', 'date']]);
```
$resource = new PostResource($post);

### Serializers
$document = Document::fromData($resource);
```

A Serializer is responsible for building attributes and relationships for a certain resource type. Serializers must implement `Tobscure\JsonApi\SerializerInterface`. An `AbstractSerializer` is provided with some basic functionality. At a minimum, a serializer must specify its **type** and provide a method to transform **attributes**:
To output a collection of resource objects, map your data to an array of resources:

```php
use Tobscure\JsonApi\AbstractSerializer;
$resources = array_map(function (Post $post) {
return new PostResource($post);
}, $posts);

class PostSerializer extends AbstractSerializer
{
protected $type = 'posts';
$document = Document::fromData($resources);
```

#### Attributes & Sparse Fieldsets

public function getAttributes($post, array $fields = null)
To add [attributes](http://jsonapi.org/format/#document-resource-object-attributes) to your resource objects, you may implement the `getAttributes()` method in your resource:

```php
public function getAttributes(array $fields = null)
{
return [
'title' => $post->title,
'body' => $post->body,
'date' => $post->date
'title' => $this->post->title,
'body' => $this->post->body,
'date' => $this->post->date
];
}
}
```

By default, a Resource object's **id** attribute will be set as the `id` property on the model. A serializer can provide a method to override this:
To output resource objects with a [sparse fieldset](http://jsonapi.org/format/#fetching-sparse-fieldsets), pass in an array of [fields](http://jsonapi.org/format/#document-resource-object-fields) (attributes and relationships), organised by resource type:

```php
public function getId($post)
{
return $post->someOtherKey;
}
$document->setFields(['posts' => ['title', 'body']]);
```

#### Relationships
The attributes returned by your resources will automatically be filtered according to the sparse fieldset for the resource type. However, if some attributes are expensive to calculate, then you can use the `$fields` argument provided to `getAttributes()`. This will be an `array` of fields, or `null` if no sparse fieldset has been specified.

The `AbstractSerializer` allows you to define a public method for each relationship that exists for a resource. A relationship method should return a `Tobscure\JsonApi\Relationship` instance.
```php
public function getAttributes(array $fields = null)
{
// Calculate the "expensive" attribute only if this field will show up
// in the final output
if ($fields === null || in_array('expensive', $fields)) {
$attributes['expensive'] = $this->getExpensiveAttribute();
}

return $attributes;
}
```

#### Relationships

You can [include related resources](http://jsonapi.org/format/#document-compound-documents) alongside the document's primary data. First you must define the available relationships on your resource. The `AbstractResource` base class allows you to define a method for each relationship. Relationship methods should return a `Tobscure\JsonApi\Relationship` instance, containing the related resource(s).

```php
public function comments($post)
{
$element = new Collection($post->comments, new CommentSerializer);
protected function getAuthorRelationship()
{
$resource = new UserResource($this->post->author);

return new Relationship($element);
}
return Relationship::fromData($resource);
}
```

By default, the `AbstractSerializer` will convert relationship names from `kebab-case` and `snake_case` into a `camelCase` method name and call that on the serializer. If you wish to customize this behaviour, you may override the `getRelationship` method:
You can then specify which relationship paths should be included on the document:

```php
public function getRelationship($model, $name)
{
// resolve Relationship called $name for $model
}
$document->setInclude(['author', 'comments', 'comments.author']);
```

### Meta & Links
By default, the `AbstractResource` implementation will convert included relationship names from `kebab-case` and `snake_case` into a `getCamelCaseRelationship` method name. If you wish to customize this behaviour, you may override the `getRelationship` method:

The `Document`, `Resource`, and `Relationship` classes allow you to add meta information:
```php
public function getRelationship($name)
{
// resolve Relationship for $name
}
```

### Meta Information & Links

The `Document`, `AbstractResource`, and `Relationship` classes allow you to add [meta information](http://jsonapi.org/format/#document-meta):

```php
$document = new Document;
$document->addMeta('key', 'value');
$document->setMeta(['key' => 'value']);
$document->setMetaItem('key', 'value');
$document->removeMetaItem('key');
```

They also allow you to add links in a similar way:
They also allow you to add [links](http://jsonapi.org/format/#document-links). A link's value may be a string, or a `Tobscure\JsonApi\Link` instance.

```php
$resource = new Resource($data, $serializer);
$resource->addLink('self', 'url');
$resource->setLinks(['key' => 'value']);
use Tobscure\JsonApi\Link;

$resource->setSelfLink('url');
$relationship->setRelatedLink(new Link('url', ['some' => 'metadata']));
```

You can also easily add pagination links:
You can also easily generate [pagination links](http://jsonapi.org/format/#fetching-pagination) on `Document` and `Relationship` instances:

```php
$document->addPaginationLinks(
$document->setPaginationLinks(
'url', // The base URL for the links
[], // The query params provided in the request
$_GET, // The query params provided in the request
40, // The current offset
20, // The current limit
100 // The total number of results
);
```
Serializers can provide links and/or meta data as well:

To define meta information and/or links globally for a resource type, call the appropriate methods in the constructor:

```php
use Tobscure\JsonApi\AbstractSerializer;
use Tobscure\JsonApi\AbstractResource;

class PostSerializer extends AbstractSerializer
{
// ...

public function getLinks($post) {
return ['self' => '/posts/' . $post->id];
}
class PostResource extends AbstractResource
{
public function __construct(Post $post)
{
$this->post = $post;

public function getMeta($post) {
return ['some' => 'metadata for ' . $post->id];
$this->setSelfLink('/posts/' . $post->id);
$this->setMetaItem('some', 'metadata for ' . $post->id);
}

// ...
}
```

**Note:** Links and metadata of the resource overrule ones with the same key from the serializer!

### Parameters

The `Tobscure\JsonApi\Parameters` class allows you to easily parse and validate query parameters in accordance with the specification.
Expand All @@ -173,25 +206,29 @@ $parameters = new Parameters($_GET);

#### getInclude

Get the relationships requested for inclusion. Provide an array of available relationship paths; if anything else is present, an `InvalidParameterException` will be thrown.
Get the relationships requested for [inclusion](http://jsonapi.org/format/#fetching-includes). Provide an array of available relationship paths; if anything else is present, an `InvalidParameterException` will be thrown.

```php
// GET /api?include=author,comments
$include = $parameters->getInclude(['author', 'comments', 'comments.author']); // ['author', 'comments']

$document->setInclude($include);
```

#### getFields

Get the fields requested for inclusion, keyed by resource type.
Get the [sparse fieldsets](http://jsonapi.org/format/#fetching-sparse-fieldsets) requested for inclusion, keyed by resource type.

```php
// GET /api?fields[articles]=title,body
$fields = $parameters->getFields(); // ['articles' => ['title', 'body']]

$document->setFields($fields);
```

#### getSort

Get the requested sort criteria. Provide an array of available fields that can be sorted by; if anything else is present, an `InvalidParameterException` will be thrown.
Get the requested [sort fields](http://jsonapi.org/format/#fetching-sorting). Provide an array of available fields that can be sorted by; if anything else is present, an `InvalidParameterException` will be thrown.

```php
// GET /api?sort=-created,title
Expand All @@ -200,7 +237,7 @@ $sort = $parameters->getSort(['title', 'created']); // ['created' => 'desc', 'ti

#### getLimit and getOffset

Get the offset number and the number of resources to display using a page- or offset-based strategy. `getLimit` accepts an optional maximum. If the calculated offset is below zero, an `InvalidParameterException` will be thrown.
Get the offset number and the number of resources to display using a [page- or offset-based strategy](http://jsonapi.org/format/#fetching-pagination). `getLimit` accepts an optional maximum. If the calculated offset is below zero, an `InvalidParameterException` will be thrown.

```php
// GET /api?page[number]=5&page[size]=20
Expand All @@ -212,36 +249,49 @@ $limit = $parameters->getLimit(100); // 100
$offset = $parameters->getOffset(); // 20
```

### Error Handling
#### getFilter

You can transform caught exceptions into JSON-API error documents using the `Tobscure\JsonApi\ErrorHandler` class. You must register the appropriate `Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface` instances.
Get the contents of the [filter](http://jsonapi.org/format/#fetching-filtering) query parameter.

```php
try {
// API handling code
} catch (Exception $e) {
$errors = new ErrorHandler;
// GET /api?filter[author]=toby
$filter = $parameters->getFilter(); // ['author' => 'toby']
```

### Errors

$errors->registerHandler(new InvalidParameterExceptionHandler);
$errors->registerHandler(new FallbackExceptionHandler);
You can create a `Document` containing [error objects](http://jsonapi.org/format/#error-objects) using `Tobscure\JsonApi\Error` instances:

$response = $errors->handle($e);
```php
use Tobscure\JsonApi\Error;

$document = new Document;
$document->setErrors($response->getErrors());
$error = new Error;

return new JsonResponse($document, $response->getStatus());
}
$error->setId('1');
$error->setAboutLink('url');
$error->setMeta('key', 'value');
$error->setStatus(400);
$error->setCode('123');
$error->setTitle('Something Went Wrong');
$error->setDetail('You dun goofed!');
$error->setSourcePointer('/data/attributes/body');
$error->setSourceParameter('include');

$document = Document::fromErrors([$error]);
```

## Examples

* [Flarum](https://github.com/flarum/core/tree/master/src/Api) is forum software that uses tobscure/json-api to power its API.

## Contributing

Feel free to send pull requests or create issues if you come across problems or have great ideas. Any input is appreciated!

### Running Tests

```bash
$ phpunit
$ vendor/bin/phpunit
```

## License
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"php": "^5.5.9 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8 || ^5.0"
"phpunit/phpunit": "^4.8"
},
"autoload": {
"psr-4": {
Expand Down
Loading