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

  1. You can install a project use Festi CLI or install manually.

  2. Configure your API entry point in index.php:

    $systemPlugin = $core->getPluginInstance('RESTful');
    $core->setSystemPlugin($systemPlugin);
    
    $options = array(
        'area' => 'api',
    );
    
    $isFoundUrl = $systemPlugin->bindRequest($options);
    Additionally, install Swagger (optional, for API documentation):

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 requests
  • onPost - Handles POST requests
  • onPut - Handles PUT requests
  • onDelete - Handles DELETE requests

You can also combine these with resource names:

  • onUserGet - Handles GET for a user resource
  • onOrdersPost - 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:

  1. Response - For returning data to the client
  2. Request - 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 Unauthorized
  • NotFoundException - Returns 404 Not Found
  • SystemException - Returns 500 Internal Server Error with details
  • ApiException - 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:

  1. Install swagger-php:

    composer global require zircote/swagger-php

  2. Add Swagger annotations to your API plugins

  3. 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

  1. Consistent Response Format: Use a consistent structure for responses

    $response->data = $userData;
    $response->status = true;
    $response->meta = ['timestamp' => time()];

  2. Proper HTTP Status Codes: Use appropriate HTTP status codes

    throw new ApiException("Resource not found", 404);

  3. Resource Nesting: Use proper resource nesting for related data

    // GET /users/123/orders
    public function onUserOrdersGet(Response &$response, Request $request, $idUser)

  4. Proper Parameter Validation: Always validate input parameters

  5. Rate Limiting: Implement rate limiting for public APIs
  6. Versioning: Consider API versioning for long-term stability
  7. 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
}