Page tree

Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Restful Mobile API design

...

Restful Mobile API design & Implementation

On this section, we will take overall about Restful Mobile go through RESTful API design concepts, how to use and extends itextend.

Note

Requirement

Phpfox mobile API requires to install two plugins:

  • Install app "core-restful-api"
  • Install app "core-mobile-api"

 

...

2 following phpFox apps are required:

  • RESTful API app (version 4.2.2 or later)
  • Mobile API app (version 4.2.0 or later)

RESTful API design concepts & convention

The fundamental concept in any RESTful API is the resource Resource. A resource Resource is an object with a type, associated data, relationships to other resourcesResource objects, and a set of methods that operate on it.
Restful API RESTful API naming convention is following follows resource-based way and supports standard HTTP GET, POST, PUT and DELETE methods.

In this article, we will use the Blog app as an example.

Resources

For example, Blog App app we have defined 2 resources:

  • "blog" resource refers to a Blog entry
  • "blog-category" resource refers  resource refers to Blog Category entry
  • "blog" resource has a many-to-many relationship with "blog-category" resource

Resource Relationship

A resource may have a relationship with other resources using property. If Resource can have either one-to-many or many-to-many relationshiprelationships with other Resources using property. The Class diagram below shows the relationship of resources in the Blog App app
Image Modified

API

...

to access Blog resource

Each resource having has the following standard API. This example of Blog resourceAPIs:

  • Listing & search resource
    • Route: "/blog" 
    • Method: "GET"
  • Create new resource
    • Route: "/blog" 
    • Method: "POST"
    • Request data in JSON format
  • Update resource
    • Route: "/blog/:id"
    • Method: "PUT"
    • Request data in JSON format
  • Get Form structure for creating/updating
    • Route: "/blog/form/" + 
    • Method: "GET"
    • Parameters: use "id" param for edit case
  • Delete resource
    • Route: "/blog/:id"
    • Method: "DELETE"
  • Make as featured
    • Route: "/blog/feature/:id
    • Method: "PUT"
  • Make as sponsor
    • Route: "/blog/sponsor/:id
    • Method: "PUT"

The similar above rule applied for Blog Category resource and another resourceother resources also have similar APIs

Resource response in JSON

A JSON presentation of a resource would look looks like the an example below.
Blog Resource Response Example  Expand source

Code Block
{

...


	"status": "success",

...


	"data": {

...


		"resource_name": "blog",

...


		"module_name": "blog",

...


		"title": "Et a dolor eum libero nostrum cumque.",

...


		"description": "Esse beatae voluptas officiis...",

...


		"module_id": "blog",

...


		"item_id": 0,

...


		"is_approved": true,

...


		"is_sponsor": false,

...


		"is_featured": false,

...


		"is_liked": false,

...


		"is_friend": false,

...


		"is_pending": false,

...


		"post_status": 1,

...


		"text": "Esse beatae voluptas officiis ratione ",

...


		"image": null,

...


		"statistic": {

...


			"total_like": 0,

...


			"total_comment": 0,

...


			"total_view": 1,

...


			"total_attachment": 0

...


		},

...


		"privacy": 0,

...


		"user": {

...


			"full_name": "Sheridan Hahn",

...


			"avatar": null,

...


			"id": 841

...


		},

...


		"categories": [

...


			{
				"id": 4,

...


				"name": "Family & Home",

...


				"subs": null

...


			},

...


			{
				"id": 9,

...


				"name": "Sports",

...


				"subs": null

...


			}
		],
		"tags": [

...


			{
				"tag_text": "tag me",

...


				"id": 3

...


			}
		],
		"attachments": [],

...


		"id": 1,

...


		"creation_date": "2016-06-21T04:53:48+00:00",

...


		"modification_date": null,

...


		"link": "http://localhost:7788/blog/1/et-a-dolor-eum-libero-nostrum-cumque/",

...


		"extra": {

...


			"can_view": true,

...


			"can_like": true,

...


			"can_share": true,

...


			"can_delete": true,

...


			"can_report": true,

...


			"can_add": true,

...


			"can_edit": true,

...


			"can_comment": true,

...


			"can_publish": false,

...


			"can_feature": true,

...


			"can_approve": false,

...


			"can_sponsor": true,

...


			"can_sponsor_in_feed": false,

...


			"can_purchase_sponsor": true

...


		},

...


		"self": null,

...


		"links": {

...


			"likes": {

...


				"ref": "mobile/like?item_type=blog&item_id=1"

...


			},

...


			"comments": {

...


				"ref": "mobile/comment?item_type=blog&item_id=1"

...


			}
		},
		"feed_param": {

...


			"item_id": 1,

...


			"comment_type_id": "blog",

...


			"total_comment": 0,

...


			"like_type_id": "blog",

...


			"total_like": 0,

...


			"feed_title": "Et a dolor eum libero nostrum cumque.",

...


			"feed_link": "http://localhost:7788/blog/1/et-a-dolor-eum-libero-nostrum-cumque/",

...


			"feed_is_liked": false,

...


			"feed_is_friend": false,

...


			"report_module": "blog"

...


		}
	},

...


	"error": null

...


}



Control Resource API response

...

  • 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

    After completed create required class and implementation, you need to register your service and resources
    Open start.php in your app and add the following row to register service.
    start.php
    <?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
    <?php
    /**
  • Define RestAPI services. Note the name must be same as definition in start.php file
    */
    $this->apiNames['mobile.post_api'] = \Apps\Posts\Service\PostApi::class;
    $this->apiNames['mobile.post_category_api'] = \Apps\Posts\Service\PostCategoryApi::class;
    /**
  • Register Resource Name, This help auto generate routing for the resource
  • Note: resource name must be mapped correctly to resource api
    */
    $this->resourceNames[\Apps\Posts\Api\Resource\PostResource::RESOURCE_NAME] = 'mobile.post_api';
    $this->resourceNames[\Apps\Posts\Api\Resource\PostCategoryResource::RESOURCE_NAME] = 'mobile.post_category_api';
     

    Extends routing to support more APIs

    After complete register API step, your Phpfox site now extend more restful APIs follow naming convention rules as mentions above
    If you would like to build more APIs that Resource Naming Convention rules has not supports. In the API service, there a magic function called "__naming()" able to do that
    PostApi.php
    <?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());
    }
    }
     
    The Full Code of Post API service
    PostApi.php  Expand source
    <?php

    namespace Apps\Posts\Service;

    use Apps\Core_MobileApi\Adapter\MobileApp\MobileApp;
    use Apps\Core_MobileApi\Adapter\MobileApp\MobileAppSettingInterface;
    use Apps\Core_MobileApi\Adapter\MobileApp\Screen;
    use Apps\Core_MobileApi\Adapter\Utility\UrlUtility;
    use Apps\Core_MobileApi\Api\AbstractResourceApi;
    use Apps\Core_MobileApi\Api\ActivityFeedInterface;
    use Apps\Core_MobileApi\Api\Exception\ValidationErrorException;
    use Apps\Core_MobileApi\Api\Form\Type\FileType;
    use Apps\Core_MobileApi\Api\Resource\AttachmentResource;
    use Apps\Core_MobileApi\Api\Resource\Object\HyperLink;
    use Apps\Core_MobileApi\Api\Resource\TagResource;
    use Apps\Core_MobileApi\Api\Security\AppContextFactory;
    use Apps\Core_MobileApi\Service\Helper\Pagination;
    use Apps\Core_MobileApi\Service\NameResource;
    use Apps\Core_MobileApi\Service\TagApi;
    use Apps\Posts\Api\Form\PostSearchForm;
    use Apps\Posts\Api\Form\PostForm;
    use Apps\Posts\Api\Resource\PostCategoryResource;
    use Apps\Posts\Api\Resource\PostResource;
    use Apps\Posts\Api\Security\PostAccessControl;
    use Phpfox;
    class PostApi extends AbstractResourceApi implements ActivityFeedInterface, MobileAppSettingInterface
    {
    const ERROR_POST_NOT_FOUND = "Post not found";
    /**
  • @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');
    }
    public function __naming()
    {
    return [
    'post/search-form'=>[
    'get'=>'searchForm'
    ],
    ];
    }
    /**
  • Get list post
    *
  • @param array $params
  • @return array|mixed
  • @throws ValidationErrorException
    */
    function findAll($params = [])
    {
    // Resolve and validate parameter from the requests
    $params = $this->resolver
    ->setDefined([
    'view', 'module_id', 'item_id', 'category', 'q', 'sort', 'when', 'profile_id', 'limit', 'page', 'tag'
    ])
    ->setDefault([
    'limit' => Pagination::DEFAULT_ITEM_PER_PAGE,
    'page' => 1
    ])
    ->setAllowedValues("sort", ['latest', 'most_viewed','most_liked','most_discussed'])
    ->setAllowedValues('view', ['my', 'spam', 'pending', 'draft', 'friend'])
    ->setAllowedValues('when', ['all-time', 'today', 'this-week', 'this-month'])
    ->setAllowedTypes('limit', 'int', [
    'min' => Pagination::DEFAULT_MIN_ITEM_PER_PAGE,
    'max' => Pagination::DEFAULT_MAX_ITEM_PER_PAGE
    ])
    ->setAllowedTypes('page', 'int', ['min' => 1])
    ->setAllowedTypes('profile_id','int', ['min' => 1])
    ->setAllowedTypes('item_id', 'int', ['min' => 1])
    ->setAllowedTypes('category', 'int', ['min' => 1])
    ->resolve($params)
    ->getParameters();
    if (!$this->resolver->isValid()) {
    return $this->validationParamsError($this->resolver->getInvalidParameters());
    }
    // Security checking
    $this->denyAccessUnlessGranted(PostAccessControl::VIEW, null, [
    'view' => $params['view']
    ]);

    $sort = $params['sort'];
    $view = $params['view'];
    $parentModule = null;
    if (!empty($params['module_id']) && !empty($params['item_id'])) {
    $parentModule = [
    'module_id' => $params['module_id'],
    'item_id' => $params['item_id'],
    ];
    }
    $user = null;
    $isProfile = $params['profile_id'];
    if ($isProfile) {
    $user = $this->userService->get($isProfile);
    if (empty($user)) {
    return $this->notFoundError("User profile not found");
    }
    $this->search()->setCondition('AND post.user_id = ' . $user['user_id']);
    }
    $browseParams = [
    'module_id' => 'post',
    'alias' => 'post',
    'field' => 'post_id',
    'table' => Phpfox::getT('post'),
    'hide_view' => ['pending', 'my'],
    'service' => 'post.browse',
    ];
    switch ($view) {
    case 'spam':
    $this->search()->setCondition('AND post.is_approved = 9');
    break;
    case 'pending':
    $this->search()->setCondition('AND post.is_approved = 0');
    break;
    case 'my':
    $this->search()>setCondition('AND post.user_id = ' . $this>getUser()->getId());
    break;
    case 'draft':
    $this->search()>setCondition("AND post.user_id = " . (int)$this>getUser()>getId() . " AND post.is_approved IN(" . ($user['user_id'] == $this>getUser()->getId() ? '0,1' : '1')
    . ") AND post.privacy IN(" . (Phpfox::getParam('core.section_privacy_item_browsing') ? '%PRIVACY%' : Phpfox::getService('core')->getForBrowse($user))
    . ") AND post.post_status = 2");
    break;
    default:
    $this->search()->setCondition("AND post.is_approved = 1 AND post.post_status = 1" . (Phpfox::getUserParam('privacy.can_comment_on_all_items') ? ""
    : " AND post.privacy IN(%PRIVACY%)"));
    break;
    }

    if (!empty($params['category'])) {
    if ($aPostCategory = $this->categoryService->getCategory($params['category'])) {
    $this->search()->setCondition('AND post_category.category_id = ' . (int)$params['category'] . ' AND post_category.user_id = ' . ($isProfile ? (int)$user['user_id'] : 0));
    }
    }
    if (isset($parentModule) && isset($parentModule['module_id'])) {
    $this->search()->setCondition('AND post.module_id = \'' . $parentModule['module_id'] . '\' AND post.item_id = ' . (int)$parentModule['item_id']);
    } else {
    if ($parentModule === null) {
    if (($view == 'pending' || $view == 'draft') && Phpfox::getUserParam('post.can_approve_posts')) {
    } else {
    $this->search()->setCondition('AND post.module_id = \'post\'');
    }
    }
    }
    // search query
    if (!empty($params['q'])) {
    $this->search()>setCondition('AND post.title LIKE "' . Phpfox::getLib('parse.input')>clean('%' . $params['q'] . '%') . '"');
    }
    // Search By tag
    if ($params['tag']) {
    if ($aTag = Phpfox::getService('tag')->getTagInfo('post', $params['tag'])) {
    $this->search()>setCondition('AND tag.tag_text = \'' . Phpfox::getLib('database')>escape($aTag['tag_text']) . '\'');
    } else {
    $this->search()->setCondition('AND 0');
    }
    }

    // sort
    switch ($sort) {
    case 'most_viewed':
    $sort = 'post.total_view DESC, post.time_stamp DESC';
    break;
    case 'most_liked':
    $sort = 'post.total_like DESC, post.time_stamp DESC';
    break;
    case 'most_discussed':
    $sort = 'post.total_comment DESC, post.time_stamp DESC';
    break;
    default:
    $sort = 'post.time_stamp DESC, post.time_stamp DESC';
    break;
    }
    // When
    if ($params['when']) {
    $iTimeDisplay = Phpfox::getLib('date')->mktime(0, 0, 0, Phpfox::getTime('m'), Phpfox::getTime('d'), Phpfox::getTime('Y'));
    switch ($params['when']) {
    case 'today':
    $iEndDay = Phpfox::getLib('date')->mktime(23, 59, 0, Phpfox::getTime('m'), Phpfox::getTime('d'), Phpfox::getTime('Y'));
    $this->search()>setCondition(' AND (post.time_stamp >= \'' . Phpfox::getLib('date')>convertToGmt($iTimeDisplay) . '\' AND post.time_stamp < \'' . Phpfox::getLib('date')->convertToGmt($iEndDay) . '\')');
    break;
    case 'this-week':
    $this->search()>setCondition(' AND post.time_stamp >= ' . (int) Phpfox::getLib('date')>convertToGmt(Phpfox::getLib('date')->getWeekStart()));
    $this->search()>setCondition(' AND post.time_stamp <= ' . (int) Phpfox::getLib('date')>convertToGmt(Phpfox::getLib('date')->getWeekEnd()));
    break;
    case 'this-month':
    $this->search()>setCondition(' AND post.time_stamp >= \'' . Phpfox::getLib('date')>convertToGmt(Phpfox::getLib('date')->getThisMonth()) . '\'');
    $iLastDayMonth = Phpfox::getLib('date')>mktime(0, 0, 0, date('n'), Phpfox::getLib('date')>lastDayOfMonth(date('n')), date('Y'));
    $this->search()>setCondition(' AND post.time_stamp <= \'' . Phpfox::getLib('date')>convertToGmt($iLastDayMonth) . '\'');
    break;
    default:
    break;
    }
    }
    $this->search()->setSort($sort)
    ->setLimit($params['limit'])
    ->setPage($params['page']);
    $this->browse()>params($browseParams)>execute();
    $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);
    $item = $this->postService->getPost($id);
    if ((!isset($item['post_id'])) || (isset($item['module_id']) && Phpfox::isModule($item['module_id']) != true)

    ($item['post_status'] == 2 && Phpfox::getUserId() != $item['user_id'])) {
    return $this->notFoundError();
    }
    if (!$this->getAccessControl()->isGrantedSetting('post.can_approve_posts')) {
    if ($item['is_approved'] != '1' && $item['user_id'] != Phpfox::getUserId()) {
    return $this->notFoundError();
    }
    }
    $resource = $this->processOne($item)
    ->lazyLoad(["user"]);
    $this->denyAccessUnlessGranted(PostAccessControl::VIEW, $resource);

    return $this->success($resource->loadFeedParam()->toArray());
    }
    /**

  • Process Detail response
  • @param $item
  • @return PostResource
    */
    public function processOne($item)
    {
    $resource = $this->processRow($item);
    $resource->categories = $this->getCategoryApi()
    >getByPostId($resource>id);
    $resource->tags = $this->getTagApi()
    >getTagsBy(PostResource::TAG_CATEGORY, $resource>id);
    if (isset($item['total_attachment']) && $item['total_attachment'] > 0) {
    $resource->attachments = $this->getAttachmentApi()
    >getAttachmentsBy($resource>getId(), 'post');
    }
    $this->setSelfHyperMediaLinks($resource);
    $this->setLinksHyperMediaLinks($resource);
    return $resource;
    }
    /**
  • Process list of post
  • @param $aRows
    */
    public function processRows(&$aRows)
    {
    /** @var TagApi $tagReducer */
    $tagReducer = $this->getTagApi();
    $tagCond = [
    'category_id' => 'post',
    'item_id' => []
    ];
    /** @var PostCategoryApi $categoryReducer */
    $categoryReducer = $this->getCategoryApi();
    $categoryCond = [
    'post_id' => []
    ];
    foreach ($aRows as $aRow) {
    $tagCond['item_id'][] = $aRow['post_id'];
    $categoryCond['post_id'][] = $aRow['post_id'];
    }
    $tagReducer->reduceFetchAll($tagCond);
    $categoryReducer->reduceFetchAll($categoryCond);
    foreach ($aRows as $key => $aRow) {
    $aRow['tags'] = $tagReducer->reduceQuery([
    'category_id' => PostResource::TAG_CATEGORY,
    'item_id' => $aRow['post_id']
    ]);
    $aRow['categories'] = $categoryReducer->reduceQuery([
    'post_id' => $aRow['post_id']
    ]);
    $aRows[$key] = $this->processRow($aRow)
    ->displayShortFields()
    ->toArray();
    }
    }
    /**
  • Process single row
  • @param array $item
  • @return PostResource|array
    */
    public function processRow($item)
    {
    $resource = $this->populateResource(PostResource::class, $item);
    $resource->setExtra($this->getAccessControl()->getPermissions($resource));
    // Add self Hyper Media Links
    $this->setSelfHyperMediaLinks($resource);

    return $resource;
    }

    public function delete($params)
    {
    $id = $this->resolver->resolveId($params);
    $post = $this->loadResourceById($id, true);
    if (!$post) {
    return $this->notFoundError();
    }
    $this->denyAccessUnlessGranted(PostAccessControl::DELETE, $post);

    $mResult = Phpfox::getService('post.process')->delete($id);
    if ($mResult !== 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 = [])
    {
    $this->denyAccessUnlessGranted(PostAccessControl::ADD);
    $editId = $this->resolver->resolveSingle($params, 'id');
    /** @var PostForm $form */
    $form = $this->createForm(PostForm::class, [
    'title' => 'adding_a_new_post',
    'method' => 'post',
    'action' => UrlUtility::makeApiUrl('post')
    ]);
    $form->setCategories($this->getCategories());
    if ($editId && ($post = $this->loadResourceById($editId, true))) {
    $this->denyAccessUnlessGranted(PostAccessControl::EDIT, $post);
    $form->setAction(UrlUtility::makeApiUrl('post/:id', $editId))
    ->setTitle('editing_post')
    ->setMethod('put');
    $form->assignValues($post);
    }
    return $this->success($form->getFormStructure());
    }
    /**
  • Create a new Post API
  • @param array $params
  • @return array|bool|mixed
    */
    public function create($params = [])
    {
    // Checking create post permission
    $this->denyAccessUnlessGranted(PostAccessControl::ADD);
    /** @var PostForm $form */
    $form = $this->createForm(PostForm::class);
    $form->setCategories($this->getCategories());
    if ($form->isValid()) {
    $id = $this->processCreate($form->getValues());
    if ($id) {
    return $this->success([
    'id' => $id,
    'resource_name'=> PostResource::populate([])->getResourceName(),
    ]);
    }
    else {
    return $this->error($this->getErrorMessage());
    }
    }
    else {
    return $this->validationParamsError($form->getInvalidFields());
    }
    }
    /**
  • Update a post
    *
  • @param $params
  • @return mixed
    */
    public function update($params)
    {
    $id = $this->resolver->resolveId($params);
    // Get post resource and checking for permission
    $post = $this->loadResourceById($id, true);
    if (empty($post)) {
    return $this->notFoundError();
    }
    $this->denyAccessUnlessGranted(PostAccessControl::EDIT, $post);
    /** @var PostForm $form */
    $form = $this->createForm(PostForm::class);
    $form->setCategories($this->getCategories());
    if ($form->isValid() && ($values = $form->getValues())) {
    $success = $this->processUpdate($id, $values, $post);
    if ($success) {
    return $this->success([
    'id' => $id,
    'resource_name'=> PostResource::populate([])->getResourceName(),
    ]);
    }
    else {
    return $this->error($this->getErrorMessage());
    }
    }
    else {
    return $this->validationParamsError($form->getInvalidFields());
    }
    }

    /**
  • @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)
    {
    // TODO: Implement updateAll() method.
    }


    /**
  • Get for display on activity feed
  • @param array $feed
  • @param array $item detail data from database
  • @return array
    */
    public function getFeedDisplay($feed, $item)
    {
    $categoryCond = [
    'post_id' => []
    ];
    /** @var PostCategoryApi $categoryReducer */
    $categoryReducer = $this->getCategoryApi();
    $categoryCond['post_id'][] = $item['post_id'];
    $categoryReducer->reduceFetchAll($categoryCond);
    $item['categories'] = $categoryReducer->reduceQuery([
    'post_id' => $item['post_id']
    ]);
    return $this->processRow($item)->toArray(['resource_name','id','title','categories','description','image']);
    }
    /**
  • @return PostCategoryApi
    */
    private function getCategoryApi()
    {
    return NameResource::instance()
    ->getApiServiceByResourceName(PostCategoryResource::RESOURCE_NAME);
    }
    /**
  • @return TagApi
    */
    public function getTagApi()
    {
    return NameResource::instance()
    ->getApiServiceByResourceName(TagResource::RESOURCE_NAME);
    }
    private function getCategories()
    {
    return Phpfox::getService('post.category')->getForBrowse();
    }
    /**
  • Create custom access control layer
    */
    public function createAccessControl()
    {
    $this->accessControl =
    new PostAccessControl($this->getSetting(), $this->getUser());
    $moduleId = $this->request()->get('module_id');
    $itemId = $this->request()->get("item_id");
    if ($moduleId && $itemId) {
    $context = AppContextFactory::create($moduleId, $itemId);
    if ($context === null) {
    return $this->notFoundError();
    }
    $this->accessControl->setAppContext($context);
    }
    }
    /**
  • Internal process adding post
  • @param $values
  • @return int
    */
    private function processCreate($values)
    {
    if (!empty($values['file']) && !empty($values['file']['temp_file'])) {
    $values['temp_file'] = $values['file']['temp_file'];
    }
    if (!empty($values['categories'])) {
    $values['selected_categories'] = $values['categories'];
    }
    if (!empty($values['attachment'])) {
    $values['attachment'] = implode(",", $values['attachment']);
    }
    if (!empty($values['tags'])) {
    $values['tag_list'] = $values['tags'];
    }
    if (!empty($values['draft'])) {
    $values['post_status'] = 2;
    }
    else {
    $values['post_status'] = 1;
    }
    $id = $this->processService->add($values);
    return $id;
    }
    /**
  • Internal process update a post
  • @param $id
  • @param $values
  • @param $item PostResource
  • @return bool
    */
    private function processUpdate($id, $values, $item)
    {
    if (!empty($values['file'])) {
    if ($values['file']['status'] == FileType::NEW_UPLOAD || $values['file']['status'] == FileType::CHANGE) {
    $values['temp_file'] = $values['file']['temp_file'];
    }
    elseif ($values['file']['status'] == FileType::REMOVE) {
    $values['remove_photo'] = 1;
    }
    }
    if (!empty($values['categories'])) {
    $values['selected_categories'] = $values['categories'];
    }
    if (!empty($values['attachment'])) {
    $values['attachment'] = implode(",", $values['attachment']);
    }
    if (!empty($values['tags'])) {
    $values['tag_list'] = $values['tags'];
    }
    if (!empty($values['draft'])) {
    $values['post_status'] = $item->post_status;
    }
    else {
    $values['post_status'] = 1;
    }
    $userId = $item->getAuthor()->getId();
    $post = $item->toArray();
    $this->processService->update($id, $userId, $values, $post);
    return true;
    }
    /**
  • @return AttachmentApi
    */
    private function getAttachmentApi()
    {
    return NameResource::instance()
    ->getApiServiceByResourceName(AttachmentResource::RESOURCE_NAME);
    }
    /**
  • @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());
    }
    /**
  • @param PostResource $resource
    */
    private function setSelfHyperMediaLinks($resource)
    {
    $resource->setSelf([
    PostAccessControl::VIEW => $this->createHyperMediaLink(PostAccessControl::VIEW,
    $resource,
    HyperLink::GET, 'post/:id',
    ['id' => $resource->getId()]),
    PostAccessControl::EDIT => $this->createHyperMediaLink(PostAccessControl::EDIT,
    $resource,
    HyperLink::GET, 'post/form/:id',
    ['id' => $resource->getId()]),
    PostAccessControl::DELETE => $this->createHyperMediaLink(PostAccessControl::DELETE,
    $resource, HyperLink::DELETE,
    'post/:id',
    ['id' => $resource->getId()])
    ]);
    }
    /**
  • @param PostResource $resource
    */
    private function setLinksHyperMediaLinks($resource)
    {
    $resource->setLinks([
    "likes" => $this->createHyperMediaLink(null,
    $resource,
    HyperLink::GET, 'like',
    ['item_type' => "post", 'item_id' => $resource->getId()]),
    "comments" => $this->createHyperMediaLink(null,
    $resource,
    HyperLink::GET, 'comment',
    ['item_type' => "post", 'item_id' => $resource->getId()])
    ]);
    }
    public function getRouteMap()
    {
    $resource = str_replace('-', '_', PostResource::RESOURCE_NAME);
    $module = 'post';
    return [
    [
    'path' => 'post/:id(/*)',
    'routeName' => ROUTE_MODULE_DETAIL,
    'defaults' => [
    'moduleName' => $module,
    'resourceName' => $resource,
    ]
    ],
    [
    'path' => 'post/category/:category(/*), post/tag/:tag',
    'routeName' => ROUTE_MODULE_LIST,
    'defaults' => [
    'moduleName' => $module,
    'resourceName' => $resource,
    ]
    ],
    [
    'path' => 'post/add',
    'routeName' => ROUTE_MODULE_ADD,
    'defaults' => [
    'moduleName' => $module,
    'resourceName' => $resource,
    ]
    ],
    [
    'path' => 'post(/*)',
    'routeName' => ROUTE_MODULE_HOME,
    'defaults' => [
    'moduleName' => $module,
    'resourceName' => $resource,
    ]
    ]
    ];
    }
    /**
  • @param $param
  • @return MobileApp
    */
    public function getAppSetting($param)
    {
    $l = $this->getLocalization();
    $app = new MobileApp('post' ,[
    'title'=> $l->translate('Posts'),
    'home_view'=>'menu',
    'main_resource'=> new PostResource([]),
    'category_resource'=> new PostCategoryResource([]),
    ]);
    $headerButtons = [
    [
    'icon' => 'list-bullet-o',
    'action' => Screen::ACTION_FILTER_BY_CATEGORY,
    ],
    ];
    if ($this->getAccessControl()->isGranted(PostAccessControl::ADD)) {
    $headerButtons[] = [
    'icon' => 'plus',
    'action' => Screen::ACTION_ADD,
    'params' => ['resource_name'=> (new PostResource([]))->getResourceName()]
    ];
    }
    $app->addSetting('home.header_buttons', $headerButtons);
    return $app;
    }
    }
     
    Now you can start testing your APIs

    Testing Your APIs

     
    Postman is one of a good application for testing and review APIs. 
    The api pattern to access Post App api

    Pattern

    description

    ex. response

    GET url/restful_api/mobile/post?access_token=token

    Get List of Posts

     Expand source
    {
    "status": "success",
    "data": [
    {
    "resource_name": "post",
    "title": "Title of the post",
    "description": "Description of the post",
    "module_id": "post",
    "item_id": 0,
    "is_approved": true,
    "is_sponsor": false,
    "is_featured": false,
    "is_liked": null,
    "is_friend": null,
    "post_status": 1,
    "image": null,
    "statistic": {
    "total_like": 0,
    "total_comment": 0,
    "total_view": 1,
    "total_attachment": 0
    },
    "privacy": 0,
    "user": {
    "full_name": "Cute User",
    "avatar": null,
    "id": 1
    },
    "categories": [
    {
    "id": 6,
    "name": "Recreation"
    }
    ],
    "tags": [],
    "attachments": [],
    "id": 1,
    "creation_date": "2018-11-30T04:44:39+00:00",
    "modification_date": null,
    "link": "http://localhost:7788/post/1/love/",
    "extra": {
    "can_view": true,
    "can_like": true,
    "can_share": true,
    "can_delete": true,
    "can_report": false,
    "can_add": true,
    "can_edit": true,
    "can_comment": true,
    "can_publish": false,
    "can_feature": true,
    "can_approve": true,
    "can_sponsor": true,
    "can_sponsor_in_feed": false,
    "can_purchase_sponsor": true
    },
    "self": null,
    "links": null
    }
    ]
    }

    GET url/restful_api/mobile/post/1?access_token=token

    Get Post Detail

     Expand source
    {
    "status": "success",
    "data": {
    "resource_name": "post",
    "title": "Title of the post",
    "description": "Description of the post",
    "module_id": "post",
    "item_id": 0,
    "is_approved": true,
    "is_sponsor": false,
    "is_featured": false,
    "is_liked": false,
    "is_friend": false,
    "post_status": 1,
    "text": "xxxx",
    "image": null,
    "statistic": {
    "total_like": 0,
    "total_comment": 0,
    "total_view": 1,
    "total_attachment": 0
    },
    "privacy": 0,
    "user": {
    "full_name": "Cute User",
    "avatar": null,
    "id": 1
    },
    "categories": [
    {
    "id": 6,
    "name": "Recreation"
    }
    ],
    "tags": [],
    "attachments": [],
    "id": 1,
    "creation_date": "2018-11-30T04:44:39+00:00",
    "modification_date": null,
    "link": "http://localhost:7788/post/1/love/",
    "extra": {
    "can_view": true,
    "can_like": true,
    "can_share": true,
    "can_delete": true,
    "can_report": false,
    "can_add": true,
    "can_edit": true,
    "can_comment": true,
    "can_publish": false,
    "can_feature": true,
    "can_approve": true,
    "can_sponsor": true,
    "can_sponsor_in_feed": false,
    "can_purchase_sponsor": true
    },
    "self": null,
    "links": {
    "likes": {
    "ref": "mobile/like?item_type=post&item_id=1"
    },
    "comments": {
    "ref": "mobile/comment?item_type=post&item_id=1"
    }
    },
    "feed_param": {
    "item_id": 1,
    "comment_type_id": "post",
    "total_comment": 0,
    "like_type_id": "post",
    "total_like": 0,
    "feed_title": "Love",
    "feed_link": "http://localhost:7788/post/1/love/",
    "feed_is_liked": false,
    "feed_is_friend": false,
    "report_module": "post"
    }
    }
    }

    GET url/restful_api/mobile/post/form/?access_token=token
     

  1. Or get Edit form response
    GET url/restful_api/mobile/post/form/1?access_token=token|Get create post form| Expand source
    {
    "status": "success",
    "data": {
    "title": "Add a new post",
    "description": "",
    "action": "mobile/post",
    "method": "post",
    "sections": {
    "basic": {
    "label": "Basic Info",
    "fields": {
    "title": {
    "name": "title",
    "component_name": "Text",
    "required": true,
    "value": "",
    "returnKeyType": "next",
    "label": "Title",
    "placeholder": "Fill title for post"
    },
    "text": {
    "name": "text",
    "component_name": "TextArea",
    "required": true,
    "value": "",
    "returnKeyType": "default",
    "label": "Post",
    "placeholder": "Add content to post"
    },
    "attachment": {
    "name": "attachment",
    "component_name": "Attachment",
    "label": "attachment",
    "item_type": "post",
    "item_id": null,
    "current_attachments": null,
    "upload_endpoint": "mobile/attachment"
    }
    }
    },
    "additional_info": {
    "label": "Additional Info",
    "fields": {
    "categories": {
    "name": "categories",
    "component_name": "Choice",
    "multiple": true,
    "value": [],
    "label": "Categories",
    "value_type": "array",
    "options": [
    {
    "value": 1,
    "label": "Business"
    },
    {
    "value": 2,
    "label": "Education"
    },
    {
    "value": 3,
    "label": "Entertainment"
    }
    ],
    "suboptions": []
    },
    "tags": {
    "name": "tags",
    "component_name": "Tags",
    "value": "",
    "returnKeyType": "next",
    "label": "Topics",
    "description": "Separate multiple topics with commas."
    },
    "file": {
    "name": "file",
    "component_name": "File",
    "label": "Photo",
    "file_type": "photo",
    "item_type": "post",
    "preview_url": null,
    "upload_endpoint": "mobile/file"
    }
    }
    },
    "settings": {
    "label": "Settings",
    "fields": {
    "privacy": {
    "name": "privacy",
    "component_name": "Privacy",
    "value": 0,
    "returnKeyType": "next",
    "options": [
    {
    "label": "Everyone",
    "value": 0
    },
    {
    "label": "Friends",
    "value": 1
    },
    {
    "label": "Friends of Friends",
    "value": 2
    },
    {
    "label": "Only Me",
    "value": 3
    },
    {
    "label": "Custom",
    "value": 4
    }
    ],
    "label": "Privacy",
    "multiple": false,
    "description": "Control who can see this post"
    }
    }
    }
    },
    "fields": {
    "module_id": {
    "name": "module_id",
    "component_name": "Hidden",
    "value": "post"
    },
    "item_id": {
    "name": "item_id",
    "component_name": "Hidden"
    },
    "draft": {
    "name": "draft",
    "component_name": "Checkbox",
    "value": 0,
    "label": "Save as Draft"
    },
    "submit": {
    "name": "submit",
    "component_name": "Submit",
    "label": "Publish",
    "value": 1
    }
    }
    }
    }|

    DELETE url/restful_api/mobile/post/post_id?access_token=token

    Delete a Post