tobyzerner/json-api-server


A fully automated framework-agnostic JSON:API server implementation in PHP.

License: MIT

Language: PHP

Keywords: eloquent, json-api, php


tobscure/json-api-server

Build Status Pre Release License

A fully automated framework-agnostic JSON:API server implementation in PHP.
Define your schema, plug in your models, and we'll take care of the rest. 🍻

composer require tobscure/json-api-server
use Tobscure\JsonApiServer\Api;
use Tobscure\JsonApiServer\Adapter\EloquentAdapter;
use Tobscure\JsonApiServer\Schema\Builder;

$api = new Api('http://example.com/api');

$api->resource('articles', new EloquentAdapter(new Article), function (Builder $schema) {
    $schema->attribute('title');
    $schema->hasOne('author', 'people');
    $schema->hasMany('comments');
});

$api->resource('people', new EloquentAdapter(new User), function (Builder $schema) {
    $schema->attribute('firstName');
    $schema->attribute('lastName');
    $schema->attribute('twitter');
});

$api->resource('comments', new EloquentAdapter(new Comment), function (Builder $schema) {
    $schema->attribute('body');
    $schema->hasOne('author', 'people');
});

/** @var Psr\Http\Message\ServerRequestInterface $request */
/** @var Psr\Http\Message\Response $response */
try {
    $response = $api->handle($request);
} catch (Exception $e) {
    $response = $api->error($e);
}

Assuming you have a few Eloquent models set up, the above code will serve a complete JSON:API that conforms to the spec, including support for:

  • Showing individual resources (GET /api/articles/1)
  • Listing resource collections (GET /api/articles)
  • Sorting, filtering, pagination, and sparse fieldsets
  • Compound documents with inclusion of related resources
  • Creating resources (POST /api/articles)
  • Updating resources (PATCH /api/articles/1)
  • Deleting resources (DELETE /api/articles/1)
  • Error handling

The schema definition is extremely powerful and lets you easily apply permissions, getters, setters, validation, and custom filtering and sorting logic to build a fully functional API in minutes.

Handling Requests

use Tobscure\JsonApiServer\Api;

$api = new Api('http://example.com/api');

try {
    $response = $api->handle($request);
} catch (Exception $e) {
    $response = $api->error($e);
}

Tobscure\JsonApiServer\Api is a PSR-15 Request Handler. Instantiate it with your API's base URL. Convert your framework's request object into a PSR-7 Request implementation, then let the Api handler take it from there. Catch any exceptions and give them back to Api if you want a JSON:API error response.

Defining Resources

Define your API's resources using the resource method. The first argument is the resource type. The second is an implementation of Tobscure\JsonApiServer\Adapter\AdapterInterface which will allow the handler to interact with your models. The third is a closure in which you'll build the schema for your resource.

use Tobscure\JsonApiServer\Schema\Builder;

$api->resource('comments', $adapter, function (Builder $schema) {
    // define your schema
});

We provide an EloquentAdapter to hook your resources up with Laravel Eloquent models. Set it up with an instance of the model that your resource represents. You can implement your own adapter if you use a different ORM.

use Tobscure\JsonApiServer\Adapter\EloquentAdapter;

$adapter = new EloquentAdapter(new User);

Attributes

Define an attribute field on your resource using the attribute method:

$schema->attribute('firstName');

By default the attribute will correspond to the property on your model with the same name. (EloquentAdapter will snake_case it automatically for you.) If you'd like it to correspond to a different property, provide it as a second argument:

$schema->attribute('firstName', 'fname');

Relationships

Define relationship fields on your resource using the hasOne and hasMany methods:

$schema->hasOne('user');
$schema->hasMany('comments');

By default the resource type that the relationship corresponds to will be derived from the relationship name. In the example above, the user relationship would correspond to the users resource type, while comments would correspond to comments. If you'd like to use a different resource type, provide it as a second argument:

$schema->hasOne('author', 'people');

Like attributes, the relationship will automatically read and write to the relation on your model with the same name. If you'd like it to correspond to a different relation, provide it as a third argument.

Has-one relationships are available for inclusion via the include query parameter. You can include them by default, if the include query parameter is empty, by calling the included method:

$schema->hasOne('user')
    ->included();

Has-many relationships must be explicitly made available for inclusion via the includable method. This is because pagination of included resources is not supported, so performance may suffer if there are large numbers of related resources.

$schema->hasMany('comments')
    ->includable();

Getters

Use the get method to define custom retrieval logic for your field, instead of just reading the value straight from the model property. (Of course, if you're using Eloquent, you could also define casts or accessors on your model to achieve a similar thing.)

$schema->attribute('firstName')
    ->get(function ($model, $request) {
        return ucfirst($model->first_name);
    });

Visibility

You can specify logic to restrict the visibility of a field using any one of the visible, visibleIf, hidden, and hiddenIf methods:

$schema->attribute('email')
    // Make a field always visible (default)
    ->visible()

    // Make a field visible only if certain logic is met
    ->visibleIf(function ($model, $request) {
        return $model->id == $request->getAttribute('userId');
    })

    // Always hide a field (useful for write-only fields like password)
    ->hidden()

    // Hide a field only if certain logic is met
    ->hiddenIf(function ($model, $request) {
        return $request->getAttribute('userIsSuspended');
    });

You can also restrict the visibility of the whole resource using the scope method. This will allow you to modify the query builder object provided by your adapter:

$schema->scope(function ($query, $request) {
    $query->where('user_id', $request->getAttribute('userId'));
});

Making Fields Writable

By default, fields are read-only. You can allow a field to be written to using any one of the writable, writableIf, readonly, and readonlyIf methods:

$schema->attribute('email')
    // Make an attribute writable
    ->writable()

    // Make an attribute writable only if certain logic is met
    ->writableIf(function ($model, $request) {
        return $model->id == $request->getAttribute('userId');
    })

    // Make an attribute read-only (default)
    ->readonly()

    // Make an attribute writable *unless* certain logic is met
    ->readonlyIf(function ($model, $request) {
        return $request->getAttribute('userIsSuspended');
    });

Default Values

You can provide a default value for a field to be used when creating a new resource if there is no value provided by the consumer. Pass a value or a closure to the default method:

$schema->attribute('joinedAt')
    ->default(new DateTime);

$schema->attribute('ipAddress')
    ->default(function ($request) {
        return $request->getServerParams()['REMOTE_ADDR'] ?? null;
    });

If you're using Eloquent, you could also define default attribute values to achieve a similar thing, although you wouldn't have access to the request object.

Validation

You can ensure that data provided for a field is valid before it is saved. Provide a closure to the validate method, and call the first argument if validation fails:

$schema->attribute('email')
    ->validate(function ($fail, $email) {
        if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $fail('Invalid email');
        }
    });

$schema->hasMany('groups')
    ->validate(function ($fail, $groups) {
        foreach ($groups as $group) {
            if ($group->id === 1) {
                $fail('You cannot assign this group');
            }
        }
    });

See Macros below to learn how to use Laravel's Validation component in your schema.

Setters & Savers

Use the set method to define custom mutation logic for your field, instead of just setting the value straight on the model property. (Of course, if you're using Eloquent, you could also define casts or mutators on your model to achieve a similar thing.)

$schema->attribute('firstName')
    ->set(function ($model, $value, $request) {
        return $model->first_name = strtolower($value);
    });

If your attribute corresponds to some other form of data storage rather than a simple property on your model, you can use the save method to provide a closure to be run after your model is saved:

$schema->attribute('locale')
    ->save(function ($model, $value, $request) {
        $model->preferences()->update(['value' => $value])->where('key', 'locale');
    });

Filtering

You can define a field as filterable to allow the resource index to be filtered by the field's value. This works for both attributes and relationships:

$schema->attribute('firstName')
    ->filterable();

$schema->hasMany('groups')
    ->filterable();
    
// e.g. GET /api/users?filter[firstName]=Toby&filter[groups]=1,2,3

You can optionally pass a closure to customize how the filter is applied to the query builder object provided by your adapter:

$schema->attribute('minPosts')
    ->hidden()
    ->filterable(function ($query, $value, $request) {
        $query->where('postCount', '>=', $value);
    });

Sorting

You can define an attribute as sortable to allow the resource index to be sorted by the attribute's value:

$schema->attribute('firstName')
    ->sortable();
    
$schema->attribute('lastName')
    ->sortable();
    
// e.g. GET /api/users?sort=lastName,firstName

Pagination

By default, resources are automatically paginated with 20 records per page. You can change this limit using the paginate method on the schema builder:

$schema->paginate(50);

Creating Resources

By default, resources are not creatable (i.e. POST requests will return 403 Forbidden). You can allow them to be created using the creatable, creatableIf, notCreatable, and notCreatableIf methods on the schema builder:

$schema->creatableIf(function ($request) {
    return $request->getAttribute('isAdmin');
});

Deleting Resources

By default, resources are not deletable (i.e. DELETE requests will return 403 Forbidden). You can allow them to be deleted using the deletable, deletableIf, notDeletable, and notDeletableIf methods on the schema builder:

$schema->deletableIf(function ($request) {
    return $request->getAttribute('isAdmin');
});

Macros

You can define macros on the Tobscure\JsonApiServer\Schema\Attribute class to aid construction of your API schema. Below is an example that sets up a rules macro which will add a validator to validate the attribute value using Laravel's Validation component:

use Tobscure\JsonApiServer\Schema\Attribute;

Attribute::macro('rules', function ($rules) use ($validator) {
    $this->validate(function ($fail, $value) use ($validator, $rules) {
        $key = $this->name;
        $validation = Validator::make([$key => $value], [$key => $rules]);

        if ($validation->fails()) {
            $fail((string) $validation->messages());
        }
    });
});
$schema->attribute('username')
    ->rules(['required', 'min:3', 'max:30']);

Examples

  • Flarum is forum software that uses tobscure/json-api-server to power its API.

Contributing

Feel free to send pull requests or create issues if you come across problems or have great ideas. See the Contributing Guide for more information.

Running Tests

$ vendor/bin/phpunit

License

This code is published under the The MIT License. This means you can do almost anything with it, as long as the copyright notice and the accompanying license file is left intact.

Project Statistics

Sourcerank 3
Repository Size 256 KB
Stars 13
Forks 3
Watchers 4
Open issues 10
Dependencies 8
Contributors 1
Tags 0
Created
Last updated
Last pushed

Top Contributors See all

Toby Zerner

Packages Referencing this Repo

tobyz/json-api-server
A fully automated JSON:API server implementation in PHP.
Latest release - Published - 13 stars
tobscure/json-api-server
A fully automated framework-agnostic JSON:API server implementation in PHP.
Latest release - Published - 13 stars

Something wrong with this page? Make a suggestion

Last synced: 2018-12-14 07:10:17 UTC

Login to resync this repository