Back-end20 minute read

PhalconPHP: A Solution for High-load RESTful APIs

PhalconPHP will make your high-load application fast and easy, it’s one of the fastest MVC frameworks for PHP available. It’s written in C and supplied as a compiled PHP extension, so it doesn’t need to be interpreted at every request. Consider PhalconPHP for your next project, you won’t regret it.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

PhalconPHP will make your high-load application fast and easy, it’s one of the fastest MVC frameworks for PHP available. It’s written in C and supplied as a compiled PHP extension, so it doesn’t need to be interpreted at every request. Consider PhalconPHP for your next project, you won’t regret it.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Andrew Belousoff
Verified Expert in Engineering
13 Years of Experience

Andrew (MCS) has done both desktop and full-stack development since 2010, focusing on PHP, JS, CSS, and C++, plus databases and CMSes.

Share

Suppose you need to create a high-load project based on a PHP MVC framework. You would probably use caching wherever possible. Maybe you would build the project in a single file, or maybe even write your own MVC framework with minimal functionality, or rewrite some parts of another framework. While, yes, this works, it’s a little bit tricky, isn’t it? Fortunately, there is one more solution that makes most of these manipulations unnecessary (save for the cache, perhaps), and this solution is called the PhalconPHP framework.

What Is PhalconPHP?

PhalconPHP is an MVC framework for PHP written in C and supplied as a compiled PHP extension. This is what makes it one of the fastest frameworks available (to be completely honest the fastest one is Yaf, but it is a micro framework and has much, much more limited functionality than Phalcon). PhalconPHP doesn’t need any long operations with PHP files and it doesn’t need to be interpreted at every request—it’s loaded into RAM once when your web server is started and consumes very few resources.

MVC frameworks have been considered best practice in web development for a long time—by now it is a sort of a professional standard, so most web developers are familiar with at least one MVC framework for PHP: Symfony, Yii, Laravel, CodeIgniter, Zend Framework, etc. They each have their own advantages and disadvantages, but what do they all have in common? All of them are written in PHP and consist of many included PHP files with a huge amount of logic that has to be run by the interpreter on every request, every single time your code runs. While this makes for great transparency, we pay with performance. Large amounts of code and lots of included files cost a great deal of memory and time, especially in PHP (since it’s interpreted, not compiled). Yes, the situation has become much better in PHP 7, but there is still lots to be improved, and PhalconPHP brings those improvements to the table.

Let’s take a look at some benchmarks.

PhalconPHP Benchmarks

The official benchmarks are five years old—too old to be valid now, but even then you can see dramatically what distinguishes PhalconPHP. Let’s look at something newer. In a 2016 comparison, Phalcon places top five—an obvious leader among the professional frameworks, and conceding only to raw PHP and some micro frameworks.

PhalconPHP benchmarks

So, Phalcon is fast. Raw PHP is also fast, but we need all the facilities that an MVC framework has to offer, and Phalcon rises to the challenge, including components such as:

  • ORM
  • Volt template Engine
  • Dependency Injection (DI) Container
  • Caching
  • Logging
  • Routing systems
  • Security block
  • Autoloader
  • Forms module

That’s just to name a few. To cut a long story short, PhalconPHP has everything you need to build a big enterprise application such as a RESTful API for a high-load system.

One more nice thing about Phalcon is its tiny style—just compare Phalcon ORM and enormous Doctrine 2.

Let’s take a look at creating a PhalconPHP project.

Two Types of Phalcon Projects: Full-stack and Micro

Generally, there are two types of MVC frameworks: full-stack frameworks (like Symfony, Yii) and micro frameworks (like Lumen, Slim, Silex).

Full-stack frameworks are a good choice for a big project since they provide more functionality, but they need a bit more qualification and time to run.

Micro frameworks allow you to create lightweight prototypes very quickly, but they lack functionality, and so it’s generally better to avoid using them for large projects. One advantage of micro frameworks, however, is their performance. They are usually much faster than full-stack ones (for example, the Yaf framework is inferior in performance only to raw PHP).

PhalconPHP supports both: You can either create a full-stack or a micro application. Even better, when you develop your project in PhalconPHP as a micro application, you still have access to most of Phalcon’s powerful features and its performance still remains faster than as a full-stack application.

At a past job, my team needed to build a high-load RESTful system. One of the things we did was compare prototype performance between a full-stack application in Phalcon and a Phalcon micro application. We found that PhalconPHP’s micro applications tended to be a lot faster. I can’t show you any benchmarks due to NDA reasons, but in my opinion, if you want to make the most of Phalcon’s performance, use micro applications. While it’s less convenient to code a micro application than a full-stack one, PhalconPHP’s micro applications still have everything that you might need for your project and better performance. To illustrate, let’s write a very simple RESTful micro application in Phalcon.

Building a RESTful API

Almost all permutations of a RESTful application have one thing in common: a User entity. So, for our example project, we will create a tiny REST application to create, read, update, and delete users, (also known as CRUD).

You can see this project fully completed at my GitLab repository. There are two branches there because I decided to divide this project into two parts: the first branch, master, contains only basic functionality without any specific PhalconPHP features, while the second one, logging-and-cache, contains Phalcon’s logging and caching functionality. You can compare them and see how easy it is to implement such functions in Phalcon.

Installation

I’m not going to go over installation: You may use any database, any operating system, and any web server that you want. It is described well in the official install documentation, so just follow the instructions depending on your operating system.

Web server installation notes are also available in the official Phalcon documentation.

Note that your PHP version should not be less than 5.6.

I use Ubuntu 16.10, PostgreSQL 9.5.6, Nginx 1.10.0, PHP 7, and Phalcon 3.0. I’ve included an Nginx config sample and a PostgreSQL dump file in the project, so feel free to use them. If you prefer another configuration, it won’t be difficult to change it.

Project Structure and Configuration

First of all, create the initial project structure.

While Phalcon lets you use any structure you like, the structure I chose for this exercise partly implements an MVC-pattern. We have no views because it’s a RESTful project, but we have controllers and models, each with their own folder, and services. Services are the classes that implement the business logic of the project, dividing the “model” part of MVC into two pieces: data models (which communicate with the database) and business logic models.

index.php, located in the public folder, is a bootstrap file that loads all the necessary parts and configuration. Note that all our configuration files are placed in the config folder. We could put these in the bootstrap file (and this is the way shown in the official documentation), but, in my opinion, this gets unreadable in big projects, so I prefer the folder separation from the very beginning.

Creating index.php

Our first pass at index.php will load configuration and autoload classes, then initialize routes, a dependency injection container, and the PhalconPHP micro application. It will then pass control over to that micro application core, which will handle requests according to the routes, run business logic, and returns results.

Let’s take a look at the code:

<?php

try {
  // Loading Configs
  $config = require(__DIR__ . '/../app/config/config.php');

  // Autoloading classes
  require __DIR__ . '/../app/config/loader.php';

  // Initializing DI container
  /** @var \Phalcon\DI\FactoryDefault $di */
  $di = require __DIR__ . '/../app/config/di.php';

  // Initializing application
  $app = new \Phalcon\Mvc\Micro();

  // Setting DI container
  $app->setDI($di);

  // Setting up routing
  require __DIR__ . '/../app/config/routes.php';

  // Making the correct answer after executing
  $app->after(
    function () use ($app) {
      // Returning a successful response
    }
  );

  // Processing request
  $app->handle();
} catch (\Exception $e) {
  // Returning an error response
}

Configuring a \Phalcon\Config Object

There are several ways to store configuration files in Phalcon:

  • A YAML file
  • A JSON file
  • An INI file
  • A PHP array

Storing your configuration in a PHP array is the fastest option, and since we’re writing a high-load application and don’t need to slow down our performance, this is what we’ll do. Specifically, we’ll use a \Phalcon\Config object to load our configuration options into the project. We’ll have a very short configuration object:

<?php

return new \Phalcon\Config(
    [
        'database' => [
            'adapter' => 'Postgresql',
            'host' => 'localhost',
            'port' => 5432,
            'username' => 'postgres',
            'password' => '12345',
            'dbname' => 'articledemo',
        ],

        'application' => [
            'controllersDir' => "app/controllers/",
            'modelsDir' => "app/models/",
            'baseUri' => "/",
        ],
    ]
);

This file contains two basic configurations, one for the database and one for the application. Obviously, the database configuration is used for connecting to the database, and as for the application array, we’ll need it later since it’s used by Phalcon’s system tools. You can read about Phalcon configurations in more detail in the official documentation.

Configuring loader.php

Let’s look at our next configuration file, loader.php. The loader.php file registers namespaces with corresponding directories via the \Phalcon\Loader object. It’s even simpler:

<?php

$loader = new \Phalcon\Loader();
$loader->registerNamespaces(
  [
    'App\Services'    => realpath(__DIR__ . '/../services/'),
    'App\Controllers' => realpath(__DIR__ . '/../controllers/'),
    'App\Models'      => realpath(__DIR__ . '/../models/'),
  ]
);

$loader->register();

Now all the classes from these namespaces will be automatically loaded and available. If you want to add a new namespace and directory, just add a line in this file. You can also avoid using namespaces by registering specific directories or specific files. All these possibilities are described in the PhalconPHP loader documentation.

Configuring the Dependency Injection Container

Like many other contemporary frameworks, Phalcon implements a dependency injection (DI) pattern. Objects will be initialized in the DI container and be accessible from it. Likewise, the DI container is connected to the application object, and it will be accessible from all the classes that inherit from the \Phalcon\DI\Injectable class, such as our controllers and services.

Phalcon’s DI pattern is very powerful. I consider this component to be one of the most important in this framework and I strongly recommend you to read its entire documentation to understand how it works. It provides the key to many of Phalcon’s functions.

Let’s take a look at a few of them. Our di.php file will look like this:

<?php

use Phalcon\Db\Adapter\Pdo\Postgresql;

// Initializing a DI Container
$di = new \Phalcon\DI\FactoryDefault();

/**
 * Overriding Response-object to set the Content-type header globally
 */
$di->setShared(
  'response',
  function () {
      $response = new \Phalcon\Http\Response();
      $response->setContentType('application/json', 'utf-8');

      return $response;
  }
);

/** Common config */
$di->setShared('config', $config);

/** Database */
$di->set(
  "db",
  function () use ($config) {
      return new Postgresql(
        [
          "host"     => $config->database->host,
          "username" => $config->database->username,
          "password" => $config->database->password,
          "dbname"   => $config->database->dbname,
        ]
      );
  }
);

return $di;

As you can see, our dependency injection (DI) file is little more complicated and there are some particulars you should be aware of. First off, consider the initialization string: $di = new \Phalcon\DI\FactoryDefault();. We create a FactoryDefault object which inherits \Phalcon\Di (Phalcon allows you to create any DI factory that you want). According to the documentation, FactoryDefault “automatically registers all the services provided by the framework. Thanks to this, the developer does not need to register each service individually providing a full stack framework.” This means that common services such as Request and Response will be accessible within the framework classes. You can see the full list of such services in the Phalcon service documentation.

The next important thing is the setting process: There are several ways to register something in the DI container, and all of them are completely described the the PhalconPHP registering documentation. In our project, though, we use three ways: an anonymous function, a variable, and a string.

The anonymous function allows us to do many things while initializing the class. In this project specifically, we first override a Response object to set the content-type as JSON for all of the project’s responses and then initialize a database adapter, using our configuration object.

As I mentioned before, this project uses PostgreSQL. If you decide to use another database engine, just change the database adapter in the db set function. You can read more about available database adapters and about the database layer in PhalconPHP’s database documentation.

The third thing of note is that I register a $config variable which implements the \Phalcon\Config service. While it’s not actually used within our example project, I’ve decided to include it here because it’s one of most commonly-used services; other projects may need access to the configuration almost everywhere.

The final interesting thing here is the actual setShared method itself. Calling this makes a service “shared,” which means that it starts acting like a singleton. According to the documentation: “Once the service is resolved for the first time the same instance of it is returned every time a consumer retrieves the service from the container.”

Configuring routes.php…or Not

The last included file is routes.php. Let’s leave it empty for now—we’ll fill it alongside our controllers.

Implementing a RESTful Core

What makes a web project RESTful? According to Wikipedia, there are three main parts to a RESTful application: - A base URL - An internet media type that defines state transition data elements - Standard HTTP methods (GET, POST, PUT, DELETE) and standard HTTP response codes (200, 403, 400, 500, etc).

In our project, base URLs will be placed into routes.php file and other mentioned points will be described now.

We’ll receive request data as application/x-www-form-urlencoded and send response data as application/json. Though I don’t believe it’s a good idea to use x-www-form-urlencoded in a real application (since you’ll struggle to send complex data structures and associative arrays with x-www-form-urlencoded), I’ve decided to implement this standard for simplicity’s sake.

If you remember, we’ve already set our response JSON header in our DI file:

$di->setShared(
  'response',
  function () {
      $response = new \Phalcon\Http\Response();
      $response->setContentType('application/json', 'utf-8');

      return $response;
  }
);

Now we have to set up response codes and a response format. The official tutorial suggests that we form JSON responses in every single method, but I don’t think it’s a good idea. It is much more universal to return controller method results as arrays and then convert them to standard JSON responses. It’s also wiser to form HTTP response codes in a single place within the project; we’re going to do this in our index.php file.

To do this, we’re going to utilize Phalcon’s ability to execute code before and after request handling with the $app->before() and $app->after() methods. We’ll place a callback in the $app->after() method for our purpose:

// Making the correct answer after executing
  $app->after(
    function () use ($app) {
      // Getting the return value of method
      $return = $app->getReturnedValue();

      if (is_array($return)) {
        // Transforming arrays to JSON
        $app->response->setContent(json_encode($return));
      } elseif (!strlen($return)) {
        // Successful response without any content
        $app->response->setStatusCode('204', 'No Content');
      } else {
        // Unexpected response
        throw new Exception('Bad Response');
      }

      // Sending response to the client
      $app->response->send();
    }

Here we get the return value and transform array to JSON. If everything was OK, but the return value was empty (for example, if we’d successfully added a new user), we would give a 204 HTTP code and send no content. In all the other cases, we throw an exception.

Handling Exceptions

One of the most important aspects of a RESTful application is correct and informative responses. High-load applications are usually big, and errors of various types can occur everywhere: validation errors, access errors, connection errors, unexpected errors, etc. We want to transform all of these errors into unified HTTP response codes. It can be easily done with the help of the exceptions.

In my project, I’ve decided to use two different kinds of exceptions: there are “local” exceptions- special classes inherited from the \RuntimeException class, separated by services, models, adapters and so on (such division helps to treat each level of the MVC model as a separate one)- then there are HttpExceptions, inherited from the AbstractHttpException class. These exceptions are consistent with HTTP response codes, so their names are Http400Exception, Http500Exception, and so on.

The AbstractHttpException class has three properties: httpCode, httpMessage and appError. First two properties are overridden in their inheritors and contain basic response information such as httpCode: 400 and httpMessage: Bad request. The appError property is an array of the detailed error information, including the error description.

Our final version of index.php will catch three types of exceptions: AbstractHttpExceptions, as described above; Phalcon Request Exceptions, which may occur while parsing a request; and all other unanticipated exceptions. All of them are converted to a pretty JSON format and sent to the client via standard Phalcon Response class:

<?php

use App\Controllers\AbstractHttpException;

try {
  // Loading Configs
  $config = require(__DIR__ . '/../app/config/config.php');

  // Autoloading classes
  require __DIR__ . '/../app/config/loader.php';

  // Initializing DI container
  /** @var \Phalcon\DI\FactoryDefault $di */
  $di = require __DIR__ . '/../app/config/di.php';

  // Initializing application
  $app = new \Phalcon\Mvc\Micro();
  // Setting DI container
  $app->setDI($di);

  // Setting up routing
  require __DIR__ . '/../app/config/routes.php';

  // Making the correct answer after executing
  $app->after(
    // After Code
  );

  // Processing request
  $app->handle();
} catch (AbstractHttpException $e) {
  $response = $app->response;
  $response->setStatusCode($e->getCode(), $e->getMessage());
  $response->setJsonContent($e->getAppError());
  $response->send();
} catch (\Phalcon\Http\Request\Exception $e) {
  $app->response->setStatusCode(400, 'Bad request')
                ->setJsonContent([
                  AbstractHttpException::KEY_CODE    => 400,
                  AbstractHttpException::KEY_MESSAGE => 'Bad request'
                ])
                ->send();
} catch (\Exception $e) {
  // Standard error format
  $result = [
    AbstractHttpException::KEY_CODE    => 500,
    AbstractHttpException::KEY_MESSAGE => 'Some error occurred on the server.'
  ];

  // Sending error response
  $app->response->setStatusCode(500, 'Internal Server Error')
                ->setJsonContent($result)
                ->send();
}

Creating Models with Phalcon Dev Tools

If you use a contemporary IDE, you’re probably used to code highlighting and completion. Likewise, in a typical PHP framework, you can include a folder with a framework to go to a function declaration just in one click. Seeing as Phalcon is an extension, we don’t get this option automatically. Fortunately, there is a tool which fills this gap called “Phalcon Dev Tools,” which can be installed via Composer (if you still don’t know what it is, the time is now to get to know about this amazing package manager). Phalcon Dev Tools consist of code stubs for all the classes and functions in Phalcon, and provide some code generators with both console and GUI versions, documented on the PhalconPHP website. These tools can help with creating all the parts of the MVC pattern, but we’ll only cover model generation.

OK, let’s install Phalcon Dev Tools via Composer. Our composer.json file will look like this:

{
  "require": {
    "php": ">=5.6.0",
    "ext-phalcon": ">=3",
    "ext-pgsql": "*"
  },
  "require-dev": {
    "phalcon/devtools": "3.*.*@dev"
  }
}

As you can see,we require PHP 5.6, Phalcon 3, and the pgsql extension (which you can change to your database extension or exclude altogether).

Make sure that you’ve got the correct PHP, Phalcon, and DB extension versions, and run composer:

$ composer install

The next step is to create our database. It is very simple and consists only of one table, users. Though I’ve included a pg_dump file in the project, here’s the SQL in PostgreSQL dialect:

CREATE DATABASE articledemo;

CREATE TABLE public.users (
  id INTEGER PRIMARY KEY NOT NULL DEFAULT nextval('users_id_seq'::regclass),
  first_name CHARACTER VARYING(255),
  last_name CHARACTER VARYING(255),
  pass CHARACTER VARYING(255),
  login CHARACTER VARYING(255) NOT NULL
);

Now that the database is created, we can proceed to the model generation process. Phalcon Dev Tools uses an empty .phalcon folder to detect if an application is a Phalcon project, so you’ll have to create this empty folder in your project root. It also uses some settings from the configuration file we’ve created—all the variables stored under the application section and adapter from the database section. To generate our model, we need to execute the following command from the project root folder:

$ php vendor/phalcon/devtools/phalcon.php model users --namespace="App\Models" --get-set

If all the previous steps have been done correctly, you’ll get a working model file, Users.php, in your models folder, already placed in a namespace with getters and setters as the command line indicated. Next is the controller.

Controllers and Routing

Since our application only CRUDs (creates, reads, updates, and deletes) users, we’ll create only one controller, the Users controller with the following operations:

  • Add user
  • Show list of users
  • Update user
  • Delete user

While controllers can be created with the help of Phalcon Dev Tools, we’ll do it manually and implement AbstractController and its child, UsersController.

Creating the AbstractController is a good decision for Phalcon because we can place all the necessary classes which we’ll get from dependency injection into the PHPDoc block. This will help with the IDE autocomplete function. We can also program in some error constants that are common to all the potential controllers.

For now, our abstract controller will look like this:

<?php

namespace App\Controllers;

/**
 * Class AbstractController
 *
 * @property \Phalcon\Http\Request              $request
 * @property \Phalcon\Http\Response             $htmlResponse
 * @property \Phalcon\Db\Adapter\Pdo\Postgresql $db
 * @property \Phalcon\Config                    $config
 * @property \App\Services\UsersService         $usersService
 * @property \App\Models\Users                  $user
 */
abstract class AbstractController extends \Phalcon\DI\Injectable
{
    /**
     * Route not found. HTTP 404 Error
     */
    const ERROR_NOT_FOUND = 1;

    /**
     * Invalid Request. HTTP 400 Error.
     */
    const ERROR_INVALID_REQUEST = 2;
}

Just a simple Phalcon injectable class, as specified by the extends syntax, nothing more. Next, let’s create the UsersController skeleton:

<?php

namespace App\Controllers;

/**
 * Operations with Users: CRUD
 */
class UsersController extends AbstractController
{
    /**
     * Adding user
     */
    public function addAction()
    {
       
    }

    /**
     * Returns user list
     *
     * @return array
     */
    public function getUserListAction()
    {
       
    }

     /**
     * Updating existing user
     *
     * @param string $userId
     */
    public function updateUserAction($userId)
    {
       
    }

    /**
     * Delete an existing user
     *
     * @param string $userId
     */
    public function deleteUserAction($userId)
    {
       
    }
}

At the moment, it’s just a class with empty actions which will eventually hold corresponding HTTP requests.

Now it’s time to fill in the routes.php file. In Phalcon micro applications we create collections, one for each controller, and add all the handled requests as get, post, put, delete methods, which take a route pattern and a proceeding function as arguments. Note that a proceeding function should either be an anonymous function or a controller’s method name. Here’s what our routes.php file looks like:

<?php

$usersCollection = new \Phalcon\Mvc\Micro\Collection();
$usersCollection->setHandler('\App\Controllers\UsersController', true);
$usersCollection->setPrefix('/user');
$usersCollection->post('/add', 'addAction');
$usersCollection->get('/list', 'getUserListAction');
$usersCollection->put('/{userId:[1-9][0-9]*}', 'updateUserAction');
$usersCollection->delete('/{userId:[1-9][0-9]*}', 'deleteUserAction');
$app->mount($usersCollection);

// not found URLs
$app->notFound(
  function () use ($app) {
      $exception =
        new \App\Controllers\HttpExceptions\Http404Exception(
          _('URI not found or error in request.'),
          \App\Controllers\AbstractController::ERROR_NOT_FOUND,
          new \Exception('URI not found: ' . $app->request->getMethod() . ' ' . $app->request->getURI())
        );
      throw $exception;
  }
);

We also set a handling controller and a URI prefix. For our example, a URI will look like http://article.dev/user/add, and it must be a post request. If we want to change user data, the URI must be a put request and will look like http://article.dev/user/12 to change data for the user with an ID of 12. We also define a not-found URL handler, which throws an error. For more information, refer to the PhalconPHP documentation for routes in a full stack application, and for routes in a micro application.

Let’s move to the controller’s body, and specifically the addAction method (all the others are similar; you can see them in the application code). A controller method does five things:

  1. Gets and validates request parameters
  2. Prepares data for the service method
  3. Calls the service method
  4. Handles exceptions
  5. Sends the response

Let’s walk through each step, starting with validation. While Phalcon has a powerful validation component, it’s much more expedient to validate data in an old-style way in this case, so, our validation block will look like this:

$errors = [];
$data = [];

$data['login'] = $this->request->getPost('login');
if (!is_string($data['login']) || !preg_match('/^[A-z0-9_-]{3,16}$/', $data['login'])) {
            $errors['login'] = 'Login must consist of 3-16 latin symbols, numbers or \'-\' and \'_\' symbols';
}

Here we check if the post parameter is a string that matches a regular expression. All values are put into the $data array, which is then passed to the UsersService class. All errors are placed into the $errors array, which then is added to an error details’ array inside Http400Exception, where it will be transformed into the detailed response seen in index.php:

Here is the full addAction method code with all its validation, which includes a call to the createUser method in UsersService (which we haven’t created yet):

    public function addAction()
    {
       /** Init Block **/
        $errors = [];
        $data = [];
   /** End Init Block **/

   /** Validation Block **/
        $data['login'] = $this->request->getPost('login');
        if (!is_string($data['login']) || !preg_match('/^[A-z0-9_-]{3,16}$/', $data['login'])) {
            $errors['login'] = 'Login must consist of 3-16 latin symbols, numbers or \'-\' and \'_\' symbols';
        }

        $data['password'] = $this->request->getPost('password');
        if (!is_string($data['password']) || !preg_match('/^[A-z0-9_-]{6,18}$/', $data['password'])) {
            $errors['password'] = 'Password must consist of 6-18 latin symbols, numbers or \'-\' and \'_\' symbols';
        }

        $data['first_name'] = $this->request->getPost('first_name');
        if ((!empty($data['first_name'])) && (!is_string($data['first_name']))) {
            $errors['first_name'] = 'String expected';
        }

        $data['last_name'] = $this->request->getPost('last_name');
        if ((!empty($data['last_name'])) && (!is_string($data['last_name']))) {
            $errors['last_name'] = 'String expected';
        }

        if ($errors) {
            $exception = new Http400Exception(_('Input parameters validation error'), self::ERROR_INVALID_REQUEST);
            throw $exception->addErrorDetails($errors);
        }
   /** End Validation Block **/

   /** Passing to business logic and preparing the response **/
        try {
            $this->usersService->createUser($data);
        } catch (ServiceException $e) {
            switch ($e->getCode()) {
                case AbstractService::ERROR_ALREADY_EXISTS:
                case UsersService::ERROR_UNABLE_CREATE_USER:
                    throw new Http422Exception($e->getMessage(), $e->getCode(), $e);
                default:
                    throw new Http500Exception(_('Internal Server Error'), $e->getCode(), $e);
            }
        }
   /** End Passing to business logic and preparing the response  **/
    }

As you can see, we handle two known exceptions in that last section: user already exists and unable to create user due to some internal problem such as a database connection error. By default, unknown exceptions will be thrown as an HTTP 500 (internal server error). Though we don’t give any details to the end user, it is strongly recommended to store all error details (including trace) in the log.

And, please, don’t forget to use all the necessary classes, borrowed from the other namespaces:

use App\Controllers\HttpExceptions\Http400Exception;
use App\Controllers\HttpExceptions\Http422Exception;
use App\Controllers\HttpExceptions\Http500Exception;
use App\Services\AbstractService;
use App\Services\ServiceException;
use App\Services\UsersService;

Business Logic

The last part to create is business logic. Just like with the controllers, we’ll create an abstract service class:

<?php

namespace App\Services;

/**
 * Class AbstractService
 *
 * @property \Phalcon\Db\Adapter\Pdo\Postgresql $db
 * @property \Phalcon\Config                    $config
 */
abstract class AbstractService extends \Phalcon\DI\Injectable
{
    /**
     * Invalid parameters anywhere
     */
    const ERROR_INVALID_PARAMETERS = 10001;

    /**
     * Record already exists
     */
    const ERROR_ALREADY_EXISTS = 10002;
}

The idea is completely the same as in controller’s block, so I won’t comment it. Here’s the skeleton of our UsersService class:

<?php

namespace App\Services;

use App\Models\Users;

/**
 * business logic for users
 *
 * Class UsersService
 */
class UsersService extends AbstractService
{
  /** Unable to create user */
  const ERROR_UNABLE_CREATE_USER = 11001;

  /**
   * Creating a new user
   *
   * @param array $userData
   */
  public function createUser(array $userData)
  {
    
  }
}

And the createUser method itself:

public function createUser(array $userData)
{
  try {
    $user   = new Users();
    $result = $user->setLogin($userData['login'])
                   ->setPass(password_hash($userData['password'], PASSWORD_DEFAULT))
                   ->setFirstName($userData['first_name'])
                   ->setLastName($userData['last_name'])
                   ->create();

    if (!$result) {
      throw new ServiceException('Unable to create user', self::ERROR_UNABLE_CREATE_USER);
    }

  } catch (\PDOException $e) {
    if ($e->getCode() == 23505) {
      throw new ServiceException('User already exists', self::ERROR_ALREADY_EXISTS, $e);
    } else {
      throw new ServiceException($e->getMessage(), $e->getCode(), $e);
    }
  }
}

This method is as easy as can be. We just create a new model object, call its setters (which returns the object itself; this allows us to make a call chain) and throw a ServiceException in the case of an error. That’s it! We can now proceed to testing.

Testing

Now let’s look at the results using Postman. Let’s test some trash data first:

Postman with invalid data.

Request:

POST http://article.dev/user/add
login:1
password:1
first_name:Name
last_name:Sourname

Response (400: Bad Request):

{
  "error": 2,
  "error_description": "Input parameters validation error",
  "details": {
    "login": "Login must consist of 3-16 latin symbols, numbers or '-' and '_' symbols",
    "password": "Password must consist of 6-18 latin symbols, numbers or '-' and '_' symbols"
  }
}

That checks out. Now for some correct data:

Postman with valid data.

Request:

POST http://article.dev/user/add
login:user4
password:password4
first_name:Name
last_name:Sourname

Response (204):

No content, which is what we expected. Now let’s make sure it worked and get the full user list (which we didn’t describe in the article, but you can see it in the application example):

Request:

GET http://article.dev/user/list

Response (200 OK):

[
  {
    "id": 1,
    "login": "user4",
    "first_name": "Name",
    "last_name": "Sourname"
  }
]

Well, it works!

Logging and Caching

It’s hard to imagine a high-load application without logging and caching, and Phalcon provides very seductive classes for it. But I’m writing an article here, not a book; I’ve added logging and caching to the sample application, but I’ve placed this code into another branch called logging-and-cache so you can easily look at it and see the difference in the code. Just like the other Phalcon features, these two are well-documented: Logging and Caching.

Disadvantages

As you can see, Phalcon is really cool, but like other frameworks, it has its disadvantages, the first of which is the same as its main advantage—it’s a compiled C extension. That’s why there is no way for you to change its code easily. Well, if you know C, you can try to understand its code and make some changes, run make and get your own modification of Phalcon, but it is much more complicated than making some tweaks in PHP code. So, generally, if you find a bug inside Phalcon, it won’t be so easy to fix.

This is partially solved in Phalcon 2 and Phalcon 3, which let you write extensions to Phalcon in Zephir. Zephir is a programming language designed to ease the creation and maintainability of extensions for PHP with a focus on type and memory safety. Its syntax is very close to PHP and Zephir code is compiled into shared libraries, same as the PHP extension. So, if you want to enhance Phalcon, now you can.

The second disadvantage is the free framework structure. While Symfony makes developers use a firm project structure, Phalcon has very few strict rules; developers can create any structure they like, though there is a structure that is recommended by its authors. This isn’t a critical disadvantage, but some people may consider it too raw when you write the paths to all the directories in a bootstrap file manually.

PhalconPHP: Not Just For High-load Apps

I hope you’ve enjoyed this brief overview of PhalconPHP’s killing features and the accompanying simple example of a Phalcon project. Obviously, I didn’t cover all the possibilities of this framework since it’s impossible to describe all of them in one article, but fortunately Phalcon has brilliantly detailed documentation with seven marvelous tutorials which help you understand almost everything about Phalcon.

You’ve now got a brand new way to create high load applications easily, and you’ll find, if you like Phalcon, it can be a good choice for other types of applications too.

Hire a Toptal expert on this topic.
Hire Now
Andrew Belousoff

Andrew Belousoff

Verified Expert in Engineering
13 Years of Experience

Ljubljana, Slovenia

Member since December 12, 2016

About the author

Andrew (MCS) has done both desktop and full-stack development since 2010, focusing on PHP, JS, CSS, and C++, plus databases and CMSes.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.