forked from silverstripe/developer-docs
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d01a1eb
commit bbbc22e
Showing
1 changed file
with
129 additions
and
0 deletions.
There are no files selected for viewing
129 changes: 129 additions & 0 deletions
129
en/02_Developer_Guides/02_Controllers/07_CMS_JSON_APIs.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
--- | ||
title: CMS JSON API's | ||
summary: Creating standardised JSON API's for authenticated users in the CMS. | ||
--- | ||
|
||
# CMS JSON API's | ||
|
||
This document contains a standard set of conventions to be used when creating JSON API's in the CMS that are used in conjunction with AJAX requests from authenticated users. | ||
|
||
It's recommend you design your API with a "REST-like" approach for JSON requests in your application, acknowledging that certain aspects may not strictly adhere to pure REST principles. | ||
|
||
New CMS JSON API's are likely to also make use of FormSchema in conjunction with the react Formbuilder. FormSchema diverges from REST principles as it utilises a combination of JSON and application/x-www-form-urlencoded POST requests. This method has proven effective, and there are currently no plans to alter its functionality. | ||
|
||
Exercise caution with the use of PUT requests, as its implementation can lead to confusion regarding the intended "insert or update" behavior in a REST context. As CMS JSON API's have a very narrow use case with no public consumption, it is recommended to prefer simply use POST over PUT or PATCH requests and not worry about strict REST compliance. | ||
|
||
To view an example of a controller that follows these standards see [`LinkFieldController`](https://github.com/silverstripe/silverstripe-linkfield/blob/4/src/Controllers/LinkFieldController.php). | ||
|
||
## Creating a controller | ||
|
||
Create a subclass of [`LeftAndMain`](api:SilverStripe\Admin\LeftAndMain). This ensures that users must be logged into the admin interface to access the endpoint. Additionally, it grants access to the methods [`LeftAndMain::jsonSuccess()`](api:SilverStripe\Admin\LeftAndMain::jsonSuccess()) and [`LeftAndMain::jsonError()`](api:SilverStripe\Admin\LeftAndMain::jsonError()). | ||
|
||
[warning] | ||
To enhance security, do not create a subclass of [`Controller`](api:SilverStripe\Control\Controller) routed using YAML on the `/admin` route. This practice is strongly discouraged as it circumvents the requirement to log in to the CMS to access the endpoints. | ||
[/warning] | ||
|
||
When naming this class add a "Controller" prefix to this class, for instance name it "MySomethingController". | ||
|
||
Define the URL segment using `private static string $url_segment = 'my-segment';`. For small optional modules, this may typically be the composer name of the module, for instance "linkfield". | ||
|
||
Include `private static string $required_permission_codes = 'CMS_ACCESS_CMSMain';`, possibly with a different permission, to allow non-admins to access the endpoints on the controller. | ||
|
||
As this is a subclass of `LeftAndMain`, it automatically gets added to the CMS menu. To remove it from the CMS menu, create a `_config.php` in the module (if it doesn't already exist) and add `CMSMenu::remove_menu_class(MySomethingController::class);`. | ||
|
||
## Handling requests with $url_handlers | ||
|
||
Utilise `private static array $url_handlers` to implement the following enhancements: | ||
- Ensure the HTTP request method aligns with the intended use for each method, for instance, restricting it to GET or POST. | ||
- Prevent potential conflicts with existing methods, such as [`LeftAndMain::sort()`](api:SilverStripe\Admin\LeftAndMain::sort()), by structuring the endpoint URL segment as `sort` and associating it with a method like `MySomethingController::apiSort()`. | ||
|
||
Use the request param `$ItemID` if you need a record ID into a url so that you have an endpoint for a specific record. Use `$ItemID` because it's consistent with the request param used in Form Schema requests. For example, to use `$ItemID` in a GET request to view a single record: | ||
|
||
```php | ||
// MySomethingController.php | ||
|
||
private static array $url_handlers = [ | ||
'GET view/$ItemID' => 'apiView', | ||
]; | ||
|
||
public function apiView(): HTTPResponse | ||
{ | ||
$itemID = (string) $request->param('ItemID'); | ||
// Note: would normally validate that $itemID is a valid integer and that $obj exists | ||
$obj = MyDataObject::get()->byID($itemID); | ||
$data = ['ID' => $obj->ID, 'Title' => $obj->Title]; | ||
return $this->jsonSuccess(200, $data); | ||
} | ||
``` | ||
|
||
## Permission checks | ||
|
||
Incorporate essential permission checks, such as `canEdit()`, into all relevant endpoints to ensure secure access control. | ||
|
||
When returning DataObjects as JSON, remember to invoke `canView()` on each DataObject. In a CMS context where the number of DataObjects is typically limited, the performance impact of these checks should not be a significant concern. | ||
|
||
When dealing with a Form, it's crucial to check whether any frontend form fields have had `->setReadonly(true)` or `->setDisabled(true)` invoked on them. In such cases, consider returning 400 status codes, similar to a failed `canEdit()` check, to appropriately handle these conditions. | ||
|
||
## Return values and error handling | ||
|
||
All endpoint methods must declare a return type of [`HTTPResponse`](api:SilverStripe\Control\HTTPResponse). | ||
|
||
All return values must utilise `jsonSuccess()`. | ||
|
||
Do not throw exceptions in the controller, as this leads to a suboptimal content-editor experience. Instead all non-success conditions must call `jsonError()` | ||
|
||
### jsonSuccess() | ||
|
||
When incorporating $optionalData in `jsonSuccess()`, refrain from adding any "success metadata" e.g. `['success' => true, 'data' => $data]`. Instead, rely on standard HTTP status codes to clearly indicate the success of the operation. | ||
|
||
For scenarios where nobody is returned upon success, employ `return $this->jsonSuccess(201);`. Alternatively, when the response includes JSON data, utilise `return $this->jsonSuccess(200, $data);` to appropriately structure the response. | ||
|
||
### jsonError() | ||
|
||
Include a concise message outlining the nature of the error when calling `jsonError()` to facilitate efficient troubleshooting of the underlying issues. These error messages are only intented for developers so do not use the `_t()` function to make them translatable. | ||
|
||
The `return` keyword is omitted for `jsonError()` because internally it triggers an exception, subsequently caught and converted into an `HTTPResponse` object. | ||
|
||
Despite the slightly unconventional format returned of `jsonError()` with multiple nodes (unlike standard usage of `jsonSuccess()`), its usage remains consistent with FormSchema. It's advisable to maintain this uniformity rather than introducing separate methods for FormSchema and non-FormSchema failures. | ||
|
||
Refer to the list of [400 status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses) to appropriately choose and communicate the nature of the error. | ||
|
||
## JavaScript AJAX requests | ||
|
||
Leverage the `backend` helper by importing it using `import backend from 'lib/Backend';` when making JavaScript requests. This is preferred over traditional `fetch()` calls. | ||
|
||
The `backend` helper provides the advantage of being able to use a `catch()` block to handle 400-500 response codes, offering a more streamlined approach compared to using vanilla `fetch()`. | ||
|
||
Take advantage of the shorthand methods offered by the backend helper, such as `.get()` and `.post()`, for making more consise requests. | ||
|
||
## Passing values from PHP to JavaScript | ||
|
||
To transmit values from PHP to JavaScript, override `LeftAndMain::getClientConfig()` within your controller. Begin your method with `$clientConfig = parent::getClientConfig();` to ensure proper inheritance. | ||
|
||
Include any relevant links to endpoints in the client configuration. For example, add `'myEndpointUrl' => $this->Link('my-endpoint')`, where `my-endpoint` is specified in `private static array $url_handlers`. | ||
|
||
In JavaScript, access these values using `import Config from 'lib/Config';` and retrieve the endpoint URL using `Config.getSection('Full\ClassName\Of\MyController').anArrayKey.myEndpointUrl;`. | ||
|
||
## Passing data from JavaScript to PHP | ||
|
||
Utilise `import backend from 'lib/Backend';` along with `backend.post(url, {mykey:'Some value'}, csrfToken).then(() => ...);` to effectively make a POST request in your JavaScript code. | ||
|
||
In the corresponding controller, retrieve the POST data using `$json = json_decode($this->getRequest()->getBody());`. If `$json` is `null`, take appropriate action by calling `jsonError()` to handle the error gracefully. | ||
|
||
## CSRF token | ||
|
||
When performing non-view operations, include an `'X-SecurityID'` header in your JavaScript request, with its value set to `SecurityToken::getSecurityID()`. | ||
|
||
Access the token value in JavaScript by logging in by using `import Config from 'lib/Config';` followed by `Config.get('SecurityID');` | ||
|
||
Ensure the security of your endpoints by validating the token header using `SecurityToken::inst()->checkRequest($this->getRequest())` on relevant endpoints. This adds an additional layer of security by confirming the legitimacy of the incoming requests. | ||
|
||
## Unit testing | ||
|
||
For effective unit testing, employ a subclass of [`FunctionalTest`](api:SilverStripe\Dev\FunctionalTest) instead of the regular [`SapphireTest`](api:SilverStripe\Dev\SapphireTest). This allows you to make HTTP requests to your endpoints and ensures comprehensive functional testing. | ||
|
||
Utilise `$this->get()` for endpoints designed to handle GET requests and `$this->post()` for those intended for POST requests. | ||
|
||
For non-GET/POST requests, such as DELETE, employ `$this->mainSession->sendRequest('DELETE', $url, [], $headers);` to simulate the respective HTTP method. | ||
|
||
Implement dataProviders extensively and pass in parameters such as `$expectedBody` and `$expectedStatusCode`. Ensure that each call to `jsonSuccess()` and `jsonError()` have at least one corresponding case in your dataProvider. This approach guarantees thorough testing of the expected responses for different scenarios. |