RESTful Plugin
The RESTful Plugin provides a structured way to build RESTful APIs. It handles request processing, parameter validation, and response formatting while following RESTful principles. The plugin is a System plugin.
Features
- Automatic request parameter validation
- Support for all HTTP methods (GET, POST, PUT, DELETE, etc.)
- Standardized error handling
- Easy integration with authentication systems
- Swagger documentation support
Installation
-
You can install a project use Festi CLI or install manually.
-
Configure your API entry point in
index.php
:
Additionally, install Swagger (optional, for API documentation):$systemPlugin = $core->getPluginInstance('RESTful'); $core->setSystemPlugin($systemPlugin); $options = array( 'area' => 'api', ); $isFoundUrl = $systemPlugin->bindRequest($options);
composer global require zircote/swagger-php
Creating API Endpoints
API endpoints are created by defining methods in API plugin classes. Each API plugin should follow these naming conventions:
- Plugin class name:
YourNameApiPlugin
- Object class name:
YourNameObject
Method Naming
The RESTful plugin uses HTTP method names in the method signatures:
onGet
- Handles GET requestsonPost
- Handles POST requestsonPut
- Handles PUT requestsonDelete
- Handles DELETE requests
You can also combine these with resource names:
onUserGet
- Handles GET for a user resourceonOrdersPost
- Handles POST for orders resource
Request Parameters
Request parameters are defined using annotations:
/**
* @requestParam name required
* @requestParam email
* @requestArray filters
*/
public function onPost(Response &$response, Request $request)
{
// Access parameters
$name = $request->name;
$email = $request->email;
$filters = $request->filters;
}
Annotations:
- @requestParam
- Defines a simple parameter (string, number, etc.)
- @requestArray
- Defines an array parameter
- Add required
after the parameter name to make it mandatory
Request and Response Objects
API methods receive two primary objects:
Response
- For returning data to the clientRequest
- For accessing request parameters
public function onGet(Response &$response, Request $request, $id = null)
{
// Set response data
$response->data = [...];
$response->status = true;
// Access request parameters
$orderBy = $request->OrderBy;
// Access JSON body
$jsonString = $request->getJson();
}
Route Parameters
Route parameters are passed as additional method parameters:
/**
* @urlRule ~^/companies/([0-9]+)/$~
*/
public function onGet(Response &$response, Request $request, $companyId = null)
{
// $companyId contains the route parameter value
}
Example: Basic CRUD API
Here's a simple example of a CRUD API for a "Companies" resource:
<?php
/**
* Class CompaniesApiPlugin
*
* @property-read CompaniesObject $object
*/
class CompaniesApiPlugin extends ObjectPlugin
{
/**
* Get company data.
*
* @requestParam OrderBy
* @requestParam Page
* @requestParam PerPage
* @requestArray Filtering
*
* @param Response $response
* @param Request $request
* @param int|null $idCompany
* @return bool
* @throws NotFoundException
*/
public function onGet(Response &$response, Request $request, int $idCompany = null): bool
{
if (!$idCompany) {
// Get list of companies
$search = array();
$orderBy = $request->OrderBy ? array('Entity.'.$request->OrderBy) : array('Entity.AddedDate DESC');
$page = $request->Page ?: 1;
$count = $request->PerPage ?: 20;
if ($request->Filtering) {
foreach ($request->Filtering as $fieldName => $values) {
$fieldName = $this->object->quoteColumnName($fieldName);
$search[$fieldName.'&LIKE'] = $values;
}
}
$result = $this->object->searchSplit($search, $orderBy, $count, $page);
$response->addParam($result);
} else {
// Get single company
$data = $this->object->get($idCompany);
if (!$data) {
throw new NotFoundException();
}
$response->data = $data;
$response->id = $idCompany;
$response->status = true;
}
return true;
}
/**
* Create or update company.
*
* @requestParam Name required
* @requestParam Code
* @requestParam CountryId
* @requestParam Address
* @requestParam Phone
*
* @param Response $response
* @param Request $request
* @param int|null $idCompany
* @return bool
*/
public function onPost(Response &$response, Request $request, int $idCompany = null)
{
if ($idCompany) {
// Update existing company
$values = array(
'Name' => $request->Name,
'ModifiedDate' => date('Y-m-d H:i:s'),
'Code' => $request->Code,
'CountryId' => $request->CountryId,
'Address' => $request->Address,
'Phone' => $request->Phone
);
$this->object->change($values, $idCompany);
} else {
// Create new company
$values = array(
'Name' => $request->Name,
'AddedDate' => date('Y-m-d H:i:s'),
'IsActive' => 1,
'Code' => $request->Code,
'CountryId' => $request->CountryId,
'Address' => $request->Address,
'Phone' => $request->Phone
);
$idCompany = $this->object->add($values);
}
$response->id = $idCompany;
$response->status = true;
return true;
}
}
Authentication
Implementing authentication with the RESTful plugin typically involves checking for authorization headers:
// index.php
$systemPlugin = $core->getPluginInstance('RESTful');
$core->setSystemPlugin($systemPlugin);
$options = array(
'area' => 'api',
ApiPlugin::OPTION_ON_BIND => function () {
$headers = apache_request_headers();
if (!array_key_exists('Authorization', $headers) || !$headers['Authorization']) {
throw new PermissionsException();
}
$token = str_replace("Bearer ", "", $headers['Authorization']);
$usersPlugin = Core::getInstance()->getPluginInstance('Users');
$data = $usersPlugin->getDataByAccessToken($token);
if (!$data) {
throw new PermissionsException();
}
$loginData = array(
'auth' => true,
'auth_id' => $data['id'],
'auth_login' => $data['username'],
DefaultUser::OPTION_AUTH_ROLE => USER_TYPE_API
);
Core::getInstance()->user->doLogin($loginData);
}
);
$isFoundUrl = $systemPlugin->bindRequest($options);
Error Handling
The RESTful plugin automatically handles exceptions and returns them as standardized error responses:
PermissionsException
- Returns 401 UnauthorizedNotFoundException
- Returns 404 Not FoundSystemException
- Returns 500 Internal Server Error with detailsApiException
- Returns the error code specified in the exception
Example:
if (!$data) {
throw new NotFoundException();
}
if (!$user->hasAccess($data)) {
throw new PermissionsException("You don't have access to this resource");
}
if (!$this->object->isValid($values)) {
throw new ApiException("Invalid data", ApiException::ERROR_CODE_BAD_REQUEST);
}
Swagger Documentation
The RESTful plugin supports Swagger for API documentation. To generate Swagger documentation:
-
Install swagger-php:
composer global require zircote/swagger-php
-
Add Swagger annotations to your API plugins
- Generate documentation using the swagger-php command
For more details, refer to the Swagger-PHP documentation.
Advanced Examples
File Upload API
/**
* @requestParam name required
*/
public function onFilePost(Response &$response, Request $request)
{
if (!isset($_FILES['file'])) {
throw new ApiException("Missing file", ApiException::ERROR_CODE_BAD_REQUEST);
}
$file = $_FILES['file'];
// Process the file...
$response->id = $fileId;
$response->status = true;
return true;
}
API with Pagination and Filtering
/**
* @requestParam page
* @requestParam limit
* @requestArray filters
*/
public function onProductsGet(Response &$response, Request $request)
{
$page = $request->page ?: 1;
$limit = $request->limit ?: 20;
$offset = ($page - 1) * $limit;
$search = array();
if ($request->filters) {
foreach ($request->filters as $key => $value) {
$search[$key] = $value;
}
}
$products = $this->object->search(
$search,
array('id DESC'),
$limit,
$offset
);
$count = $this->object->count($search);
$response->data = $products;
$response->meta = array(
'total' => $count,
'page' => $page,
'pages' => ceil($count / $limit)
);
return true;
}
Common Patterns and Best Practices
-
Consistent Response Format: Use a consistent structure for responses
$response->data = $userData; $response->status = true; $response->meta = ['timestamp' => time()];
-
Proper HTTP Status Codes: Use appropriate HTTP status codes
throw new ApiException("Resource not found", 404);
-
Resource Nesting: Use proper resource nesting for related data
// GET /users/123/orders public function onUserOrdersGet(Response &$response, Request $request, $idUser)
-
Proper Parameter Validation: Always validate input parameters
- Rate Limiting: Implement rate limiting for public APIs
- Versioning: Consider API versioning for long-term stability
- Documentation: Document all endpoints thoroughly
Examples
class DownloadApiPlugin extends ObjectPlugin
{
/**
* @urlRule ~^/download/update/([0-9]+)/$~
* @requestParam license
* @requestParam code required
*/
public function onUpdateGet(
Response &$response, Request $request, $idPlugin = false
)
{
echo $request->license;
} // end onUpdateGet
}
Create new company
POST: /companies/
/**
* Class CompanyApiPlugin
*
* @property-read CompaniesObject $object
*/
class CompanyApiPlugin extends ObjectPlugin
{
/**
* @requestParam name required
* @requestParam code
* @param Response $response
* @param Request $request
* @return bool
*/
public function onPost(Response &$response, Request $request)
{
$values = array(
'name' => $request->name,
'cdate' => date('Y-m-d H:i:s'),
'is_active' => true,
'code' => $request->code
);
$response->id = $this->object->add($values);
return true;
} // end onPost
}
Companies API detail example
<?php
/**
* Class CompanyApiPlugin
*
* @property-read CompaniesObject $object
*/
class CompaniesApiPlugin extends ObjectPlugin
{
/**
* Get company data.
*
* @requestParam OrderBy
* @requestParam Page
* @requestParam PerPage
* @requestArray Filtering
*
* @param Response $response
* @param Request $request
* @param int|null $idCompany
* @return bool
* @throws NotFoundException
*/
public function onGet(Response &$response, Request $request, int $idCompany = null): bool
{
if (!$idCompany) {
$this->_getList($response, $request);
} else {
$this->_get($response, $request, $idCompany);
}
return true;
} // end onGet
private function _get(Response &$response, Request $request, int $idCompany): void
{
$data = $this->object->get($idCompany);
if (!$data) {
throw new NotFoundException();
}
$this->_verifyUserPermission($idCompany);
$data['Users'] = $this->object->getUsers($idCompany);
$response->data = $data;
$response->id = $idCompany;
$response->status = true;
} // end _get
private function _getList(Response &$response, Request $request): void
{
$search = array();
$orderBy = null;
if ($request->OrderBy) {
$orderBy = array('Entity.'.$request->OrderBy);
} else {
$orderBy = array('Entity.AddedDate DESC');
}
if ($request->Filtering) {
foreach ($request->Filtering as $fieldName => $values) {
$fieldName = $this->object->quoteColumnName($fieldName);
$search[$fieldName.'&LIKE'] = $values;
}
}
$page = $request->Page ? $request->Page : 1;
$count = $request->PerPage ? $request->PerPage : DEFAULT_SEARCH_RESULTS_COUNT;
$result = $this->object->searchSplit($search, $orderBy, $count, $page);
$response->addParam($result);
} // end _getList
/**
* Create and update company.
*
* @requestParam Name required
* @requestParam Code
* @requestParam CountryId
* @requestParam Address
* @requestParam Phone
* @requestParam RegistrationNumber
* @requestParam Description
*
* @param Response $response
* @param Request $request
* @param int|null $idCompany
* @return bool
* @throws NotFoundException
*/
public function onPost(Response &$response, Request $request, int $idCompany = null)
{
if ($idCompany) {
$res = $this->_update($request, $idCompany);
if (!$res) {
throw new NotFoundException();
}
} else {
$idCompany = $this->_add($request);
}
$response->id = $idCompany;
$response->status = true;
return true;
} // end onPost
private function _add(Request $request): int
{
$values = array(
'Name' => $request->Name,
'AddedDate' => date('Y-m-d H:i:s'),
'ModifiedDate' => date('Y-m-d H:i:s'),
'IsActive' => 1,
'Code' => $request->Code,
'CountryId' => $request->CountryId,
'Address' => $request->Address,
'Phone' => $request->Phone,
'RegistrationNumber' => $request->RegistrationNumber,
'Description' => $request->Description
);
$this->object->begin();
$idCompany = $this->object->add($values);
$relationValues = array(
'CompanyId' => $idCompany,
'UserId' => App::getUserID(),
'UserTypeId' => USER_TYPE_ADMIN
);
$this->object->addUser($relationValues);
$this->object->commit();
return $idCompany;
} // end _add
private function _update(Request $request, int $idCompany): bool
{
$this->_verifyUserPermission($idCompany);
$values = array(
'Name' => $request->Name,
'ModifiedDate' => date('Y-m-d H:i:s'),
'Code' => $request->Code,
'CountryId' => $request->CountryId,
'Address' => $request->Address,
'Phone' => $request->Phone,
'RegistrationNumber' => $request->RegistrationNumber,
'Description' => $request->Description
);
$updatedRows = $this->object->change($values, $idCompany);
return $updatedRows > 0;
} // end _update
}