diff --git a/docs/data/sponsors.yml b/docs/data/sponsors.yml new file mode 100644 index 0000000..eda4759 --- /dev/null +++ b/docs/data/sponsors.yml @@ -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 diff --git a/docs/docs/index.md b/docs/docs/index.md new file mode 100644 index 0000000..7d27015 --- /dev/null +++ b/docs/docs/index.md @@ -0,0 +1,50 @@ +--- +hide: + - navigation +--- + + +
+ +------ +**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 -%} + +{% endfor -%} +{% endif %} + + diff --git a/docs/docs/quick-start/index.md b/docs/docs/quick-start/index.md new file mode 100644 index 0000000..3e90a30 --- /dev/null +++ b/docs/docs/quick-start/index.md @@ -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 +``` + diff --git a/docs/docs/quick-start/json-schemas.md b/docs/docs/quick-start/json-schemas.md new file mode 100644 index 0000000..4477194 --- /dev/null +++ b/docs/docs/quick-start/json-schemas.md @@ -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"] +} +``` diff --git a/docs/docs/quick-start/router.md b/docs/docs/quick-start/router.md new file mode 100644 index 0000000..9411e91 --- /dev/null +++ b/docs/docs/quick-start/router.md @@ -0,0 +1,184 @@ +The first thing we need to do is to create a Router. + +```php +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 +get('(?PMessage body
', + ]); + $response = $this->server->dispatch($request); + expect($response->get_status())->toBe(200); + $postId = $response->get_data(); + // Check that the post details are correct + expect(get_post($postId)) + ->toBeInstanceOf(\WP_Post::class) + ->toHaveProperty('post_title', 'My testing message') + ->toHaveProperty('post_status', 'publish') + ->toHaveProperty('post_type', 'post') + ->toHaveProperty('post_content', 'Message body
'); +})->group('api', 'posts'); +``` + +Here, we take advantage of the existent [testing factories](https://make.wordpress.org/core/handbook/testing/automated-testing/writing-phpunit-tests/#fixtures-and-factories) +to create a single user with the necessary capability to publish posts. +Then, we make mimic a REST request from that given user, and lastly, we check if that +blog post was created. diff --git a/docs/docs/quick-start/tests/unit.md b/docs/docs/quick-start/tests/unit.md new file mode 100644 index 0000000..376fcad --- /dev/null +++ b/docs/docs/quick-start/tests/unit.md @@ -0,0 +1,41 @@ +As an example of a unit test, we are going add a test to check the 1) request payload schema +used and 2) the necessary user permissions on the endpoint that allows a user to create a new blog post. + +We could have separated each assertion in its own unit test but for the sake of simplicity we +are going to make both of them in the same test. + +```php +shouldReceive('schema') + ->once() + ->with('Posts/CreateOrUpdate') + ->andReturnSelf(); + // Assert that user permissions are correct + $endpoint + ->shouldReceive('hasCap') + ->once() + ->with('publish_posts'); + // Create router. Make sure that var name matches your router variable + $router = Mockery::mock(Router::class) + ->makePartial(); + // Assert that router endpoint is called + $router + ->shouldReceive('post') + ->once() + ->with('/', Mockery::type('callable')) + ->andReturn($endpoint); + // Needed to attach endpoints + require \ROUTERS_DIR.'/Posts.php'; +})->group('api', 'posts'); +``` + +The reason we are able to make the assertions above is +[due to this line](https://github.com/matapatos/wp-fastendpoints/wiki/Quick-start#the-actual-code---srcapirouterspostsphp). +Specially, regarding this part ```$router ??```. This allows us to replace our original router with our mocked version. + +As you might notice, this is just pure PHP code nothing magical! πͺ diff --git a/docs/docs/reference/index.md b/docs/docs/reference/index.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/docs/release.md b/docs/docs/release.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 0000000..36ad49a --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,61 @@ +site_name: WP-FastEndpoints +site_description: FastEndpoints is an elegant way of writing custom WordPress REST endpoints with a focus on readability and IDE auto completion support +site_url: https://matapatos.github.io/wp-fastendpoints/ +watch: + - data + - docs + - images +theme: + name: material + palette: + primary: indigo + features: + - search.suggest + - search.highlight + - content.tabs.link + - navigation.indexes + - content.tooltips + - navigation.path + - content.code.annotate + - content.code.copy + - content.code.select + - navigation.tabs + - navigation.footer + icon: + repo: fontawesome/brands/github-alt + language: en +repo_name: matapatos/wp-fastendpoints +repo_url: https://github.com/matapatos/wp-fastendpoints +edit_uri: https://github.com/matapatos/wp-fastendpoints/tree/main/docs/docs +plugins: + search: null + macros: + include_yaml: + - sponsors: data/sponsors.yml +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.highlight: + anchor_linenums: true + pygments_lang_class: true +nav: + - WP-FastEndpoints: index.md + - Quick Start: + - Quick Start: quick-start/index.md + - JSON Schemas: quick-start/json-schemas.md + - Router: quick-start/router.md + - Service Provider: quick-start/service-provider.md + - Testing: + - Testing: quick-start/tests/index.md + - Integration Tests: quick-start/tests/integration.md + - Unit Tests: quick-start/tests/unit.md + - Reference: reference/index.md + - Release Notes: release.md +extra: + social: + - icon: fontawesome/brands/github-alt + link: https://github.com/matapatos/wp-fastendpoints + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/andre-gil/