Skip to content

Instantly share code, notes, and snippets.

@stecman
Last active August 17, 2022 15:20
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save stecman/e3ab1796c8c78524cd8e to your computer and use it in GitHub Desktop.
Save stecman/e3ab1796c8c78524cd8e to your computer and use it in GitHub Desktop.
Generic REST controller for use in the Phalcon PHP framework

REST(-ish)* abstract controller for Phalcon PHP 1.x/2.x

This is an example of how to automatically route to actions based on the request method (GET, POST, DELETE, etc) in the Phalcon framework. RestController additionally wraps all action return values as JSON, and captures exceptions to return a consistent JSON response instead of an HTML error page.

Note that the RestController code assumes you have a constant DEV_MODE defined as a boolean. Phalcon doesn't have environment switching as part of its core, so this is the pattern I usually use.

* Calling this REST is a bit of a misnomer, since it's more of a simple action mapper + response converter for pretty URLs and pretty responses. You could use it to do wonderful REST things though, if you like.

Example use

services.php

// ...

$di->set('router', function () use ($di) {
    return require __DIR__ . '/routes.php';
});

// ...

ExampleController.php

class ExampleController extends RestController
{
    /**
     * Runs for GET requests to /api/example
     */
    public function getAction()
    {
        $this->response->setCache(3600);

        // Return the things
    }
    
    /**
     * Runs for POST requests to /api/example
     */
    public function postAction()
    {
        // Receive the things
    }

    /**
     * Runs for POST requests to /api/example/foo
     * @return array
     */
    public function getFooAction()
    {
        return [
            'name' => 'Foo Barison'
        ];
    }

    /**
     * Runs for DELETE requests to /api/example/foo
     * @return array
     */
    public function deleteFooAction()
    {
        // Delete the foo
    }
}
<?php
use Phalcon\Mvc\Controller;
use Phalcon\Mvc\Dispatcher;
use Phalcon\Mvc\DispatcherInterface;
use Phalcon\Mvc\Model\CriteriaInterface;
use Phalcon\Mvc\ModelInterface;
use Phalcon\Mvc\View;
abstract class RestController extends Controller
{
/**
* If the controller is in an error state
* This should be set to true if an exception is uncaught by the called action
* @var bool
*/
private $isErrored = false;
protected function initialize()
{
// Use a custom handler for any exceptions thrown from this controller type
$this->eventsManager->attach('dispatch:beforeException', $this);
}
/**
* Get the search/query/filter from the request
* @return string
*/
public function getQuery()
{
return $this->request->get('q', 'trim');
}
/**
* Get the offset to return from relative to the start of the result set
* @return int
*/
public function getOffset()
{
return (int) $this->request->get('offset');
}
/**
* Get the maximum number of results the request should return
* @return int
*/
public function getLimit()
{
return (int) $this->request->get('limit');
}
/**
* @param CriteriaInterface $query
* @return \Phalcon\Mvc\Model\CriteriaInterface
*/
protected function applyLimitsToQuery(CriteriaInterface $query)
{
if ($this->getLimit()) {
$query->limit($this->getLimit(), $this->getOffset());
}
return $query;
}
/**
* Wrap all results and send to client encoded as JSON
* @param DispatcherInterface $dispatcher
*/
public function afterExecuteRoute(DispatcherInterface $dispatcher)
{
$result = $dispatcher->getReturnedValue();
// Don't attempt to transform the response as it wasn't completed
if ($this->isErrored) {
return;
}
// Expand query objects into arrays
if ($result instanceof CriteriaInterface) {
$result = $result->execute()->toArray();
}
// Transform a model return value to an array of fields
if ($result instanceof ModelInterface) {
$result = $result->toArray();
}
// Only accept arrays and scalars to send to the client
if (!is_array($result) && !is_scalar($result) && !is_null($result)) {
$this->sendException(
new \RuntimeException(sprintf(
'Expected array or scalar return type from controller. Got %s instead',
is_object($result) ? get_class($result) : gettype($result)
))
);
}
$output = [
'result' => $result
];
$this->send($output);
}
public function beforeException(\Phalcon\Events\Event $event, Dispatcher $dispatcher, \Exception $exception)
{
$this->isErrored = true;
$this->sendException($exception);
return false;
}
protected function sendAccessDenied()
{
$this->isErrored = true;
$this->response->setStatusCode(403, 'Access denied');
$this->send([
'error' => true,
'message' => 'You don\'t have permission to access that resource.'
]);
}
protected function sendNotFound()
{
$this->isErrored = true;
$this->response->setStatusCode(404, 'Resource not found');
$this->send([
'error' => true,
'message' => 'No resource found'
]);
}
protected function checkSecurityToken()
{
$token = $this->request->getPost('token');
if ($token && $token === $this->security->getSessionToken()) {
return true;
}
$this->isErrored = true;
$this->response->setStatusCode(400, 'Invalid security token');
$this->send([
'error' => true,
'code' => 'bad-token',
'message' => 'Invalid security token'
]);
return false;
}
private function sendException(\Exception $exception)
{
$this->response->setStatusCode(500, 'Server error');
if (DEV_MODE) {
$this->send([
'error' => true,
'message' => $exception->getMessage(),
'trace' => $exception->getTraceAsString()
]);
} else {
$this->send([
'error' => true,
'message' => 'The server encountered a fatal error trying to process your request.'
]);
// Log the exception with your own logger here if needed
}
}
/**
* Put data in the response as JSON
* @param mixed $output
*/
private function send($output)
{
$this->view->setRenderLevel(View::LEVEL_NO_RENDER);
$this->response->setContentType('application/json');
$opts = DEV_MODE ? JSON_PRETTY_PRINT : 0;
$json = json_encode($output, $opts);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException('Failed to convert server response to JSON: ' . json_last_error_msg());
}
$this->response->setContent($json);
$this->response->send();
}
}
<?php
$router = new \Phalcon\Mvc\Router(false);
$api = new \Phalcon\Mvc\Router\Group();
$api->setPrefix('/api');
$api->add('/:controller/:action/:params', [
'controller' => 1,
'action' => 2,
'params' => 3,
]);
$api->add('/:controller/:action', [
'controller' => 1,
'action' => 2
]);
$api->add('/:controller', [
'controller' => 1,
'action' => ''
]);
$api->add('/:controller/([0-9]+)', [
'controller' => 1,
'id' => 2,
'action' => ''
]);
$api->add('/:controller/([0-9]+)/:action', [
'controller' => 1,
'id' => 2,
'action' => 3
]);
/**
* Prefix incoming action name with HTTP method
*/
foreach ($api->getRoutes() as $route) {
$route->convert('action', function($action) use ($di) {
$method = strtolower($di['request']->getMethod());
return $method . ucfirst($action);
});
}
// Under Phalcon 1.x, the above convert call needs to be replaced
// with the following as the router API differs slightly
// $api->convert('action', function($action) use ($di) {
// $method = strtolower($di['request']->getMethod());
// return $method . ucfirst($action);
// });
$router->mount($api);
@max-arshinov
Copy link

Hi, I am new to phalcon, but need this kind of generic rest controller badly, because I really don't want to copy-paste code for every entity. Unfortunatelly I wasn't able to run your example: i just see a white screen. Can you please provide an example how to run it from index.php?

@stecman
Copy link
Author

stecman commented Mar 11, 2016

@max-arshinov - if the json_encode call failed, that would've caused an empty response. Not sure if that was your problem, but I've updated the gist to throw an exception if encoding as JSON fails.

@pokisin
Copy link

pokisin commented Nov 8, 2016

Excelente aporte, necesitaba algo como esto, GRACIAS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment