From ee1c90d868c0c8b62d8b31e3d1355362ed419ed9 Mon Sep 17 00:00:00 2001 From: matapatos Date: Fri, 24 May 2024 17:07:50 +0100 Subject: [PATCH] docs: Advanced user guide --- README.md | 2 +- .../dependency-injection.md | 37 +++++++++ docs/docs/advanced-user-guide/index.md | 6 ++ .../json-schemas/multiple-root-dirs.md | 65 ++++++++++++++++ .../json-schemas/references.md | 12 +++ .../json-schemas/validator.md | 62 +++++++++++++++ docs/docs/advanced-user-guide/middlewares.md | 47 ++++++++++++ .../advanced-user-guide/request-life-cycle.md | 75 +++++++++++++++++++ docs/docs/advanced-user-guide/responses.md | 52 +++++++++++++ docs/docs/index.md | 2 +- docs/docs/quick-start/json-schemas.md | 6 +- docs/docs/quick-start/router.md | 10 +-- docs/docs/reference/index.md | 0 docs/docs/release.md | 8 +- docs/mkdocs.yml | 13 +++- src/Endpoint.php | 6 +- 16 files changed, 386 insertions(+), 17 deletions(-) create mode 100644 docs/docs/advanced-user-guide/dependency-injection.md create mode 100644 docs/docs/advanced-user-guide/index.md create mode 100644 docs/docs/advanced-user-guide/json-schemas/multiple-root-dirs.md create mode 100644 docs/docs/advanced-user-guide/json-schemas/references.md create mode 100644 docs/docs/advanced-user-guide/json-schemas/validator.md create mode 100644 docs/docs/advanced-user-guide/middlewares.md create mode 100644 docs/docs/advanced-user-guide/request-life-cycle.md create mode 100644 docs/docs/advanced-user-guide/responses.md delete mode 100644 docs/docs/reference/index.md diff --git a/README.md b/README.md index e6e76b0..530dce6 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ - Middlewares support - IDE auto completion support - No magic router. It uses WordPress [`register_rest_route`](https://developer.wordpress.org/reference/functions/register_rest_route/) -- Support for newer JSON schema drafts thanks to [json/opis](https://opis.io/json-schema/2.x/) +- Support for newer JSON schema drafts thanks to [opis/json-schema](https://opis.io/json-schema/2.x/) ## Requirements diff --git a/docs/docs/advanced-user-guide/dependency-injection.md b/docs/docs/advanced-user-guide/dependency-injection.md new file mode 100644 index 0000000..05079ba --- /dev/null +++ b/docs/docs/advanced-user-guide/dependency-injection.md @@ -0,0 +1,37 @@ +Each REST endpoint has its unique logic. Same goes with the data that it needs to work. + +For that reason, WP-FastEndpoints provides dependency injection support for all handlers +e.g. permission handlers, main endpoint handler and middlewares. + +With dependency injection our endpoints do look much cleaner ✨🧹 + +=== "With dependency injection" + + ```php + // We only need the ID. So we type $ID + $router->get('/posts/(?P[\d]+)', function ($ID) { + return get_post($ID); + }); + + // We don't need anything. So no arguments are defined :D + $router->get('/posts/random', function () { + $allPosts = get_posts(); + return $allPosts ? $allPosts[array_rand($allPosts)] : new WpError(404, 'No posts found'); + }); + ``` + +=== "No dependency injection" + + ```php + // Unable to fetch a dynamic parameter. Have to work with the $request argument + $router->get('/posts/(?P[\d]+)', function ($request) { + return get_post($request->get('ID')); + }); + + // Forced to accept $request even if not used :( + $router->get('/posts/random', function ($request) { + $allPosts = get_posts(); + return $allPosts ? $allPosts[array_rand($allPosts)] : new WpError(404, 'No posts found'); + }); + ``` + diff --git a/docs/docs/advanced-user-guide/index.md b/docs/docs/advanced-user-guide/index.md new file mode 100644 index 0000000..1b5a599 --- /dev/null +++ b/docs/docs/advanced-user-guide/index.md @@ -0,0 +1,6 @@ +The [Quick Start](/wp-fastendpoints/quick-start/) should be able to get you a feel of +the main features of WP-FastEndpoints. + +However, it's possible that the solution for your use case might not be in the Quick Start +tutorial. In the next sections we will take a look at further functionalities that +WP-FastEndpoints provides. diff --git a/docs/docs/advanced-user-guide/json-schemas/multiple-root-dirs.md b/docs/docs/advanced-user-guide/json-schemas/multiple-root-dirs.md new file mode 100644 index 0000000..455ed0a --- /dev/null +++ b/docs/docs/advanced-user-guide/json-schemas/multiple-root-dirs.md @@ -0,0 +1,65 @@ +For most projects, all JSON schemas might be kept inside a single root directory, like: + +```text +my-plugin +β”‚ +└───src +β”‚ β”‚ +β”‚ └───Api +β”‚ β”‚ β”‚ +β”‚ β”‚ └───Schemas // Root dir +β”‚ β”‚ β”‚ +β”‚ β”‚ └───Posts +β”‚ β”‚ β”‚ β”‚ (...) +β”‚ β”‚ β”‚ +β”‚ β”‚ └───Users +β”‚ β”‚ β”‚ (...) +``` + +However, when your API starts to grow you might end up having the need for multiple root directories. + +## Example + +Let's imagine that your API consists on two different versions: v1 and v2, like the following: + +```text +my-plugin +β”‚ +└───src +β”‚ β”‚ +β”‚ └───Api +β”‚ β”‚ β”‚ +β”‚ β”‚ └───v1 +β”‚ β”‚ β”‚ └───Schemas // V1 JSON schemas root dir +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ └───Posts +β”‚ β”‚ β”‚ β”‚ (...) +β”‚ β”‚ β”‚ +β”‚ β”‚ └───v2 +β”‚ β”‚ └───Schemas // V2 JSON schemas root dir +β”‚ β”‚ β”‚ +β”‚ β”‚ └───Posts +β”‚ β”‚ β”‚ (...) +``` + +In this case scenario your code would look something like this: + +```php +$router->appendSchemaDir(MY_PLUGIN_DIR.'Api/v1/Schemas', 'https://www.wp-fastendpoints.com/v1'); +$router->appendSchemaDir(MY_PLUGIN_DIR.'Api/v2/Schemas', 'https://www.wp-fastendpoints.com/v2'); +``` + +Then in all your endpoints you will have to specify the full schema prefix. It's important that +you specify the full prefix because we can't guarantee the order or even if the same schema +directory is returned all the time. + +=== "Using v1 schemas" + ```php + $router->get('/test', function(){return true;}) + ->returns('https://www.wp-fastendpoints.com/v1/Posts/Get.json'); + ``` +=== "Using v2 schemas" + ```php + $router->get('/test', function(){return true;}) + ->returns('https://www.wp-fastendpoints.com/v2/Posts/Get.json'); + ``` \ No newline at end of file diff --git a/docs/docs/advanced-user-guide/json-schemas/references.md b/docs/docs/advanced-user-guide/json-schemas/references.md new file mode 100644 index 0000000..33bbe20 --- /dev/null +++ b/docs/docs/advanced-user-guide/json-schemas/references.md @@ -0,0 +1,12 @@ +[References](https://opis.io/json-schema/2.x/references.html) is another great feature from +[opis/json-schema](https://opis.io/json-schema/2.x/). + +With references, you are able to point to another JSON schema inside of schema. This can +be useful to reuse the same schema multiple times. + +!!! tip + Bear in mind that when referencing a schema the full prefix must be used e.g. + *https://www.wp-fastendpoints.com/Posts/Get.json* + +Take a look at their [References Docs Β»](https://opis.io/json-schema/2.x/references.html) +for more information diff --git a/docs/docs/advanced-user-guide/json-schemas/validator.md b/docs/docs/advanced-user-guide/json-schemas/validator.md new file mode 100644 index 0000000..8d830ce --- /dev/null +++ b/docs/docs/advanced-user-guide/json-schemas/validator.md @@ -0,0 +1,62 @@ +WP-FastEndpoints uses [opis/json-schema](https://opis.io/json-schema/2.x/) for JSON schema validation. + +The reason we don't use the default +[WordPress JSON schema validation functionality](https://developer.wordpress.org/reference/functions/rest_validate_value_from_schema/) +is because it's quite outdated: it only partially supports [JSON schema draft 4](https://json-schema.org/specification#migrating-from-older-drafts). +[opis/json-schema](https://opis.io/json-schema/2.x/) on the other side, does support the latest JSON schema drafts. + +## Customising validator + +One of the coolest features of [opis/json-schema](https://opis.io/json-schema/2.x/) is that +is super flexible, and supports: + +- [Custom formats](https://opis.io/json-schema/2.x/php-format.html) +- [Custom filters](https://opis.io/json-schema/2.x/php-filter.html) +- [Custom media types](https://opis.io/json-schema/2.x/php-media-type.html) and +- [Custom content encoding](https://opis.io/json-schema/2.x/php-content-encoding.html) + +These, can be super useful when ever you need some custom functionality in your JSON schemas. + +### Available hooks + +There are three WordPress filter hooks that you can use to customise the JSON schema validators +used in WP-FastEndpoints: + +1. `fastendpoints_validator` - Triggered by both middlewares +2. `fastendpoints_schema_validator` - Only triggered for Schema middlewares validators +3. `fastendpoints_response_validator` - Only triggered for Response middlewares validators + +#### Example + +Imagine we only want to accept even numbers. To solve this issue, we might want to create a new custom format +for integers, called `even`, which checks if a given number is even, like: + +```php +use Opis\JsonSchema\Validator; + +/** + * Adds custom format resolvers to all JSON validators: request payload schema and response. + * + * @see fastendpoints_schema_validator - To update only the request payload schema validator, or + * @see fastendpoints_response_validator - To update only the response validator + */ +add_filter('fastendpoints_validator', function (Validator $validator): Validator { + $formatsResolver = $validator->parser()->getFormatResolver(); + $formatsResolver->registerCallable('integer', 'even', function (int $value): bool { + return $value % 2 === 0; + }); + + return $validator; +}); +``` + +Here is an example of a JSON schema using our custom `even` format: + +```json +{ + "type": "integer", + "format": "even" +} +``` + +More examples can be found in [Custom Formats docs Β»](https://opis.io/json-schema/2.x/php-format.html) diff --git a/docs/docs/advanced-user-guide/middlewares.md b/docs/docs/advanced-user-guide/middlewares.md new file mode 100644 index 0000000..8dfe04d --- /dev/null +++ b/docs/docs/advanced-user-guide/middlewares.md @@ -0,0 +1,47 @@ +Another cool feature of WP-FastEndpoints is the support for middlewares. + +Middlewares are pieces of code that can either run before and/or after a request is handled. + +At this stage, you might be already familiar with both the `schema(...)` and `returns(...)` +middlewares. However, you can also create your own. + +```php +use Wp\FastEndpoints\Contracts\Middleware; + +class MyCustomMiddleware extends Middleware +{ + /** + * Create this function if you want that your middleware is + * triggered when it receives a request and after checking + * the user permissions. + */ + public function onRequest(/* Type what you need */) + { + return; + } + + /** + * Create this function when you want your middleware to be + * triggered before sending a response to the client + */ + public function onResponse(/* Type what you need */) { + return; + } +} + +// Attach middleware to endpoint +$router->get('/test', function () { + return true; +}) +->middleware(new MyCustomMiddleware()); +``` + +???+ tip + You can create both methods in a middleware: `onRequest` and `onResponse`. + However, to save some CPU cycles only create the one you need [CPU emoji] + +## Responses + +If you need you can also take advantage of either WP_Error and WP_REST_Response to send +a direct response to the client. See [Responses page](/wp-fastendpoints/advanced-user-guide/responses) +for more info \ No newline at end of file diff --git a/docs/docs/advanced-user-guide/request-life-cycle.md b/docs/docs/advanced-user-guide/request-life-cycle.md new file mode 100644 index 0000000..0e3ea68 --- /dev/null +++ b/docs/docs/advanced-user-guide/request-life-cycle.md @@ -0,0 +1,75 @@ +In WP-FastEndpoints an endpoint can have multiple optional handlers attached to: + +1. Permission handlers via `hasCap(...)` or `permission(...)` - Used to check for user permissions +2. Middlewares + 1. Request Payload Schema Middleware via `schema(...)` - Validates the request payload + 2. Response Schema Middleware via `returns(...)` - Makes sure that the proper response is sent to the client + 3. Custom middlewares via `middleware(...)` - Any other custom logic that you might want to run + +## Permission handlers + +When a request is received the first handlers to run are the permissions handlers. Permission handlers are called +by WordPress via [`permission_callback`](https://developer.wordpress.org/rest-api/extending-the-rest-api/routes-and-endpoints/#callbacks). + +In contrast to WordPress, you can have one or multiple permission handlers attached to the same endpoint. + +???+ note + In the background all permission handlers are wrapped into one callable which is later on used as + `permission_callback` by the endpoint + +These handlers will then be called in the same order as they were attached. For instance: + +```php +$router->get('/test', function () {return true;}) +->hasCap('read') # Called first +->hasCap('edit_posts') # Called second if the first one was successful +->permission('__return_true') # Called last if both the first and second were successful +``` + +## Middlewares + +If all the permission handlers are successful the next set of handlers that run are the middlewares which +implement the `onRequest` function. + +Remember that a middleware can implement `onRequest` and/or `onResponse` functions. The first one, runs before +the main endpoint handler and the later one should run after the main endpoint handler. + +!!! warning + Please bear in mind that if either a [WP_Error](https://developer.wordpress.org/reference/classes/wp_error/) or + a [WP_REST_Response](https://developer.wordpress.org/reference/classes/wp_rest_response/) is returned by + the main endpoint handler following middlewares will not run. See + [Responses page](/wp-fastendpoints/advanced-user-guide/responses) for more info. + +### onRequest + +Same as with the permission handlers, middlewares are called with the same order that they were attached. + +```php +class OnRequestMiddleware extends \Wp\FastEndpoints\Contracts\Middleware +{ + public function onRequest(/* Type what you need */){ + return; + } +} + +$router->post('/test', function () {return true;}) +->middleware(OnRequestMiddleware()) # Called first +->schema('Basics/Bool'); # Called second +``` + +### onResponse + +Likewise, middlewares implementing onResponse functions will be triggered in the same order as they were attached. + +```php +class OnResponseMiddleware extends \Wp\FastEndpoints\Contracts\Middleware +{ + public function onResponse(/* Type what you need */){ + return; + } +} + +$router->post('/test', function () {return true;}) +->returns('Basics/Bool') # Called first +->middleware(OnResponseMiddleware()); # Called second +``` diff --git a/docs/docs/advanced-user-guide/responses.md b/docs/docs/advanced-user-guide/responses.md new file mode 100644 index 0000000..f0bf53e --- /dev/null +++ b/docs/docs/advanced-user-guide/responses.md @@ -0,0 +1,52 @@ +When building an API sometimes we want to return a response directly to the client. For example: + +```php +$router->get('/posts/(?P[\d]+)', function ($ID) { + return get_post($ID); +}) +->returns('Posts/Get'); // It will raise a 422 HTTP error when we are unable to find a post +``` + +The code above, will raise a 422 HTTP status code error when ever we are unable to find +a given post. This is where returning a message directly to the client can be useful. + +## Early return + +To trigger those scenarios we can either return a WP_Error or a WP_REST_Response. + +=== "WP_REST_Response" + ```php + $router->get('/posts/(?P[\d]+)', function ($ID) { + $post = get_post($ID); + return $post ?: new WP_REST_Response("No posts found", 404); + }) + ->returns('Posts/Get'); // This will not be triggered if no posts are found + ``` +=== "WP_Error" + ```php + $router->get('/posts/(?P[\d]+)', function ($ID) { + $post = get_post($ID); + return $post ?: new WpError(404, "No posts found"); + }) + ->returns('Posts/Get'); // This will not be triggered if no posts are found + ``` + +### Difference between returning WP_REST_Response or WP_Error + +The main difference between returning a WP_Error or a WP_REST_Response +is regarding the JSON returned in the body. + +=== "WP_REST_Response" + ```json + "No posts found" + ``` +=== "WP_Error" + ```json + { + "error": 404, + "message": "No posts found", + "data": { + "status": 404 + } + } + ``` diff --git a/docs/docs/index.md b/docs/docs/index.md index 7d27015..d8546d3 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -22,7 +22,7 @@ hide: - Middlewares support - IDE auto completion support - No magic router. It uses WordPress [`register_rest_route`](https://developer.wordpress.org/reference/functions/register_rest_route/) -- Support for newer JSON schema drafts thanks to [json/opis](https://opis.io/json-schema/2.x/) +- Support for newer JSON schema drafts thanks to [opis/json-schema](https://opis.io/json-schema/2.x/) ## Requirements diff --git a/docs/docs/quick-start/json-schemas.md b/docs/docs/quick-start/json-schemas.md index 4477194..653a0b7 100644 --- a/docs/docs/quick-start/json-schemas.md +++ b/docs/docs/quick-start/json-schemas.md @@ -1,6 +1,6 @@ For this scenario, we are going to create two JSON schemas: 1) for validating the request payload and another 2) to discard unwanted fields from responses (e.g. sensitive information). -### Request payload (create/update) +## Request payload (create/update) For the payload we decided to accept the following fields: 1) _post_title_, 2) _post_status_, 3) _post_type_ and 4) optionally _post_content_ @@ -27,7 +27,7 @@ For the payload we decided to accept the following fields: 1) _post_title_, 2) _ } ``` -### Response (retrieve) +## Response (retrieve) For the response we decided to only return the following fields: 1) _post_title_ and 2) optionally _post_excerpt_ @@ -46,3 +46,5 @@ For the response we decided to only return the following fields: 1) _post_title_ "required": ["post_title"] } ``` + + diff --git a/docs/docs/quick-start/router.md b/docs/docs/quick-start/router.md index bb7decc..037668f 100644 --- a/docs/docs/quick-start/router.md +++ b/docs/docs/quick-start/router.md @@ -14,7 +14,7 @@ by same namespace and (optionally) same version. For instance, in this tutorial we are going to create a main router with a base namespace `my-plugin` and a version of `v1` which will add `/my-plugin/v1/` to the beginning of each attached endpoint from all sub-routers. -#### Create a post +## Create a post With the posts router in place we can now start attaching our endpoints. We start adding the one responsible to create a new blog post. @@ -42,7 +42,7 @@ When a request is received by this endpoint the following happens: In this scenario we are not using a JSON schema to discard fields because the [_wp_insert_post_](https://developer.wordpress.org/reference/functions/wp_insert_post/) either returns the ID of the post or a WP_Error which is already what we want 😊 -#### Retrieve a post +## Retrieve a post Some endpoints however do need to return more complex objects. And in those cases JSON schemas can be of a great help. @@ -81,7 +81,7 @@ Going back to the endpoint, this is what happens if a request comes in: The [WpError](https://github.com/matapatos/wp-fastendpoints/blob/main/src/Helpers/WpError.php) is just a subclass of WP_Error which automatically set's the HTTP status code of the response -#### Update a post +## Update a post Checking for user capabilities such as `publish_posts` and `read` is cool. However, in the real world we sometimes also need to check for a particular resource. @@ -106,7 +106,7 @@ which will try to replace it by the _post_id_ parameter. in an endpoint is checking the user capabilities. As such, at that time the request params have not been already validated by the request payload schema. -#### Delete a post +## Delete a post ```php use Wp\FastEndpoints\Helpers\WpError; @@ -120,7 +120,7 @@ $router->delete('(?P[\d]+)', function ($ID) { ->hasCap('delete_post', '{ID}'); ``` -### Everything together +## Everything together ```php """ diff --git a/docs/docs/reference/index.md b/docs/docs/reference/index.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/docs/release.md b/docs/docs/release.md index 306cb45..8b8058d 100644 --- a/docs/docs/release.md +++ b/docs/docs/release.md @@ -21,10 +21,10 @@ add_filter('fastendpoints_validator', function (Validator $validator): Validator !!! info For more customisations check the following links: - 1) [Custom formats](https://opis.io/json-schema/2.x/php-format.html), - 2) [Custom filters](https://opis.io/json-schema/2.x/php-filter.html), - 3) [Custom media types](https://opis.io/json-schema/2.x/php-media-type.html) and, - 4) [Custom content encoding](https://opis.io/json-schema/2.x/php-content-encoding.html) + 1. [Custom formats](https://opis.io/json-schema/2.x/php-format.html), + 2. [Custom filters](https://opis.io/json-schema/2.x/php-filter.html), + 3. [Custom media types](https://opis.io/json-schema/2.x/php-media-type.html) and, + 4. [Custom content encoding](https://opis.io/json-schema/2.x/php-content-encoding.html) ## v1.2.1 diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 469af74..f90ccf8 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -39,6 +39,8 @@ markdown_extensions: - pymdownx.superfences - pymdownx.highlight: use_pygments: false + - pymdownx.tabbed: + alternate_style: true nav: - WP-FastEndpoints: index.md - Quick Start: @@ -50,7 +52,16 @@ nav: - Testing: quick-start/tests/index.md - Integration Tests: quick-start/tests/integration.md - Unit Tests: quick-start/tests/unit.md - - Reference: reference/index.md + - Advanced User Guide: + - Advanced User Guide: advanced-user-guide/index.md + - Request Life Cycle: advanced-user-guide/request-life-cycle.md + - Dependency Injection: advanced-user-guide/dependency-injection.md + - Responses: advanced-user-guide/responses.md + - Middlewares: advanced-user-guide/middlewares.md + - JSON Schemas: + - Multiple Root Dirs: advanced-user-guide/json-schemas/multiple-root-dirs.md + - Validator: advanced-user-guide/json-schemas/validator.md + - References: advanced-user-guide/json-schemas/references.md - Release Notes: release.md extra: social: diff --git a/src/Endpoint.php b/src/Endpoint.php index 5bbf92b..bf7aeda 100644 --- a/src/Endpoint.php +++ b/src/Endpoint.php @@ -312,20 +312,20 @@ public function callback(WP_REST_Request $request): WP_REST_Response|WP_Error 'request' => $request, 'response' => new WP_REST_Response(), ] + $request->get_url_params(); - // Request handlers. + // onRequest handlers. $result = $this->runHandlers($this->onRequestHandlers, $dependencies); if (\is_wp_error($result) || $result instanceof WP_REST_Response) { return $result; } - // Main handler. + // Main endpoint handler. $result = $this->invoker->call($this->handler, $dependencies); if (\is_wp_error($result) || $result instanceof WP_REST_Response) { return $result; } $dependencies['response']->set_data($result); - // ResponseMiddleware handlers. + // onResponse handlers. $result = $this->runHandlers($this->onResponseHandlers, $dependencies); if (\is_wp_error($result) || $result instanceof WP_REST_Response) { return $result;