Recommended layout for more complex API

Hey there,

I intend to use \Phalcon\Mvc\Micro for a REST API. I wonder how I could employ the module approach in combination with the Micro MVC. I'd like to be able to have controllers inside modules which handle GET/POST/PUT/DELETE etc. Is there some smart way? :) Thanks in advance!

@Edit: I noticed Phalcon\Mvc\Micro\Collection exists, which seems to be what I'm looking for. But how do I do the routing for that, in a general way?

Best, Philipp

You can implement such functionality with two controller classes that will serve as the base of your controller logic

// Base controller
namespace Niden;

class Controller extends \Phalcon\Mvc\Controller
{
}

The above is the base controller which all controllers extend. It contains common functionality you will need for any controller that extends it. Your controllers can extend this class if you do not want a particular controller not to have REST capabilities

// REST controller
namespace Niden;

use \Phalcon\Http\Response as PhResponse;

class RESTController extends \Niden\Controller
{

    protected $statusCode = 200;
    protected $headers    = array();
    protected $payload    = '';
    protected $format     = '';

    /**
     * Getters
     */

    /**
     * @return int
     */
    public function getStatusCode()
    {
        return $this->statusCode;
    }

    /**
     * @return array
     */
    public function getHeaders()
    {
        return $this->headers;
    }

    /**
     * @return string
     */
    public function getPayload()
    {
        return $this->payload;
    }

    /**
     * @return string
     */
    public function getFormat()
    {
        return !empty($this->format) ? $this->format : $this->config->app_format;
    }

    /**
     * Setters
     */
    public function setStatusCode($code)
    {
        $this->statusCode = $code;
    }

    public function setHeaders($key, $value)
    {
        $this->headers[$key] = $value;
    }

    public function setPayload($payload)
    {
        $this->payload = $payload;
    }

    public function setFormat($format)
    {
        $this->format = $format;
    }

    protected function getResultset($data, $node, $class)
    {
        return resultsToArray($data, $node, $class);
    }

    protected function initResponse()
    {
        $this->statusCode = 200;
        $this->headers    = array();
        $this->payload    = '';
    }

    protected function render()
    {
        $format      = $this->getFormat();
        $payload     = $this->getPayload();
        $status      = $this->getStatusCode();
        $description = $this->getResponseDescription($status);
        $headers     = $this->getHeaders();

        switch ($format)
        {
            case 'json':
                $contentType = 'application/json';
                $content     = json_encode($payload);
                break;
            case 'xml':
                $contentType = 'application/xml';
                /**
                 * @todo implement arrayToXML
                 */
                $content     = arrayToXML($payload);
                break;
            default:
                $contentType = 'text/plain';
                $content     = $payload;
                break;
        }

        $response = new PhResponse();

        $response->setStatusCode($status, $description);
        $response->setContentType($contentType, 'UTF-8');
        $response->setHeader('Access-Control-Allow-Origin', '*');
        $response->setHeader('Access-Control-Allow-Headers', 'X-Requested-With');
        $response->setContent($content);

        // Set the additional headers
        foreach ($headers as $key => $value) {
            $response->setHeader($key, $value);
        }

        $this->view->disable();

        return $response;
    }

    public function getAction($field, $value)
    {
        // implement some functionality to get the class name
        // and the method from the get_called_class()
        $class  = controllerToModel(get_called_class());
        $node   = controllerToClass(get_called_class());
        $method = 'fetchBy' . ucfirst($field);
        $node   = strtolower($node);

        $data = $class::$method($value);

        return $data;
    }

    public function postAction()
    {
        if ($this->request->isPost()) {

            $payload = $_POST;
            $class   = controllerToModel(get_called_class());

            $object = new $class();

            foreach ($payload as $field => $value) {
                $object->$field = $value;
            }

            $status = $object->save();

            $data           = array();
            $data['status'] = $status;

            if (!$status) {

                $data['errno'] = -1;

                foreach ($object->getMessages() as $message) {
                    $data['error'][] = $message;
                }
            }

            // @todo Check for errors here
            return $data;
        }
    }

    public function putAction()
    {
        if ($this->request->isPost()) {

            $payload = $_POST;
            $class   = controllerToModel(get_called_class());

            $object = new $class();

            foreach ($payload as $field => $value) {

                $object->$field = $value;

            }

            $status = $object->save();

            $data           = array();
            $data['status'] = $status;

            if (!$status) {

                $data['errno'] = -1;

                foreach ($object->getMessages() as $message) {
                    $data['error'][] = $message;
                }
            }

            // @todo Check for errors here
            return $data;
        }
    }

    public function listAction($page = null)
    {
        // Some manipulation needed to check what we need to call
        // i.e. which model. This comes from the controller name
        $class = controllerToModel(get_called_class());
        $node  = controllerToClass(get_called_class());
        $node  = strtolower($node);

        $data = $class::fetchAll();

        // Here you can implement some sort of pagination for your data
        return $data;
    }

    public function countAction()
    {
        // Some manipulation needed to check what we need to call
        // i.e. which model. This comes from the controller name
        $model = controllerToModel(get_called_class());

        $data['count'] = $model::fetchCount();

        $this->initResponse();
        $this->setPayload($data);

        return $this->render();
    }

    protected function schema($class)
    {
        $output = $class::fetchSchema();

        $this->setFormat('xml');
        $this->setPayload($output);

        return $this->render();
    }

    protected function getResponseDescription($code)
    {
        $codes = array(

            // Informational 1xx
            100 => 'Continue',
            101 => 'Switching Protocols',

            // Success 2xx
            200 => 'OK',
            201 => 'Created',
            202 => 'Accepted',
            203 => 'Non-Authoritative Information',
            204 => 'No Content',
            205 => 'Reset Content',
            206 => 'Partial Content',

            // Redirection 3xx
            300 => 'Multiple Choices',
            301 => 'Moved Permanently',
            302 => 'Found',  // 1.1
            303 => 'See Other',
            304 => 'Not Modified',
            305 => 'Use Proxy',
            // 306 is deprecated but reserved
            307 => 'Temporary Redirect',

            // Client Error 4xx
            400 => 'Bad Request',
            401 => 'Unauthorized',
            402 => 'Payment Required',
            403 => 'Forbidden',
            404 => 'Not Found',
            405 => 'Method Not Allowed',
            406 => 'Not Acceptable',
            407 => 'Proxy Authentication Required',
            408 => 'Request Timeout',
            409 => 'Conflict',
            410 => 'Gone',
            411 => 'Length Required',
            412 => 'Precondition Failed',
            413 => 'Request Entity Too Large',
            414 => 'Request-URI Too Long',
            415 => 'Unsupported Media Type',
            416 => 'Requested Range Not Satisfiable',
            417 => 'Expectation Failed',

            // Server Error 5xx
            500 => 'Internal Server Error',
            501 => 'Not Implemented',
            502 => 'Bad Gateway',
            503 => 'Service Unavailable',
            504 => 'Gateway Timeout',
            505 => 'HTTP Version Not Supported',
            509 => 'Bandwidth Limit Exceeded'
        );

        $result = (isset($codes[$code])) ?
            $codes[$code]          :
            'Unknown Status Code';

        return $result;
    }
}

The above is the base controller that any controller that needs REST capabilities should extend. A sample below:

// UserController

namespace Niden\Controllers;

use \Phalcon\Mvc\View as PhView;
use \Niden\Models\User as NidenUser;

class UserController extends \Niden\RESTController
{
    /**
     * Initializes the controller
     */
    public function initialize()
    {
        parent::initialize();
    }

    /**
     * The index action
     */
    public function indexAction()
    {
        // You can return anything you want here, perhaps the schema
    }

    public function getByUserIdAction($id)
    {
        $data = NidenUser::fetchById($id);

        $results = $this->getResultset(
            $data,
            'user',
            '\\Niden\\Models\\User'
        );

        $this->initResponse();
        $this->setPayload($results);

        return $this->render();
    }
}

Hey @niden,

thanks for your snippets, they helped me a lot!

Best, Philipp



6.4k

@Philipp and @niden,

I am brand new to phalconphp (coming from ZF) and I, too, have to design a "more complex api" and was wondering if it's possible to see a folder structure (using modules and controllers per each module) and how does one implement the routing/loading for each module (bootstrapping) when using Micro.

@metialb

I did not use a modular design in the end and never even tried to use the micro framework as it was far from feature-complete at the time. I basically rewrote the Request and Response classes and implemented the routing using annotations. The project I used this for, however, is on hold, so I stopped working on it quite some time ago and didn't dig any deeper into the topic. Sorry to disappoint :)



6.4k

Thanks for the reply @Philipp, I will try to read more and look into it.....meanwhile @niden if you have any suggestion, I would surely appreciate it.