-
Notifications
You must be signed in to change notification settings - Fork 1
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
Showing
12 changed files
with
622 additions
and
0 deletions.
There are no files selected for viewing
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,4 @@ | ||
companies: | ||
- url: https://www.myspanishnow.com/ | ||
title: "One-to-one online Spanish lessons to connect with your family and friends." | ||
img: https://avatars.githubusercontent.com/u/128193617?s=400&u=47f022a3fad60ddade1add885f56eb9ceb830145&v=4 |
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,50 @@ | ||
--- | ||
hide: | ||
- navigation | ||
--- | ||
|
||
<img src="https://raw.githubusercontent.com/matapatos/wp-fastendpoints/main/docs/images/wp-fastendpoints-wallpaper.png" alt="WordPress REST endpoints made easy"> | ||
<p align="center"> | ||
<a href="https://github.com/matapatos/wp-fastendpoints/actions"><img alt="GitHub Actions Workflow Status (main)" src="https://img.shields.io/github/actions/workflow/status/matapatos/wp-fastendpoints/tests.yml"></a> | ||
<a href="https://codecov.io/gh/matapatos/wp-fastendpoints" ><img alt="Code Coverage" src="https://codecov.io/gh/matapatos/wp-fastendpoints/graph/badge.svg?token=8N7N9NMGLG"/></a> | ||
<a href="https://packagist.org/packages/matapatos/wp-fastendpoints"><img alt="Latest Version" src="https://img.shields.io/packagist/v/matapatos/wp-fastendpoints"></a> | ||
<a href="https://packagist.org/packages/matapatos/wp-fastendpoints"><img alt="Supported WordPress Versions" src="https://img.shields.io/badge/6.x-versions?logo=wordpress&label=versions"></a> | ||
<a href="https://packagist.org/packages/matapatos/wp-fastendpoints"><img alt="Software License" src="https://img.shields.io/packagist/l/matapatos/wp-fastendpoints"></a> | ||
</p> | ||
|
||
------ | ||
**FastEndpoints** is an elegant way of writing custom WordPress REST endpoints with a focus on readability and IDE auto completion support. | ||
|
||
## Features | ||
|
||
- Decouples request validation from main logic | ||
- Removes unwanted fields from responses | ||
- 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/) | ||
|
||
## Requirements | ||
|
||
- PHP 8.1+ | ||
- WordPress 6.x | ||
- [opis/json-schema](https://opis.io/json-schema/2.x/) | ||
- [php-di/invoker](https://packagist.org/packages/php-di/invoker) | ||
|
||
We aim to support versions that haven't reached their end-of-life. | ||
|
||
## Installation | ||
|
||
```bash | ||
composer require wp-fastendpoints | ||
``` | ||
|
||
## Sponsors | ||
|
||
{% if sponsors %} | ||
{% for sponsor in sponsors.companies -%} | ||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px; max-width: 200px; max-height: 200px;"></a> | ||
{% endfor -%} | ||
{% endif %} | ||
|
||
<!-- /sponsors --> |
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,44 @@ | ||
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 »](https://github.com/matapatos/wp-fastendpoints-my-plugin)** | ||
|
||
## Plugin code structure 🔨 | ||
|
||
To hold this API we are going to create a plugin called *MyPLugin* - 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 # Validates our request payload | ||
│ │ │ Get.json # Discards unwanted fields from our response | ||
│ │ | ||
│ │ | ||
│ └───Providers | ||
│ │ ApiServiceProvider.php # Registers all routers | ||
│ │ MyPluginProvider.php # Bootstraps our plugin | ||
│ │ ProviderContract.php | ||
│ | ||
└───tests | ||
``` | ||
|
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,48 @@ | ||
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) | ||
|
||
For the payload we decided to accept the following fields: 1) _post_title_, 2) _post_status_, 3) _post_type_ and 4) optionally _post_content_ | ||
|
||
```json | ||
{ | ||
"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"] | ||
} | ||
``` | ||
|
||
### Response (retrieve) | ||
|
||
For the response we decided to only return the following fields: 1) _post_title_ and 2) optionally _post_excerpt_ | ||
|
||
```json | ||
{ | ||
"type": "object", | ||
"properties": { | ||
"post_title": { | ||
"type": "string" | ||
}, | ||
"post_excerpt": { | ||
"type": "string", | ||
"contentMediaType": "text/html" | ||
} | ||
}, | ||
"required": ["post_title"] | ||
} | ||
``` |
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,184 @@ | ||
The first thing we need to do is to create a Router. | ||
|
||
```php | ||
<?php | ||
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 using multiple routers is to group endpoints 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 sub-routers. | ||
|
||
#### Create a post | ||
|
||
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. | ||
|
||
```php | ||
<?php | ||
$router->post('/', function (\WP_REST_Request $request): int|\WP_Error { | ||
$payload = $request->get_params(); | ||
|
||
return wp_insert_post($payload, true); | ||
}) | ||
->schema('Posts/CreateOrUpdate') | ||
->hasCap('publish_posts'); | ||
``` | ||
|
||
When a request is received by this endpoint the following happens: | ||
|
||
1) Firstly, the user permissions are checked - Makes sure that the user has [*publish_posts*](https://wordpress.org/documentation/article/roles-and-capabilities/#publish_posts) capability | ||
2) 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 | ||
3) Lastly, if the validation process also passes the handler is called. | ||
|
||
!!! info | ||
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 | ||
|
||
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. | ||
|
||
```php | ||
<?php | ||
use Wp\FastEndpoints\Helpers\WpError; | ||
|
||
$router->get('(?P<ID>[\d]+)', function ($ID) { | ||
$post = get_post($ID); | ||
|
||
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: | ||
|
||
1) Firstly, it checks the user has [_read_](https://wordpress.org/documentation/article/roles-and-capabilities/#read) | ||
capability - one of the lowest WordPress users capabilities | ||
2) If so, it then calls the handler which either retrieves the post data (e.g. array or object) | ||
or a [_WpError_](https://github.com/matapatos/wp-fastendpoints/blob/main/src/Helpers/WpError.php) | ||
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. | ||
3) 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](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 | ||
|
||
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. | ||
|
||
```php | ||
<?php | ||
$router->put('(?P<ID>[\d]+)', function (\WP_REST_Request $request): int|\WP_Error { | ||
$payload = $request->get_params(); | ||
|
||
return wp_update_post($payload, true); | ||
}) | ||
->schema('Posts/CreateOrUpdate') | ||
->hasCap('edit_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. | ||
|
||
!!! warning | ||
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. | ||
|
||
#### Delete a post | ||
|
||
```php | ||
<?php | ||
use Wp\FastEndpoints\Helpers\WpError; | ||
|
||
$router->delete('(?P<ID>[\d]+)', function ($ID) { | ||
$post = wp_delete_post($postId); | ||
|
||
return $post ?: new WpError(500, 'Unable to delete post'); | ||
}) | ||
->returns('Posts/Get') | ||
->hasCap('delete_post', '{ID}'); | ||
``` | ||
|
||
### Everything together | ||
|
||
```php | ||
<?php | ||
""" | ||
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 in the 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, true); | ||
}) | ||
->schema('Posts/CreateOrUpdate') | ||
->hasCap('publish_posts'); | ||
|
||
// Fetches a single post | ||
$router->get('(?P<ID>[\d]+)', function ($ID) { | ||
$post = get_post($ID); | ||
|
||
return $post ?: new WpError(404, 'Post not found'); | ||
}) | ||
->returns('Posts/Get') | ||
->hasCap('read'); | ||
|
||
// Updates a post | ||
$router->put('(?P<ID>[\d]+)', function (\WP_REST_Request $request): int|\WP_Error { | ||
$payload = $request->get_params(); | ||
|
||
return wp_update_post($payload, true); | ||
}) | ||
->schema('Posts/CreateOrUpdate') | ||
->hasCap('edit_post', '{ID}'); | ||
|
||
// Deletes a post | ||
$router->delete('(?P<ID>[\d]+)', function ($ID) { | ||
$post = wp_delete_post($postId); | ||
|
||
return $post ?: new WpError(500, 'Unable to delete post'); | ||
}) | ||
->returns('Posts/Get') | ||
->hasCap('delete_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; | ||
``` |
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,46 @@ | ||
Now that we have our posts router built the last main three bits missing are the following: | ||
|
||
1) create a main router to hold all sub-routers (e.g. posts router) | ||
2) specifying where to look for the JSON schemas (one or multiple directories) and | ||
3) lastly register the router. This is what adds the `rest_api_init` hook for registering all | ||
the endpoints. | ||
|
||
```php | ||
<?php | ||
""" | ||
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, 'http://www.my-plugin.com'); | ||
foreach (glob(\ROUTERS_DIR.'/*.php') as $filename) { | ||
$router = require $filename; | ||
$this->appRouter->includeRouter($router); | ||
} | ||
$this->appRouter->register(); | ||
} | ||
} | ||
``` | ||
|
||
!!! tip | ||
Adding the schema directory to the main router will share it across | ||
all sub-routers. | ||
|
||
## It's running | ||
|
||
🎉 Congrats you just created your first set of REST FastEndpoints | ||
|
||
Now let's see [how to test it out](https://github.com/matapatos/wp-fastendpoints/wiki/Testing)! 😄 | ||
|
||
Full source code can be found at **[matapatos/wp-fastendpoints-my-plugin »](https://github.com/matapatos/wp-fastendpoints-my-plugin)** |
Oops, something went wrong.