-
Notifications
You must be signed in to change notification settings - Fork 1
Quick start
To better exemplify the benefits of using FastEndpoints we are going to build an API for manipulating blog posts.
This API will be able to:
- Create
- Retrieve
- Update and
- Delete a blog post
Full source code can be found at matapatos/wp-fastendpoints-my-plugin »
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).
For the payload we decided to accept the following fields: 1) post_title, 2) post_status, 3) post_type and 4) optionally post_content
{
"type": "object",
"properties": {
"post_title": {
"type": "string"
},
"post_status": {
"enum": ["publish", "draft", "private"],
"default": "publish"
},
"post_type": {
"const": "post"
},
"post_content": {
"type": "string",
"contentMediaType": "text/html"
}
},
"required": ["post_title", "post_status", "post_type"]
}
For the response we decided to only return the following fields: 1) post_title and 2) optionally post_excerpt
{
"type": "object",
"properties": {
"post_title": {
"type": "string"
},
"post_excerpt": {
"type": "string",
"contentMediaType": "text/html"
}
},
"required": ["post_title"]
}
With our JSON schemas done we can now start writing some code 🤓
In this case, we are creating a plugin - don't forget that logic shouldn't be contained in a theme - with the following structure:
my-plugin
│ my-plugin.php # Registers the plugin provider
│ composer.json
│
└───src
│ │ constants.php
│ │
│ └───Api
│ │ │
│ │ └───Routers
│ │ │ │ Posts.php # Holds our custom endpoints
│ │ │
│ │ └───Schemas
│ │ │
│ │ └───Posts
│ │ │ CreateOrUpdate.json # Request payload schema
│ │ │ Get.json # Response schema
│ │
│ │
│ └───Providers
│ │ ApiServiceProvider.php # Registers all routers
│ │ MyPluginProvider.php # Bootstraps my plugin
│ │ ProviderContract.php
│
└───tests
The first thing we need to do is to create a Router.
use Wp\FastEndpoints\Router;
// Dependency injection to enable us to mock router in tests
$router = $router ?? new Router('posts');
A router is just an instance which allow us to attach and register endpoints.
We can have an application with one or multiple routers. One main benefit of a router is that allows us to share a namespace and (optionally) a version to all attached endpoints or sub routers. 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 registered endpoint.
With the posts router in place we can now start attaching our endpoints. We start adding the one to that allows a user to create a blog post.
$router->post('/', function (\WP_REST_Request $request): int|\WP_Error {
$payload = $request->get_params();
return wp_insert_post($payload);
})
->schema('Posts/CreateOrUpdate')
->hasCap('publish_posts');
When a request is received by this endpoint the following happens:
- Firstly, the user permissions are checked - Makes sure that the user has publish_posts capability
- Then, if successful, it validates the request payload by using the Posts/CreateOrUpdate schema. We still didn't specify where the endpoints should look for the schemas, but don't worry we are getting into that in a moment
- Lastly, if the validation process also passes the handler is called.
PS: In this scenario we are not using a JSON schema to discard fields because the wp_insert_post either returns the ID of the post or a WP_Error which is already what we want 😊
Some endpoints however do need to return more complex objects. And in those cases JSON schemas can be of a great help.
JSON schemas can help us to make sure that we are returning all the required fields as well as to avoid retrieving sensitive information. The last one is configurable.
use Wp\FastEndpoints\Helpers\WpError;
$router->get('(?P<post_id>[\d]+)', function (\WP_REST_Request $request) {
$postId = $request->get_param('post_id');
$post = get_post($postId);
return $post ?: new WpError(404, 'Post not found');
})
->returns('Posts/Get')
->hasCap('read');
In this case, we didn't set a JSON schema on purpose because we only need the post_id which is already parsed by the regex rule - we could have made that rule to match only positive integers though 🤔
Going back to the endpoint, this is what happens if a request comes in:
- Firstly, it checks the user has read capability - one of the lowest WordPress users capabilities
- If so, it then calls the handler which either retrieves the post data (e.g. array or object) or a WpError in case that is not found. If a WpError or WP_Error is returned it stops further code execution and returns that error message to the client - avoiding triggering response schema validation for example.
- Lastly, if the post data is returned by the handler the response schema will be triggered and will check the response according to the given schema (e.g. Posts/Get)
NOTE: The WpError is just a subclass of WP_Error which automatically set's the HTTP status code of the response
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.
$router->put('(?P<post_id>[\d]+)', function (\WP_REST_Request $request): int|\WP_Error {
$payload = $request->get_params();
$payload['ID'] = $request->get_param('post_id');
return wp_update_post($payload);
})
->schema('Posts/CreateOrUpdate')
->hasCap('edit_post', '{post_id}');
The code above is not that different from the one for creating a post. However, in the last line
hasCap('edit_post', '{post_id}')
the second parameter is a special one for FastEndpoints
which will try to replace it by the post_id parameter.
Important: FastEndpoints will only replace the {PARAM_NAME} if that parameter exists in the request payload. Otherwise, will not touch it. Also, bear in mind that the first stage 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.
use Wp\FastEndpoints\Helpers\WpError;
$router->delete('(?P<post_id>[\d]+)', function (\WP_REST_Request $request) {
$postId = $request->get_param('post_id');
$post = wp_delete_post($postId);
return $post ?: new WpError(500, 'Unable to delete post');
})
->returns('Posts/Get')
->hasCap('delete_post', '{post_id}');
"""
Api/Endpoints/Posts.php
"""
declare(strict_types=1);
namespace MyPlugin\Api\Routers;
use Wp\FastEndpoints\Helpers\WpError;
use Wp\FastEndpoints\Router;
// Dependency injection to enable us to mock router later on in the Unit tests
$router = $router ?? new Router('posts');
// Creates a post
$router->post('/', function (\WP_REST_Request $request): int|\WP_Error {
$payload = $request->get_params();
return wp_insert_post($payload);
})
->schema('Posts/CreateOrUpdate')
->hasCap('publish_posts');
// Fetches a single post
$router->get('(?P<post_id>[\d]+)', function (\WP_REST_Request $request) {
$postId = $request->get_param('post_id');
$post = get_post($postId);
return $post ?: new WpError(404, 'Post not found');
})
->returns('Posts/Get')
->hasCap('read');
// Updates a post
$router->put('(?P<post_id>[\d]+)', function (\WP_REST_Request $request): int|\WP_Error {
$payload = $request->get_params();
$payload['ID'] = $request->get_param('post_id');
return wp_update_post($payload);
})
->schema('Posts/CreateOrUpdate')
->hasCap('edit_post', '{post_id}');
// Deletes a post
$router->delete('(?P<post_id>[\d]+)', function (\WP_REST_Request $request) {
$postId = $request->get_param('post_id');
$post = wp_delete_post($postId);
return $post ?: new WpError(500, 'Unable to delete post');
})
->returns('Posts/Get')
->hasCap('delete_post', '{post_id}');
// IMPORTANT: If no service provider is used make sure to set a version to the $router and call
// the following function here:
// $router->register();
// Used later on by the ApiProvider
return $router;
Now that we have our posts router built the last main three bits missing are the following:
- create a main router to hold all sub-routers (e.g. posts router)
- specifying where to look for the JSON schemas (one or multiple directories) and
- lastly register the router. This is what adds the
rest_api_init
hook for registering all the endpoints.
"""
src/Providers/ApiProvider.php
"""
declare(strict_types=1);
namespace MyPlugin\Providers;
use Wp\FastEndpoints\Router;
class ApiProvider implements ProviderContract
{
protected Router $appRouter;
public function register(): void
{
$this->appRouter = new Router('my-plugin', 'v1');
$this->appRouter->appendSchemaDir(\SCHEMAS_DIR);
foreach (glob(\ROUTERS_DIR.'/*.php') as $filename) {
$router = require $filename;
$this->appRouter->includeRouter($router);
}
$this->appRouter->register();
}
}
NOTE: By adding the schema directory to the main router it will share it across all sub-routers.
🎉 Congrats you just created your first set of REST FastEndpoints
Now let's see how to test it out! 😄
Full source code can be found at matapatos/wp-fastendpoints-my-plugin »
FastEndpoints was created by André Gil and is open-sourced software licensed under the MIT license.