Crud listener for building JSON API Servers with almost no code.
Comes with advanced features like:
Install the JsonApi Listener by running the following command inside your project folder:
composer require friendsofcake/crud-json-api
It is highly recommended that you install the Search plugin as well:
composer require friendsofcake/search
Only run the following command if your application does not yet use Crud:
bin/cake plugin load Crud
Before you can start producing JSON:API you will have to set up your application by following the steps in this section.
CakePHP needs to be told that JSON:API requests should be parsed as JSON.
To do this, the BodyParserMiddleware must be added to your application
middleware queue, and a parser for the application/vnd.api+json mime-type
must be added.
In your Application class’ middleware method, add the following.
$bodies = new BodyParserMiddleware();
$bodies->addParser(['application/vnd.api+json'], function ($body) {
return json_decode($body, true);
});
$middlewareQueue->add($bodies);
Assuming you are using the default App Skeleton’s middleware queue, change it to.
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
$bodies = new BodyParserMiddleware();
$bodies->addParser(['application/vnd.api+json'], function ($body) {
return json_decode($body, true);
});
$middlewareQueue
// Catch any exceptions in the lower layers,
// and make an error page/response
->add(new ErrorHandlerMiddleware(Configure::read('Error')))
// Handle plugin/theme assets like CakePHP normally does.
->add(new AssetMiddleware([
'cacheTime' => Configure::read('Asset.cacheTime'),
]))
// Add routing middleware.
// If you have a large number of routes connected, turning on routes
// caching in production could improve performance. For that when
// creating the middleware instance specify the cache config name by
// using it's second constructor argument:
// `new RoutingMiddleware($this, '_cake_routes_')`
->add(new RoutingMiddleware($this))
// Parse various types of encoded request bodies so that they are
// available as array through $request->getData()
// https://book.cakephp.org/4/en/controllers/middleware.html#body-parser-middleware
->add($bodies);
return $middlewareQueue;
}
Attach the listener using the components array if you want to attach
it to all controllers, application wide, and make sure RequestHandler
is loaded before Crud.
class AppController extends Controller
{
public function initialize()
{
$this->loadComponent('RequestHandler');
$this->loadComponent('Crud.Crud', [
'actions' => [
'Crud.Index',
'Crud.View',
],
'listeners' => ['CrudJsonApi.JsonApi'],
]);
}
}
Alternatively, attach the listener to your controllers beforeFilter
if you prefer attaching the listener to only specific controllers on the fly.
class SamplesController extends AppController
{
public function beforeFilter(\Cake\Event\Event $event) {
parent::beforeFilter();
$this->Crud->addListener('CrudJsonApi.JsonApi');
}
}
The JsonApi listener overrides the Exception.renderer for jsonapi requests,
so in case of an error, a standardized error will be returned,
according to the JSON API specification.
Create a custom exception renderer by extending the Crud’s JsonApiExceptionRenderer
class and enabling it with the exceptionRenderer configuration option.
class AppController extends Controller
{
public function initialize()
{
parent::initialize();
$this->Crud->config(['listeners.jsonApi.exceptionRenderer' => 'App\Error\JsonApiExceptionRenderer']);
}
}
Note
The listener setting above is ignored when using CakePHP’s PSR7 middleware feature.
If you want to use CakePHP’s ErrorHandlerMiddleware:
Error.exceptionRenderer option in config/app.php to 'CrudJsonApi\Error\JsonApiExceptionRenderer' like shown below:'Error' => [
'errorLevel' => E_ALL,
'exceptionRenderer' => 'CrudJsonApi\Error\JsonApiExceptionRenderer',
'skipLog' => [],
'log' => true,
'trace' => true,
],
Only controllers explicitly mapped can be exposed as API resources so make sure
to configure your global routing scope in config/routes.php similar to:
const API_RESOURCES = [
'Countries',
'Currencies',
];
Router::scope('/', function ($routes) {
foreach (API_RESOURCES as $apiResource) {
$routes->resources($apiResource, [
'inflect' => 'dasherize',
]);
}
});
The JsonApi Listener adds the jsonapi request detector
to your Request object which checks if the request
contains a HTTP Accept header set to application/vnd.api+json
and can be used like this inside your application:
if ($this->request->is('jsonapi')) {
return 'cool, using JSON API';
}
Note
To make sure the listener won’t get in your way it will
return null for all requests unless is('jsonapi') is true.
The output produced by the listener is highly configurable using the Crud configuration options described in this section.
Either configure the options on the fly per action or enable them for all
actions in your controller by adding them to your contoller’s initialize() event
like this:
public function initialize()
{
parent::initialize();
$this->Crud->config('listeners.jsonApi.withJsonApiVersion', true);
}
Pass this mixed option a boolean with value true (default: false) to
make the listener add the top-level jsonapi node with member node
version to each response like shown below.
{
"jsonapi": {
"version": "1.0"
}
}
Passing an array or hash will achieve the same result but will also generate the additional meta child node.
{
"jsonapi": {
"version": "1.0",
"meta": {
"cool": "stuff"
}
}
}
Pass this array option (default: empty) an array or hash will make the listener
add the the top-level jsonapi node with member node meta to each response
like shown below.
{
"jsonapi": {
"meta": {
"copyright": {
"name": "FriendsOfCake"
}
}
}
}
Setting this boolean option to true (default: false) will make the listener generate absolute links for the JSON API responses.
Setting this boolean option to false (default: true) will make the listener render non-pretty json in debug mode.
Pass this array option (default: empty) an array with PHP Predefined JSON Constants to manipulate the generated json response. For example:
public function initialize()
{
parent::initialize();
$this->Crud->config('listeners.jsonApi.jsonOptions', [
JSON_HEX_QUOT,
JSON_UNESCAPED_UNICODE,
]);
}
Pass this array option (default: empty) an array with associated entity
names to limit the data added to the json included node.
Please note that entity names:
$this->Crud->config('listeners.jsonApi.include', [
'currency', // belongsTo relationship and thus singular
'cultures', // hasMany relationship and thus plural
]);
Note
The value of the include configuration will be overwritten if the
the client uses the ?include query parameter.
Pass this array option (default: empty) a hash with field names to limit the attributes/fields shown in the generated json. For example:
$this->Crud->config('listeners.jsonApi.fieldSets', [
'countries' => [ // main record
'name',
],
'currencies' => [ // associated data
'code',
],
]);
Note
Please note that there is no need to hide id fields as this
is handled by the listener automatically as per the
JSON API specification.
Setting this boolean option to true (default: false) will make the listener
add an about link pointing to an explanation for all validation errors caused
by posting request data in a format that does not comply with the JSON API document
structure.
This option is mainly intended to help developers understand what’s wrong with their
posted data structure. An example of an about link for a validation error caused
by a missing type node in the posted data would be:
{
"errors": [
{
"links": {
"about": "http://jsonapi.org/format/#crud-creating"
},
"title": "_required",
"detail": "Primary data does not contain member 'type'",
"source": {
"pointer": "/data"
}
}
]
}
This array option allows you to specify query parameters to parse in your application.
Currently this listener supports the official include parameter. You can easily add your own
by specifying a callable.
$this->Crud->listener('jsonApi')->config('queryParameter.parent', [
'callable' => function ($queryData, $subject) {
$subject->query->where('parent' => $queryData);
},
]);
This listener fully supports the Crud API Query Log listener and will,
once enabled as described here
, add a top-level query node to every response when debug mode is enabled.
{
"query": {
"default": [
{
"query": "SHOW FULL COLUMNS FROM `countries`",
"took": 0,
"params": [],
"numRows": 10,
"error": null
}
]
}
}
This listener comes with an additional Pagination listener that, once enabled,
wil add the meta and links nodes as per the JSON API specification.
Attach the listener using the components array if you want to attach it to all controllers, application wide.
class AppController extends Controller
{
public function initialize()
{
$this->loadComponent('RequestHandler');
$this->loadComponent('Crud.Crud', [
'actions' => [
'Crud.Index',
'Crud.View',
],
'listeners' => [
'CrudJsonApi.JsonApi',
'CrudJsonApi.Pagination',
],
]);
}
}
Alternatively, attach the listener to your controllers beforeFilter
if you prefer attaching the listener to only specific controllers on the fly.
class SamplesController extends AppController
{
public function beforeFilter(\Cake\Event\EventInterface $event)
{
parent::beforeFilter($event);
$this->Crud->addListener('CrudJsonApi.Pagination');
}
}
All GET requests to the index action will now add
JSON API pagination information to the response as shown below.
{
"meta": {
"record_count": 15,
"page_count": 2,
"page_limit": null
},
"links": {
"self": "/countries?page=2",
"first": "/countries?page=1",
"last": "/countries?page=2",
"prev": "/countries?page=1",
"next": null
}
}
This listener makes use of NeoMerx schemas to handle the heavy lifting that is required for converting CakePHP entities to JSON API format.
By default all entities in the _entities viewVar will be passed to the
Listener’s DynamicEntitySchema for conversion. This dynamic schema extends
Neomerx\JsonApi\Schema\SchemaProvider and is, amongst other things, used to
override NeoMerx methods so we can generate CakePHP specific output (like links).
Even though the dynamic entity schema provided by Crud should cater to the needs of most users, creating your own custom schemas is also supported. When using custom schemas please note that the listener will use the first matching schema, following this order:
Use a custom entity schema in situations where you need to alter the generated JSON API but only for a specific controller/entity.
An example would be overriding the NeoMerx getSelfSubUrl method used
to prefix all self links in the generated json for a Countries
controller. This would require creating a src/Schema/JsonApi/CountrySchema.php
file looking similar to:
namespace App\Schema\JsonApi;
use CrudJsonApi\Schema\JsonApi\DynamicEntitySchema;
class CountrySchema extends DynamicEntitySchema
{
public function getSelfSubUrl($entity = null)
{
return 'https://countryies.example.com/controller/self-links/';
}
}
Use a custom dynamic schema if you need to alter the generated JSON API for all controllers, application wide.
An example of a custom dynamic schema would require creating
a src/Schema/JsonApi/DynamicEntitySchema.php file looking similar to:
namespace App\Schema\JsonApi;
use CrudJsonApi\Schema\JsonApi\DynamicEntitySchema as CrudDynamicEntitySchema;
class DynamicEntitySchema extends CrudDynamicEntitySchema
{
public function getSelfSubUrl($entity = null)
{
return 'https://api.example.com/controller/self-links/';
}
}
Fetching JSON API Resource Collections is done by calling the index action of your API with:
HTTP GET request typeAccept header set to application/vnd.api+jsonA successful request will respond with HTTP response code 200
and response body similar to this output produced by
http://example.com/countries:
{
"data": [
{
"type": "countries",
"id": "1",
"attributes": {
"code": "NL",
"name": "The Netherlands"
},
"links": {
"self": "/countries/1"
}
},
{
"type": "countries",
"id": "2",
"attributes": {
"code": "BE",
"name": "Belgium"
},
"links": {
"self": "/countries/2"
}
}
]
}
Fetch a single JSON API Resource by calling the view action of your API with:
HTTP GET request typeAccept header set to application/vnd.api+jsonA successful request will respond with HTTP response code 200
and response body similar to this output produced by
http://example.com/countries/1:
{
"data": {
"type": "countries",
"id": "1",
"attributes": {
"code": "NL",
"name": "The Netherlands",
"dummy-counter": 11111
},
"relationships": {
"currency": {
"data": {
"type": "currencies",
"id": "1"
},
"links": {
"self": "/currencies/1"
}
},
"national-capital": {
"data": {
"type": "national-capitals",
"id": "1"
},
"links": {
"self": "/national-capitals/1"
}
}
},
"links": {
"self": "/countries/1"
}
}
}
Note
When retrieving a single Resource, crud-json-api will automatically generate relationships links for
all belongsTo attributes in your model UNLESS you pass the include request parameter OR define
a contain statement inside your Controller.
Creating a new JSON API Resource is done by calling the add action of your API with:
HTTP POST request typeAccept header set to application/vnd.api+jsonContent-Type header set to application/vnd.api+jsonA successful request will respond with HTTP response code 201
and a JSON API response body presenting the newly created Resource
along with id, attributes and belongsTo relationships.
All data posted to the listener is transformed from JSON API format to standard CakePHP format so it can be processed “as usual” once the data is accepted.
To make sure posted data complies with the JSON API specification it is first validated by the listener’s DocumentValidator which will throw a (422) ValidationException if it does not comply along with a pointer to the cause.
A valid JSON API request body for creating a new Country would look similar to:
{
"data": {
"type": "countries",
"attributes": {
"code": "NL",
"name": "The Netherlands"
}
}
}
The same rules apply when you create a new Resource and want to set its belongsTo relationships.
For example, the JSON API request body for creating a new Country with currency_id=1 would like:
{
"data": {
"type": "countries",
"attributes": {
"code": "NL",
"name": "The Netherlands"
},
"relationships": {
"currency": {
"data": {
"type": "currencies",
"id": "1"
}
}
}
}
}
Note
See this link for more examples of valid JsonApiRequestBodies.
Side-posting is an often requested feature which would allow creating multiple Resources (and/or relationships) using a single POST request.
However, this functionality is NOT supported by version 1.0 of the JSON API specification and is therefore NOT supported by crud-json-api.
In practice this means:
belongsTo relationships pointing to EXISTING foreign keysBadRequestException when it detects attempts to side-post hasMany relationshipsNote
Side-posting might land in version 1.1 of the JSON API specification, more information available in this Pull Request.
Updating an existing JSON API Resource is done by calling the edit action of your API with:
HTTP PATCH request typeAccept header set to application/vnd.api+jsonContent-Type header set to application/vnd.api+jsonid of the resource to updateA successful request will respond with HTTP response code 200
and response body similar to the one produced by the view action.
A valid JSON API document structure for updating the name field
for a Country with id 10 would look similar to the following output
produced by http://example.com/countries/1:
{
"data": {
"type": "countries",
"id": "10",
"attributes": {
"name": "My new name"
}
}
}
When updating a primary JSON API Resource, you can use the same PATCH request to set one or multiple To-One
(or belongsTo) relationships but only as long as the following conditions are met:
id of the related resource MUST correspond with an EXISTING foreign keyFor example, a valid JSON API document structure that would set a single related
national-capital for a given country would look like:
{
"data": {
"type": "countries",
"id": "2",
"relationships": {
"national-capital": {
"data": {
"type": "national-capitals",
"id": "4"
}
}
}
}
}
Note
Please note that JSON API does not support updating attributes for the related resource(s) and thus will simply ignore them if detected in the request body.
When updating a primary JSON API Resource, you can use the same PATCH request to set one or multiple To-Many
(or hasMany) relationships but only as long as the following conditions are met:
id of the related resource MUST correspond with an EXISTING foreign keyFor example, a valid JSON API document structure that would set multiple related cultures
for a given country would look like:
{
"data": {
"type": "countries",
"id": "2",
"relationships": {
"cultures": {
"data": [
{
"type": "cultures",
"id": "2"
},
{
"type": "cultures",
"id": "3"
}
]
}
}
}
}
Note
Please note that JSON API does not support updating attributes for the related resource(s) and thus will simply ignore them if detected in the request body.
Deleting an existing JSON API Resource is done by calling the delete action of your API with:
HTTP DELETE request typeAccept header set to application/vnd.api+jsonContent-Type header set to application/vnd.api+jsonid of the resource to deleteA successful request will return HTTP response code 204 (No Content)
and empty response body. Failed requests will return HTTP response
code 400 with empty response body.
An valid JSON API document structure for deleting a Country
with id 10 would look similar to:
{
"data": {
"type": "countries",
"id": "10"
}
}
}
The listener will produce error responses in the following JSON API format for all standard errors and all non-validation exceptions:
{
"errors": [
{
"code": "501",
"title": "Not Implemented"
}
],
"debug": {
"class": "Cake\\Network\\Exception\\NotImplementedException",
"trace": []
}
}
Note
Please note that the debug node with the stack trace will only be included if debug is true.
The listener will produce validation error (422) responses in the following JSON API format for all validation errors:
{
"errors": [
{
"title": "_required",
"detail": "Primary data does not contain member 'type'",
"source": {
"pointer": "/data"
}
}
]
}
Please be aware that the listener will also respond with (422) validation errors if request data is posted in a structure that does not comply with the JSON API specification.
The listener will detect associated data as produced by
contain and will automatically render those associations
into the JSON API response as specified by the specification.
Let’s take the following example code for the view action of
a Country model with a belongsTo association to Currencies
and a hasMany relationship with Cultures:
public function view()
{
$this->Crud->on('beforeFind', function (Event $event) {
$event->getSubject()->query->contain([
'Currencies',
'Cultures',
]);
});
return $this->Crud->execute();
}
Assuming a successful find the listener would produce the following JSON API response including all associated data:
{
"data": {
"type": "countries",
"id": "2",
"attributes": {
"code": "BE",
"name": "Belgium"
},
"relationships": {
"currency": {
"data": {
"type": "currencies",
"id": "1"
},
"links": {
"self": "/currencies/1"
}
},
"cultures": {
"data": [
{
"type": "cultures",
"id": "2"
},
{
"type": "cultures",
"id": "3"
}
],
"links": {
"self": "/cultures?country_id=2"
}
}
},
"links": {
"self": "/countries/2"
}
},
"included": [
{
"type": "currencies",
"id": "1",
"attributes": {
"code": "EUR",
"name": "Euro"
},
"links": {
"self": "/currencies/1"
}
},
{
"type": "cultures",
"id": "2",
"attributes": {
"code": "nl-BE",
"name": "Dutch (Belgium)"
},
"links": {
"self": "/cultures/2"
}
},
{
"type": "cultures",
"id": "3",
"attributes": {
"code": "fr-BE",
"name": "French (Belgium)"
},
"links": {
"self": "/cultures/3"
}
}
]
}
Crud-json-api fully supports the JSON API include request parameter which allows a client
to specify which related/associated resources should be returned.
As an example, a client could produce the exact same JSON API response as shown above by using
/countries/2?include=cultures,currencies.
Note
If the include parameter is provided, then only the requested relationships will be included
in the included schema.
It is possible to denyList, or allowList what the client is allowed to include. This is done using the listener configuration:
public function view()
{
$this->Crud
->listener('jsonApi')
->config('queryParameters.include.allowList', ['cultures', 'cities']);
return $this->Crud->execute();
}
allowListing will prevent all non-allowListed associations from being
contained. denyListing will prevent associations from being included.
denyListing takes precedence over allowListing (i.e denyListing and
allowListing the same association will prevent it from being included).
If you wish to prevent any associations, set the denyList``config
option to ``true:
public function view()
{
$this->Crud
->listener('jsonApi')
->config('queryParameters.include.denyList', true);
return $this->Crud->execute();
}
JSON API Sparse Fieldsets
allow you to limit the fields returned by your API by passing the fields parameter
in your request.
To select all countries but only retrieve their code field:
/countries?fields[countries]=code
To select a single country and only retrieve its name field:
/countries/1?fields[countries]=name
It is also possible to limit the fields of associated data. The following example will
return all fields for countries (the primary data) but will limit the fields returned
for currencies (the associated data) to id and name.
/countries?include=currencies&fields[currencies]=id,name
Please note that you MUST include the associated data in the fields args, eg:
/countries?fields[countries]=name&include=currencies&fields[currencies]=id,code will NOT work/countries?fields[countries]=name,currency&include=currencies&fields[currencies]=id,code does WORKYou may also use any combination of the above. In this case we are limiting the fields for both the primary resource and the associated data.
/countries/1?fields[countries]=name,currency&include=currencies&fields[currencies]=id,name
JSON API Sorting
allows you to sort the results produced by your API according to one
by passing one or more criteria to your request using the sort parameter.
Before continuing please note that the default sort order for each field is ascending
UNLESS the field is prefixed with a hyphen (-) in which case the sort order will
be descending.
To sort by a single field using ascending order:
/currencies?sort=code
To sort descending:
/currencies?sort=-code
To sort by multiple fields simply pass comma-separated sort fields in the order you want them applied:
/currencies?sort=code,name/currencies?sort=-code,name/currencies?sort=-code,-name/currencies?sort=name,codeCrudJsonApi supports any combination of the above sorts. E.g.
/currencies?include=countries&sort=name,countries.code/currencies?include=countries&sort=name,-countries.codeJSON API Filtering allow searching your API and requires:
Crud SearchListener as described hereNow create search aliases named filter in your tables like shown below:
// src/Model/Table/CountriesTable.php
public function searchManager()
{
$searchManager = $this->behaviors()->Search->searchManager();
$searchManager->like('filter', [
'before' => true,
'after' => true,
'field' => [$this->aliasField('name')],
]);
return $searchManager;
}
Once that is done you will be able to search your API using URLs similar to:
/countries?filter=netherlands/countries?filter=netherWe realize the JSON API document structure can be complex and hard to memorize which is exactly why we have decided to use pure JSON API documents as fixtures for our integration tests. This not only assures crud-json-api will behave exactly as we expect, it also provides you with a reference directory you can use to lookup fully-functional examples of:
Note
Please submit a PR if you are missing a use case.
Crud-json-api does not require you to create templates so if you see the following error you are
most likely not sending the correct application/vnd.api+json Accept Header with your requests:
Error: Missing Template
Crud-json-api depends on CakePHP Routing to generate the correct links for all resources in your JSON API response.
If you encounter errors like the one shown below, make sure that both your primary resource and all related
resources are added to the API_RESOURCES constant found in your config/routes.php file.
A route matching '' could not be found.
If you see the following error make sure that valid Table and Entity classes are
present for both the primary resource and all related resources.
Schema is not registered for a resource at path ''.
If you are just getting back a standard page response, rather than a JSON response (and you have confirmed that you are sending the correct JSON API Request Headers) it is most likely because you already have a controller action defined for the resource you are trying to request. For CRUD to handle it, you must remove any existing controller actions that conflict with the routes you are trying to configure.
By default crud-json-api will return timestamps in the following format:
"created-at": "2018-06-10T13:41:05+00:00"
If you prefer a different format, either specify it in your bootstrap.php file or right before a
specific action. E.g.
\Cake\I18n\FrozenTime::setJsonEncodeFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
\Cake\I18n\FrozenDate::setJsonEncodeFormat('yyyy-MM-dd');
There are many ways you can help improving this plugin:
Note
We welcome and appreciate contributions on all levels.