PhalconPHP: una solución para API RESTful de alta carga
Publicado: 2022-03-11Suponga que necesita crear un proyecto de alta carga basado en un marco PHP MVC. Probablemente usaría el almacenamiento en caché siempre que sea posible. Tal vez compilaría el proyecto en un solo archivo, o tal vez incluso escribiría su propio marco MVC con una funcionalidad mínima, o reescribiría algunas partes de otro marco. Si bien, sí, esto funciona, es un poco complicado, ¿no? Afortunadamente, hay una solución más que hace que la mayoría de estas manipulaciones sean innecesarias (salvo para el caché, tal vez), y esta solución se llama PhalconPHP framework.
¿Qué es PhalconPHP?
PhalconPHP es un marco MVC para PHP escrito en C y suministrado como una extensión de PHP compilada. Esto es lo que lo convierte en uno de los frameworks más rápidos disponibles (para ser completamente honesto, el más rápido es Yaf, pero es un microframework y tiene una funcionalidad mucho, mucho más limitada que Phalcon). PhalconPHP no necesita operaciones largas con archivos PHP y no necesita ser interpretado en cada solicitud: se carga en la RAM una vez que se inicia su servidor web y consume muy pocos recursos.
Los marcos MVC se han considerado las mejores prácticas en el desarrollo web durante mucho tiempo; ahora es una especie de estándar profesional, por lo que la mayoría de los desarrolladores web están familiarizados con al menos un marco MVC para PHP: Symfony, Yii, Laravel, CodeIgniter, Zend Marco, etc. Cada uno tiene sus propias ventajas y desventajas, pero ¿qué tienen todos en común? Todos ellos están escritos en PHP y consisten en muchos archivos PHP incluidos con una gran cantidad de lógica que el intérprete debe ejecutar en cada solicitud, cada vez que se ejecuta su código. Si bien esto genera una gran transparencia, pagamos con el rendimiento. Grandes cantidades de código y muchos archivos incluidos cuestan una gran cantidad de memoria y tiempo, especialmente en PHP (ya que se interpreta, no se compila). Sí, la situación ha mejorado mucho en PHP 7, pero aún queda mucho por mejorar, y PhalconPHP trae esas mejoras a la mesa.
Echemos un vistazo a algunos puntos de referencia.
Puntos de referencia PhalconPHP
Los puntos de referencia oficiales tienen cinco años, demasiado para ser válidos ahora, pero incluso entonces puede ver dramáticamente lo que distingue a PhalconPHP. Veamos algo más nuevo. En una comparación de 2016, Phalcon se ubica entre los cinco primeros: un líder obvio entre los marcos de trabajo profesionales, y cede solo a PHP sin formato y algunos micro marcos de trabajo.
Entonces, Phalcon es rápido. Raw PHP también es rápido, pero necesitamos todas las facilidades que ofrece un marco MVC, y Phalcon está a la altura del desafío, incluidos componentes como:
- ORM
- Plantilla de voltios Motor
- Contenedor de inyección de dependencia (DI)
- almacenamiento en caché
- Inicio sesión
- Sistemas de enrutamiento
- Bloque de seguridad
- cargador automático
- Módulo de formularios
Eso es solo por nombrar algunos. Para acortar una larga historia, PhalconPHP tiene todo lo que necesita para crear una gran aplicación empresarial, como una API RESTful para un sistema de alta carga.
Una cosa más agradable sobre Phalcon es su estilo diminuto: solo compare Phalcon ORM y el enorme Doctrine 2.
Echemos un vistazo a la creación de un proyecto PhalconPHP.
Dos tipos de proyectos Phalcon: Full-stack y Micro
Generalmente, hay dos tipos de frameworks MVC: frameworks de pila completa (como Symfony, Yii) y microframeworks (como Lumen, Slim, Silex).
Los marcos de trabajo de pila completa son una buena opción para un gran proyecto, ya que brindan más funcionalidad, pero necesitan un poco más de calificación y tiempo para ejecutarse.
Los micro frameworks le permiten crear prototipos livianos muy rápidamente, pero carecen de funcionalidad, por lo que generalmente es mejor evitar usarlos para proyectos grandes. Sin embargo, una ventaja de los micro marcos es su rendimiento. Por lo general, son mucho más rápidos que los de pila completa (por ejemplo, el marco Yaf es inferior en rendimiento solo a PHP sin procesar).
PhalconPHP es compatible con ambos: puede crear una aplicación de pila completa o una micro. Aún mejor, cuando desarrolla su proyecto en PhalconPHP como una microaplicación, todavía tiene acceso a la mayoría de las potentes funciones de Phalcon y su rendimiento sigue siendo más rápido que como una aplicación de pila completa.
En un trabajo anterior, mi equipo necesitaba construir un sistema RESTful de alta carga. Una de las cosas que hicimos fue comparar el rendimiento del prototipo entre una aplicación de pila completa en Phalcon y una microaplicación de Phalcon. Descubrimos que las microaplicaciones de PhalconPHP tendían a ser mucho más rápidas. No puedo mostrarte ningún punto de referencia debido a razones de NDA, pero en mi opinión, si quieres aprovechar al máximo el rendimiento de Phalcon, usa microaplicaciones. Si bien es menos conveniente codificar una microaplicación que una de pila completa, las microaplicaciones de PhalconPHP aún tienen todo lo que podría necesitar para su proyecto y un mejor rendimiento. Para ilustrar, escribamos una microaplicación RESTful muy simple en Phalcon.
Creación de una API RESTful
Casi todas las permutaciones de una aplicación RESTful tienen una cosa en común: una entidad User
. Entonces, para nuestro proyecto de ejemplo, crearemos una pequeña aplicación REST para crear, leer, actualizar y eliminar usuarios (también conocida como CRUD).
Puedes ver este proyecto completamente completado en mi repositorio de GitLab. Hay dos ramas allí porque decidí dividir este proyecto en dos partes: la primera rama, master
, contiene solo la funcionalidad básica sin ninguna característica específica de PhalconPHP, mientras que la segunda, logging-and-cache
, contiene la funcionalidad de registro y almacenamiento en caché de Phalcon. Puede compararlos y ver lo fácil que es implementar tales funciones en Phalcon.
Instalación
No voy a repasar la instalación: puede usar cualquier base de datos, cualquier sistema operativo y cualquier servidor web que desee. Se describe bien en la documentación de instalación oficial, así que solo siga las instrucciones según su sistema operativo.
Las notas de instalación del servidor web también están disponibles en la documentación oficial de Phalcon.
Tenga en cuenta que su versión de PHP no debe ser inferior a 5.6.
Uso Ubuntu 16.10, PostgreSQL 9.5.6, Nginx 1.10.0, PHP 7 y Phalcon 3.0. Incluí una muestra de configuración de Nginx y un archivo de volcado de PostgreSQL en el proyecto, así que siéntete libre de usarlos. Si prefieres otra configuración, no será difícil cambiarla.
Estructura y configuración del proyecto
En primer lugar, cree la estructura inicial del proyecto.
Si bien Phalcon le permite usar cualquier estructura que desee, la estructura que elegí para este ejercicio implementa en parte un patrón MVC. No tenemos vistas porque es un proyecto RESTful, pero tenemos controladores y modelos, cada uno con su propia carpeta y servicios. Los servicios son las clases que implementan la lógica de negocios del proyecto, dividiendo la parte del "modelo" de MVC en dos partes: modelos de datos (que se comunican con la base de datos) y modelos de lógica de negocios.
index.php
, ubicado en la carpeta public
, es un archivo de arranque que carga todas las partes y configuraciones necesarias. Tenga en cuenta que todos nuestros archivos de configuración se colocan en la carpeta de config
. Podríamos ponerlos en el archivo de arranque (y esta es la forma que se muestra en la documentación oficial), pero, en mi opinión, esto se vuelve ilegible en proyectos grandes, por lo que prefiero la separación de carpetas desde el principio.
Creando index.php
Nuestro primer paso en index.php
cargará la configuración y las clases de carga automática, luego inicializará las rutas, un contenedor de inyección de dependencia y la microaplicación PhalconPHP. Luego pasará el control a ese núcleo de microaplicación, que manejará las solicitudes de acuerdo con las rutas, ejecutará la lógica comercial y devolverá los resultados.
Echemos un vistazo al código:
<?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 }
Configuración de un objeto \Phalcon\Config
Hay varias formas de almacenar archivos de configuración en Phalcon:
- Un archivo YAML
- Un archivo JSON
- Un archivo INI
- Una matriz de PHP
Almacenar su configuración en una matriz de PHP es la opción más rápida, y dado que estamos escribiendo una aplicación de alta carga y no necesitamos ralentizar nuestro rendimiento, esto es lo que haremos. Específicamente, usaremos un objeto \Phalcon\Config
para cargar nuestras opciones de configuración en el proyecto. Tendremos un objeto de configuración muy corto:
<?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' => "/", ], ] );
Este archivo contiene dos configuraciones básicas, una para la base de datos y otra para la aplicación. Obviamente, la configuración de la base de datos se usa para conectarse a la base de datos y, en cuanto a la matriz de application
, la necesitaremos más adelante, ya que la usan las herramientas del sistema de Phalcon. Puede leer sobre las configuraciones de Phalcon con más detalle en la documentación oficial.
Configurando loader.php
Veamos nuestro próximo archivo de configuración, loader.php
. El archivo loader.php
registra espacios de nombres con los directorios correspondientes a través del objeto \Phalcon\Loader
. Es aún más simple:
<?php $loader = new \Phalcon\Loader(); $loader->registerNamespaces( [ 'App\Services' => realpath(__DIR__ . '/../services/'), 'App\Controllers' => realpath(__DIR__ . '/../controllers/'), 'App\Models' => realpath(__DIR__ . '/../models/'), ] ); $loader->register();
Ahora todas las clases de estos espacios de nombres se cargarán automáticamente y estarán disponibles. Si desea agregar un nuevo espacio de nombres y directorio, simplemente agregue una línea en este archivo. También puede evitar el uso de espacios de nombres registrando directorios específicos o archivos específicos. Todas estas posibilidades se describen en la documentación del cargador de PhalconPHP.
Configuración del contenedor de inyección de dependencia
Como muchos otros marcos contemporáneos, Phalcon implementa un patrón de inyección de dependencia (DI). Los objetos se inicializarán en el contenedor DI y serán accesibles desde él. Asimismo, el contenedor DI está conectado al objeto de la aplicación, y será accesible desde todas las clases que heredan de la clase \Phalcon\DI\Injectable
, como nuestros controladores y servicios.
El patrón DI de Phalcon es muy poderoso. Considero que este componente es uno de los más importantes en este marco y le recomiendo que lea toda su documentación para comprender cómo funciona. Proporciona la clave para muchas de las funciones de Phalcon.
Echemos un vistazo a algunos de ellos. Nuestro archivo di.php
se verá así:
<?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;
Como puede ver, nuestro archivo de inyección de dependencia (DI) es un poco más complicado y hay algunos detalles que debe tener en cuenta. En primer lugar, considere la cadena de inicialización: $di = new \Phalcon\DI\FactoryDefault();
. Creamos un objeto FactoryDefault
que hereda \Phalcon\Di
(Phalcon le permite crear cualquier fábrica DI que desee). Según la documentación, FactoryDefault
“registra automáticamente todos los servicios proporcionados por el marco. Gracias a esto, el desarrollador no necesita registrar cada servicio de forma individual, lo que proporciona un marco de trabajo completo”. Esto significa que los servicios comunes como Request
y Response
estarán accesibles dentro de las clases del marco. Puede ver la lista completa de dichos servicios en la documentación del servicio de Phalcon.
La siguiente cosa importante es el proceso de configuración: hay varias formas de registrar algo en el contenedor DI, y todas ellas se describen completamente en la documentación de registro de PhalconPHP. En nuestro proyecto, sin embargo, usamos tres formas: una función anónima, una variable y una cadena.
La función anónima nos permite hacer muchas cosas mientras inicializamos la clase. Específicamente en este proyecto, primero anulamos un objeto de Response
para establecer el content-type
como JSON
para todas las respuestas del proyecto y luego inicializamos un adaptador de base de datos, usando nuestro objeto de configuración.
Como mencioné antes, este proyecto usa PostgreSQL. Si decide utilizar otro motor de base de datos, simplemente cambie el adaptador de la base de datos en la función db
set. Puede leer más sobre los adaptadores de base de datos disponibles y sobre la capa de la base de datos en la documentación de la base de datos de PhalconPHP.
Lo tercero a tener en cuenta es que registro una variable $config
que implementa el servicio \Phalcon\Config
. Si bien en realidad no se usa dentro de nuestro proyecto de ejemplo, he decidido incluirlo aquí porque es uno de los servicios más utilizados; otros proyectos pueden necesitar acceso a la configuración en casi todas partes.
Lo interesante final aquí es el propio método setShared
real. Llamar a esto hace que un servicio sea "compartido", lo que significa que comienza a actuar como un singleton. Según la documentación: "Una vez que el servicio se resuelve por primera vez, se devuelve la misma instancia cada vez que un consumidor recupera el servicio del contenedor".
Configurando routes.php
... o no
El último archivo incluido es routes.php
. Dejémoslo vacío por ahora, lo llenaremos junto con nuestros controladores.
Implementación de un núcleo RESTful
¿Qué hace que un proyecto web sea RESTful? Según Wikipedia, hay tres partes principales en una aplicación RESTful: - Una URL base - Un tipo de medio de Internet que define elementos de datos de transición de estado - Métodos HTTP estándar ( GET
, POST
, PUT
, DELETE
) y códigos de respuesta HTTP estándar (200, 403, 400, 500, etc.).
En nuestro proyecto, las URL base se colocarán en el archivo routes.php
y ahora se describirán otros puntos mencionados.
Recibiremos los datos de solicitud como application/x-www-form-urlencoded
y enviaremos los datos de respuesta como application/json
. Aunque no creo que sea una buena idea usar x-www-form-urlencoded
en una aplicación real (ya que tendrá dificultades para enviar estructuras de datos complejas y matrices asociativas con x-www-form-urlencoded
), he decidió implementar este estándar por razones de simplicidad.
Si recuerda, ya configuramos nuestro encabezado JSON de respuesta en nuestro archivo DI:
$di->setShared( 'response', function () { $response = new \Phalcon\Http\Response(); $response->setContentType('application/json', 'utf-8'); return $response; } );
Ahora tenemos que configurar códigos de respuesta y un formato de respuesta. El tutorial oficial sugiere que formemos respuestas JSON en todos los métodos, pero no creo que sea una buena idea. Es mucho más universal devolver los resultados del método del controlador como matrices y luego convertirlos en respuestas JSON estándar. También es más inteligente formar códigos de respuesta HTTP en un solo lugar dentro del proyecto; vamos a hacer esto en nuestro archivo index.php
.
Para hacer esto, vamos a utilizar la capacidad de Phalcon para ejecutar código antes y después del manejo de solicitudes con los $app->before()
y $app->after()
. Colocaremos una devolución de llamada en el $app->after()
para nuestro propósito:
// 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(); }
Aquí obtenemos el valor de retorno y transformamos la matriz a JSON. Si todo estuviera bien, pero el valor de retorno estuviera vacío (por ejemplo, si hubiésemos agregado correctamente un nuevo usuario), daríamos un código HTTP 204 y no enviaríamos ningún contenido. En todos los demás casos, lanzamos una excepción.
Manejo de excepciones
Uno de los aspectos más importantes de una aplicación RESTful son las respuestas correctas e informativas. Las aplicaciones de alta carga suelen ser grandes y pueden ocurrir errores de varios tipos en todas partes: errores de validación, errores de acceso, errores de conexión, errores inesperados, etc. Queremos transformar todos estos errores en códigos de respuesta HTTP unificados. Se puede hacer fácilmente con la ayuda de las excepciones.
En mi proyecto, he decidido usar dos tipos diferentes de excepciones: hay excepciones "locales": clases especiales heredadas de la clase \RuntimeException
, separadas por servicios, modelos, adaptadores, etc. (dicha división ayuda a tratar cada nivel del modelo MVC como uno separado), luego están las HttpExceptions
, heredadas de la clase AbstractHttpException
. Estas excepciones son coherentes con los códigos de respuesta HTTP, por lo que sus nombres son Http400Exception
, Http500Exception
, etc.
La clase AbstractHttpException
tiene tres propiedades: httpCode
, httpMessage
y appError
. Las dos primeras propiedades se anulan en sus herederos y contienen información de respuesta básica, como httpCode: 400
y httpMessage: Bad request
. La propiedad appError
es una matriz de la información detallada del error, incluida la descripción del error.

Nuestra versión final de index.php
detectará tres tipos de excepciones: AbstractHttpExceptions
, como se describe arriba; Excepciones de solicitud de Phalcon, que pueden ocurrir al analizar una solicitud; y todas las demás excepciones imprevistas. Todos ellos se convierten a un bonito formato JSON y se envían al cliente a través de la clase de respuesta estándar de Phalcon:
<?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(); }
Creación de modelos con Phalcon Dev Tools
Si usa un IDE contemporáneo, probablemente esté acostumbrado a resaltar y completar el código. Del mismo modo, en un marco típico de PHP, puede incluir una carpeta con un marco para ir a la declaración de una función con solo un clic. Dado que Phalcon es una extensión, no obtenemos esta opción automáticamente. Afortunadamente, existe una herramienta que llena este vacío llamada "Phalcon Dev Tools", que se puede instalar a través de Composer (si aún no sabe qué es, ahora es el momento de conocer este increíble administrador de paquetes). Las herramientas de desarrollo de Phalcon consisten en fragmentos de código para todas las clases y funciones en Phalcon, y proporcionan algunos generadores de código con versiones de consola y GUI, documentados en el sitio web de PhalconPHP. Estas herramientas pueden ayudar con la creación de todas las partes del patrón MVC, pero solo cubriremos la generación de modelos.
Bien, instalemos Phalcon Dev Tools a través de Composer. Nuestro archivo composer.json
se verá así:
{ "require": { "php": ">=5.6.0", "ext-phalcon": ">=3", "ext-pgsql": "*" }, "require-dev": { "phalcon/devtools": "3.*.*@dev" } }
Como puede ver, requerimos PHP 5.6, Phalcon 3 y la extensión pgsql
(que puede cambiar a la extensión de su base de datos o excluirla por completo).
Asegúrese de tener las versiones de extensión PHP, Phalcon y DB correctas, y ejecute composer:
$ composer install
El siguiente paso es crear nuestra base de datos. Es muy simple y consta solo de una tabla, users
. Aunque he incluido un archivo pg_dump
en el proyecto, aquí está el SQL en el dialecto de PostgreSQL:
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 );
Ahora que se creó la base de datos, podemos continuar con el proceso de generación del modelo. Phalcon Dev Tools utiliza una carpeta .phalcon
vacía para detectar si una aplicación es un proyecto de Phalcon, por lo que deberá crear esta carpeta vacía en la raíz de su proyecto. También utiliza algunas configuraciones del archivo de configuración que hemos creado: todas las variables almacenadas en la sección de la application
y el adapter
de la sección de la base de database
. Para generar nuestro modelo, necesitamos ejecutar el siguiente comando desde la carpeta raíz del proyecto:
$ php vendor/phalcon/devtools/phalcon.php model users --namespace="App\Models" --get-set
Si todos los pasos anteriores se han realizado correctamente, obtendrá un archivo de modelo de trabajo, Users.php
, en su carpeta de models
, ya colocado en un espacio de nombres con getters y setters como indica la línea de comando. El siguiente es el controlador.
Controladores y enrutamiento
Dado que nuestra aplicación solo CRUD (crea, lee, actualiza y elimina) usuarios, crearemos solo un controlador, el controlador de Users
con las siguientes operaciones:
- Agregar usuario
- Mostrar lista de usuarios
- Actualizar usuario
- Borrar usuario
Si bien los controladores se pueden crear con la ayuda de Phalcon Dev Tools, lo haremos manualmente e implementaremos AbstractController
y su hijo, UsersController
.
Crear AbstractController
es una buena decisión para Phalcon porque podemos colocar todas las clases necesarias que obtendremos de la inyección de dependencia en el bloque PHPDoc. Esto ayudará con la función de autocompletar IDE. También podemos programar algunas constantes de error que son comunes a todos los controladores potenciales.
Por ahora, nuestro controlador abstracto se verá así:
<?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; }
Solo una clase inyectable de Phalcon simple, como se especifica en la sintaxis extends
, nada más. A continuación, creemos el esqueleto de UsersController
:
<?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) { } }
Por el momento, es solo una clase con acciones vacías que eventualmente contendrá las solicitudes HTTP correspondientes.
Ahora es el momento de completar el archivo routes.php
. En las microaplicaciones de Phalcon, creamos colecciones, una para cada controlador, y agregamos todas las solicitudes manejadas como métodos get
, post
, put
, delete
, que toman un patrón de ruta y una función de procedimiento como argumentos. Tenga en cuenta que una función de procedimiento debe ser una función anónima o el nombre de un método de controlador. Así es como se ve nuestro archivo routes.php
:
<?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; } );
También establecemos un controlador de manejo y un prefijo URI. Para nuestro ejemplo, un URI se verá como http://article.dev/user/add
y debe ser una solicitud de post
. Si queremos cambiar los datos del usuario, el URI debe ser una solicitud de put
y se verá como http://article.dev/user/12
para cambiar los datos del usuario con una ID de 12
. También definimos un controlador de URL no encontrado, que genera un error. Para obtener más información, consulte la documentación de PhalconPHP para rutas en una aplicación de pila completa y para rutas en una microaplicación.
Pasemos al cuerpo del controlador, y específicamente al método addAction
(todos los demás son similares; puedes verlos en el código de la aplicación). Un método de controlador hace cinco cosas:
- Obtiene y valida los parámetros de la solicitud
- Prepara datos para el método de servicio.
- Llama al método de servicio
- Maneja excepciones
- Envía la respuesta
Recorramos cada paso, comenzando con la validación. Si bien Phalcon tiene un poderoso componente de validación, en este caso es mucho más conveniente validar los datos al estilo antiguo, por lo que nuestro bloque de validación se verá así:
$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'; }
Aquí verificamos si el parámetro de post
es una cadena que coincide con una expresión regular. Todos los valores se colocan en la matriz $data
, que luego se pasa a la clase UsersService
. Todos los errores se colocan en la matriz $errors
, que luego se agrega a una matriz de detalles de error dentro de Http400Exception
, donde se transformará en la respuesta detallada que se ve en index.php
:
Aquí está el código completo del método addAction
con toda su validación, que incluye una llamada al método createUser
en UsersService
(que aún no hemos creado):
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 **/ }
Como puede ver, manejamos dos excepciones conocidas en la última sección: user already exists
y unable to create user
debido a algún problema interno, como un error de conexión a la base de datos. De forma predeterminada, las excepciones desconocidas se generarán como un HTTP 500
(error interno del servidor). Aunque no proporcionamos ningún detalle al usuario final, se recomienda encarecidamente almacenar todos los detalles del error (incluido el seguimiento) en el registro.
Y, por favor, no olvides use
todas las clases necesarias, tomadas de otros espacios de nombres:
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;
Lógica de negocios
La última parte a crear es la lógica empresarial. Al igual que con los controladores, crearemos una clase de servicio abstracta:
<?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; }
La idea es completamente la misma que en el bloque del controlador, así que no lo comentaré. Aquí está el esqueleto de nuestra clase UsersService
:
<?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) { } }
Y el propio método createUser
:
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); } } }
Este método es tan fácil como puede ser. Simplemente creamos un nuevo objeto modelo, llamamos a sus setters (que devuelven el objeto en sí; esto nos permite hacer una cadena de llamadas) y lanzamos una ServiceException
en caso de error. ¡Eso es todo! Ahora podemos proceder a la prueba.
Pruebas
Ahora veamos los resultados usando Postman. Probemos primero algunos datos basura:
Solicitud:
POST http://article.dev/user/add login:1 password:1 first_name:Name last_name:Sourname
Respuesta (400: Solicitud incorrecta):
{ "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" } }
Eso se comprueba. Ahora para algunos datos correctos:
Solicitud:
POST http://article.dev/user/add login:user4 password:password4 first_name:Name last_name:Sourname
Respuesta (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.
Desventajas
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.