Skip to content

Commit

Permalink
docs: github doc pages
Browse files Browse the repository at this point in the history
  • Loading branch information
matapatos committed May 18, 2024
1 parent d9aff8b commit 823e883
Show file tree
Hide file tree
Showing 12 changed files with 622 additions and 0 deletions.
4 changes: 4 additions & 0 deletions docs/data/sponsors.yml
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
50 changes: 50 additions & 0 deletions docs/docs/index.md
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 -->
44 changes: 44 additions & 0 deletions docs/docs/quick-start/index.md
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
```

48 changes: 48 additions & 0 deletions docs/docs/quick-start/json-schemas.md
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"]
}
```
184 changes: 184 additions & 0 deletions docs/docs/quick-start/router.md
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;
```
46 changes: 46 additions & 0 deletions docs/docs/quick-start/service-provider.md
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)**
Loading

0 comments on commit 823e883

Please sign in to comment.