Restful Mobile API design
Restful Mobile API design & Implementation
On this section, we will take overall about Restful Mobile API design concepts, how to use and extends it.
Requirement
Phpfox mobile API requires to install two plugins:
- Install app "core-restful-api"
- Install app "core-mobile-api"
Restful API design concepts & convention
The fundamental concept in any RESTful API is the resource. A resource is an object with a type, associated data, relationships to other resources, and a set of methods that operate on it.
Restful API naming convention is following resource based way and supports standard HTTP GET, POST, PUT and DELETE methods.
Resources
For example, Blog App we have defined 2 resources:
- "blog" resource refers to a Blog entry
- "blog-category" 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 one-to-many or many-to-many relationship. The Class diagram below shows the relationship of resources in the Blog App
API for accessing Blog resource
Each resource having following standard API. This example of Blog resource
- 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 resource
Resource response in JSON
A JSON presentation of a resource would look like the example below.
Blog Resource Response Example Expand source
{
"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
In the new Core Mobile API app, we define ResourceBase class for controlling resource output, mapping data fields and generate routing for resource API
Every new resource should extend from the base class and use the list of core resources for the relationship when building APIs. It helps to reduce the code to build the API
Reusable core resources and common use cases
All resource class is defined in the folder "PF.Site/Apps/core-mobile-api/Api/Resource" under name Space "\Apps\Core_MobileApi\Api\Resource"
Core's Resource |
Description |
Use Case |
UserResource |
Presentation of a Phpfox's User |
|
TagResource |
Tag feature in Phpfox |
|
NotificationResource |
Core notification feature |
|
LikeResource |
Core like feature |
|
CommentResource |
Core comment feature |
|
FriendResource |
|
|
FileResource |
|
|
FeedResource |
|
|
AttachmentResource |
|
|
Reusable Core Objects
Unlike resource, Core Object is used to group related property of the Resource into an object. Following are a list of reusable objects
Object Class |
Use Case |
Image |
Presentation as an Image of an Item (blog, marketplace listing) |
Privacy |
Privacy of an item |
FeedParam |
Feed parameter of an Item |
Statistic |
Total Like, Total Comment, Total View of an Item |
Mapping property, related resource and object
To create a resource object, we fetch data from the database as an array then populate the Resource. Code example of creating a Resource from PHP array
<?php
// Fetch blog data from database then create blog resource
$blog = \Phpfox::getService("blog")->get($blogId);
$blogResource = new BlogResource($blog)
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 can override or manual mapping use Setter and Getter methods. Setter method uses to control input data, Getter control the output.
All field query from the database 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:
<?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
Following section, we base on an app named Posts clone from the Core Blog app to write example code.
The features of Posts app are the same as the Blog app, just change routing from "blog" to "post". Features included:
- Posts listing, search, my posts, friend's post, browse by category
- Create or Edit a Post
- Delete a Post
- Categories
- Tags to a Post
Posts app information:
- App's ID: "Posts"
- App's Dir: "PF.Site/Apps/Posts"
- App's Alias: "post"
- App's Routing: "post/*"
- App's Namespace: "\Apps\Posts"
Step by step to extend APIs:
- Create a resource class for each resource of the App
- Create a Service to handle API request for each resource
- Hook services to register new APIs
Structure overview
The picture above shows the basic structure to extend your app to support Core Mobile API. Take a look at highlighted items and let go to the detail of each section
Section |
Mission |
Api/Resource |
This section defines all the resources in your app. Each resource has fields and method to control the application login and API response result. |
Api/Form |
Create form extend from Mobile API app to help generate form structure that can understand by Mobile Application |
Api/Security |
The convenient way to control permission to access APIs |
Service |
Handle your API request and return the response |
hooks |
Register your APIs or Extent core API |
Restful API routing & service handler
Each API routing contains information on how to map HTTP request with a service method handler
API Url pattern: url
/restful_api/mobile/route_path
?access_token=token
url
: your PHPFOX websitetoken
: access token generate by AUTH API use to access APIroute_path
: Define by your new application
We can define new restful API routing in several ways. Because restful API is closely related to a resource, it should be generated from resource class or manual definition in case your app has more APIs to implement.
By extends ResourceBase class, PostResource able to generate resource API routes and links to PostApi service implementation as service handler automatically.
Manual routes can be add via PostApi:__naming() method. See example:
PostApi.php
<?php
class PostApi extends AbstractResourceApi implements ActivityFeedInterface, MobileAppSettingInterface
{
/.../
public function __naming()
{
return [
/* API route `/restful_api/mobile/post/search-form` will map with PostApi::searchForm() method */
'post/search-form'=>[
'get'=>'searchForm'
],
];
}
}
The final step to help system understands new routes by implement hook "mobile_api_routing_registration.php" (example implement show in the section below).
Following table show detail Resource API mapping were generated from PostResource and PostApi implementation
Route path pattern |
Request Method |
Map Method |
/post |
GET |
PostApi::findAll() |
/post/:id |
GET |
PostApi::findOne() |
/post/form |
GET |
PostApi::form() |
/post/form/:id |
GET |
PostApi::form() |
/post |
POST |
PostApi::create() |
/post/:id |
PUT |
PostAPI::update() |
/post/:id |
DELETE |
PostApi::delete() |
/post/feature/:id |
PUT |
PostApi::feature() |
/post/approve/:id |
PUT |
PostApi::approve() |
/post/sponsor/:id |
PUT |
PostApi::sponsor() |
Define your resources
Post app has two resources post and post-category, User can be only able to create post resource
Now let create your post resource. Create new class PostResource extend from ResourceBase.
The resource's properties are auto map base on same name rule
PostResource.php Expand source
<?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
<?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
<?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
<?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 example above - The class extent AbstractResourceApi to reuse resource base feature
- 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 bellow:
PostApi.php Expand source
<?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
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 APIsTesting Your APIs
Postman is one of a good application for testing and review APIs.
The api pattern to access Post App apiPattern
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
- Or get Edit form response
GETurl
/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