...
Restful Mobile API design & Implementation
On this section, we will go through RESTful API design concepts, how to use and extend.
...
We define some of convention way to mapping from a data source to resource's properties
Map resource's properties by naming convention:
If the data source has key same with the property name. It's auto mapped
Map with User Resource
If the data source has a set of "user_" prefix key and `user` property, It auto combine field to UserResource and Map to user property of the current resource
Manual mapping
Use We can override or manual mapping fields use Setter and Getter methods. Setter method uses to control input data, Getter control the output. The naming convention of setter & getter methods follow examples bellowAll field
Resource's property name | Setter method | Getter method |
---|---|---|
name | setName() | getName() |
short_description | setShortDescription() | getShortDescription() |
is_approved | setIsApproved() | getIsApproved() |
All fields query from the database table has a String data type. But Native App requires to return exactly data type in JSON.
Override "loadMetadataSchema" method of resource to control the response data type.
Following is an example:
Code Block |
---|
<?php /* A lot of code above */ class PostResource extends ResourceBase { /* ... */ protected function loadMetadataSchema(ResourceMetadata $metadata = null) { parent::loadMetadataSchema($metadata); $this->metadata ->mapField('title', ['type' => ResourceMetadata::STRING]) ->mapField('description', ['type' => ResourceMetadata::STRING]) ->mapField('item_id', ['type' => ResourceMetadata::INTEGER]) ->mapField('module_id', ['type' => ResourceMetadata::STRING]) ->mapField('is_approved', ['type' => ResourceMetadata::BOOL]) ->mapField('is_sponsor', ['type' => ResourceMetadata::BOOL]) ->mapField('is_featured', ['type' => ResourceMetadata::BOOL]) ->mapField('is_liked', ['type' => ResourceMetadata::BOOL]) ->mapField('is_friend', ['type' => ResourceMetadata::BOOL]) ->mapField('post_status', ['type' => ResourceMetadata::INTEGER]); } } |
Develop restful resource APIs
...
Code Block |
---|
<?php namespace Apps\Posts\Api\Resource; /* ... */ class PostResource extends ResourceBase { /** Define the unique resource name */ const RESOURCE_NAME = "post"; const TAG_CATEGORY = 'post'; public $resource_name = self::RESOURCE_NAME; public $module_name = 'post'; /** @var string Post's title mapping */ public $title; /** @var string Post's description mapping */ public $description; /** @var string Post's parent module id */ public $module_id; /** @var int Post's parent id */ public $item_id; /** @var bool Status of the post */ public $is_approved; /** @var bool sponsor status of the post */ public $is_sponsor; /** @var bool sponsor status of the post */ public $is_featured; public $is_liked; public $is_friend; public $post_status; public $text; |
Override ResourceBase::loadMetadataSchema() method to define database for each field when mapping
Code Block |
---|
<?php /* A lot of code above */ class PostResource extends ResourceBase { /* ... */ protected function loadMetadataSchema(ResourceMetadata $metadata = null) { parent::loadMetadataSchema($metadata); $this->metadata ->mapField('title', ['type' => ResourceMetadata::STRING]) ->mapField('description', ['type' => ResourceMetadata::STRING]) ->mapField('item_id', ['type' => ResourceMetadata::INTEGER]) ->mapField('module_id', ['type' => ResourceMetadata::STRING]) ->mapField('is_approved', ['type' => ResourceMetadata::BOOL]) ->mapField('is_sponsor', ['type' => ResourceMetadata::BOOL]) ->mapField('is_featured', ['type' => ResourceMetadata::BOOL]) ->mapField('is_liked', ['type' => ResourceMetadata::BOOL]) ->mapField('is_friend', ['type' => ResourceMetadata::BOOL]) ->mapField('post_status', ['type' => ResourceMetadata::INTEGER]); } } |
If mapping base on the field's name and data type are not enough. We can override the property's value via getting/setting method
Code Block |
---|
<?php /* A lot of code above */ class PostResource extends ResourceBase { /* ... */ /** Get detail url @return string */ public function getLink() { return \Phpfox::permalink('post', $this->id, $this->title); } public function getImage() { return Image::createFrom([ 'file' => $this->rawData['image_path'], 'server_id' => $this->rawData['server_id'], 'path' => 'post.url_photo', 'suffix' => '_1024' ]); } /** @return array @throws \Exception */ public function getCategories() { return $this->categories; } public function getTags() { return $this->tags; } public function getText() { if (empty($this->text) && !empty($this->rawData['text'])) { $this->text = $this->rawData['text']; } TextFilter::pureHtml($this->text, true); return $this->text; } /* ... */ } |
Define API service to handle API requests
API Service is similar to PHPFOX App's Service, you can extend from the parent class to minimal and reuse code
Create PostApi.php within Service folder
PostApi.php
Code Block |
---|
<?php namespace Apps\Posts\Service; /* ... */ class PostApi extends AbstractResourceApi implements ActivityFeedInterface, MobileAppSettingInterface { /** @var Post */ private $postService; /** @var Process */ private $processService; /** @var Category */ private $categoryService; /** @var \User_Service_User */ private $userService; public function __construct() { parent::__construct(); $this->postService = Phpfox::getService("post"); $this->categoryService = Phpfox::getService('post.category'); $this->processService = Phpfox::getService('post.process'); $this->userService = Phpfox::getService('user'); } /* .. */ } |
In
Describe the example above:
- The PostApi class extent AbstractResourceApi to reuse resource base featureall features
- Implement ActivityFeedInterface to able display the resource to Activity Feed page
- Implement MobileAppSettingInterface to register more screens and actions to Mobile App without change code
Now you need to implement all abstract method from parent class and interfaces.
The initial code of your service would look like bellowlike below:
PostApi.php
Code Block |
---|
<?php namespace Apps\Posts\Service; /* ... */ class PostApi extends AbstractResourceApi implements ActivityFeedInterface, MobileAppSettingInterface { /** * Get list post * * @param array $params * @return array|mixed * @throws ValidationErrorException */ function findAll($params = []) { /* ... */ $aItems = $this->browse()->getRows(); if ($aItems) { $this->processRows($aItems); } return $this->success($aItems); } /** * @param $params * @return array|bool */ function findOne($params) { $id = $this->resolver->resolveId($params); /* ... */ return $this->success($resource->loadFeedParam()->toArray()); } public function delete($params) { $id = $this->resolver->resolveId($params); $result = Phpfox::getService('post.process')->delete($id); if ($result !== false) { return $this->success([ 'id' => $id ]); } return $this->error('Cannot delete post'); } /** * Get Create/Update document form * @param array $params * @return mixed * @throws \Exception */ public function form($params = []) { /** @var PostForm $form */ $form = $this->createForm(PostForm::class, [ 'title' => 'adding_a_new_post', 'method' => 'post', 'action' => UrlUtility::makeApiUrl('post') ]); /* ... */ return $this->success($form->getFormStructure()); } /** * Create a new Post API * @param array $params * @return array|bool|mixed */ public function create($params = []) { /* ... */ } /** * Update a post * * @param $params * @return mixed */ public function update($params) { /* ... */ } /** * @param $id * @param bool $returnResource * @return array|PostResource */ function loadResourceById($id, $returnResource = false) { $item = Phpfox::getService("post")->getPost($id); if (empty($item['post_id'])) { return null; } if ($returnResource) { return $this->processOne($item); } return $item; } /** * Update multiple document base on document query * * @param $params * @return mixed * @throws \Exception */ public function patchUpdate($params) { /* ... */ } /** * Get for display on activity feed * @param array $feed * @param array $item detail data from database * @return array */ public function getFeedDisplay($feed, $item) { /* ... */ } /** * Create custom access control layer */ public function createAccessControl() { $this->accessControl = new PostAccessControl($this->getSetting(), $this->getUser()); /* ... */ } /** * @param array $params * @return mixed */ function searchForm($params = []) { $this->denyAccessUnlessGranted(PostAccessControl::VIEW); /** @var PostSearchForm $form */ $form = $this->createForm(PostSearchForm::class, [ 'title' =>'search', 'method' => 'GET', 'action' => UrlUtility::makeApiUrl('post') ]); return $this->success($form->getFormStructure()); } public function getRouteMap() { /* ... */ } /** * @param $param * @return MobileApp */ public function getAppSetting($param) { /* ... */ } } |
Register services and resources
...
Code Block |
---|
<?php /* ... */ Phpfox::getLib('module') ->addAliasNames('post', 'Posts') ->addServiceNames([ // New API service register here 'mobile.post_api' => Service\PostApi::class, 'mobile.post_category_api' => Service\PostCategoryApi::class, // Other Services of the app 'post.category' => Service\Category\Category::class, 'post.category.process' => Service\Category\Process::class, 'post.api' => Service\Api::class, 'post' => Service\Posts::class, 'post.browse' => Service\Browse::class, 'post.cache.remove' => Service\Cache\Remove::class, 'post.callback' => Service\Callback::class, 'post.process' => Service\Process::class, 'post.permission' => Service\Permission::class, ]); /* ... */ |
Next step, you need to create hook "mobile_api_routing_registration.php" in the hooks folder.
mobile_api_routing_registration.php
...
Code Block |
---|
<?php class PostApi extends AbstractResourceApi implements ActivityFeedInterface, MobileAppSettingInterface { /** This method allow you add custom route for APIs Return an array with key is routing rule and mapping condition In this case, 'mobile/post/search-form' map to `searchForm` method */ public function __naming() { return [ 'post/search-form'=> [ 'get'=>'searchForm' ] ]; } /** @param array $params @return mixed */ function searchForm($params = []) { $this->denyAccessUnlessGranted(PostAccessControl::VIEW); /** @var PostSearchForm $form */ $form = $this->createForm(PostSearchForm::class, [ 'title' =>'search', 'method' => 'GET', 'action' => UrlUtility::makeApiUrl('post') ]); return $this->success($form->getFormStructure()); } } |
You can download the full sample code of Post API service.
...
Postman is one of a good application for testing APIs.
Here are the API patterns to access Posts app
Pattern | description | ex. response | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Get List of Posts | {
{
},
},
{
} ],
},
} ] }GET url
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Get Post Detail | Expand source {
},
},
{
} ],
},
},
} },
} } | GET
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
| Get create post form | {
|
| {
|
|
|
|
| {
| {
|
| {
| {
|
|
| true,
|
|
|
|
| {
|
|
| true,
|
|
|
|
| {
|
|
|
|
| null,
| null,
|
| {
|
| {
| {
|
|
| true,
|
|
|
| [
| 1,
|
| 2,
|
| 3,
|
|
| {
|
|
|
|
|
|
| {
|
|
|
|
|
| null,
|
| {
|
| {
| {
|
|
| 0,
|
| [
|
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Delete a Post |