PhalconPHP: uma solução para APIs RESTful de alta carga
Publicados: 2022-03-11Suponha que você precise criar um projeto de alta carga baseado em uma estrutura PHP MVC. Você provavelmente usaria o cache sempre que possível. Talvez você construa o projeto em um único arquivo, ou talvez até escreva seu próprio framework MVC com funcionalidade mínima, ou reescreva algumas partes de outro framework. Embora, sim, isso funcione, é um pouco complicado, não é? Felizmente, há mais uma solução que torna a maioria dessas manipulações desnecessárias (exceto pelo cache, talvez), e essa solução é chamada de framework PhalconPHP.
O que é PhalconPHP?
PhalconPHP é um framework MVC para PHP escrito em C e fornecido como uma extensão PHP compilada. Isso é o que o torna um dos frameworks mais rápidos disponíveis (para ser completamente honesto, o mais rápido é o Yaf, mas é um micro framework e tem uma funcionalidade muito, muito mais limitada que o Phalcon). O PhalconPHP não precisa de nenhuma operação longa com arquivos PHP e não precisa ser interpretado a cada solicitação - ele é carregado na RAM uma vez quando seu servidor web é iniciado e consome muito poucos recursos.
Os frameworks MVC são considerados as melhores práticas em desenvolvimento web há muito tempo—agora é uma espécie de padrão profissional, então a maioria dos desenvolvedores web estão familiarizados com pelo menos um framework MVC para PHP: Symfony, Yii, Laravel, CodeIgniter, Zend Framework, etc. Cada um deles tem suas próprias vantagens e desvantagens, mas o que todos eles têm em comum? Todos eles são escritos em PHP e consistem em muitos arquivos PHP incluídos com uma enorme quantidade de lógica que deve ser executada pelo interpretador em cada solicitação, toda vez que seu código é executado. Embora isso proporcione grande transparência, pagamos com desempenho. Grandes quantidades de código e muitos arquivos incluídos custam muita memória e tempo, especialmente em PHP (já que é interpretado, não compilado). Sim, a situação melhorou muito no PHP 7, mas ainda há muito a ser melhorado, e o PhalconPHP traz essas melhorias para a mesa.
Vamos dar uma olhada em alguns benchmarks.
Referências PhalconPHP
Os benchmarks oficiais têm cinco anos – velhos demais para serem válidos agora, mas mesmo assim você pode ver dramaticamente o que distingue o PhalconPHP. Vamos olhar para algo mais novo. Em uma comparação de 2016, a Phalcon fica entre os cinco primeiros – um líder óbvio entre os frameworks profissionais, e concedendo apenas ao PHP bruto e alguns micro frameworks.
Então, Phalcon é rápido. O PHP bruto também é rápido, mas precisamos de todas as facilidades que um framework MVC tem a oferecer, e o Phalcon está à altura do desafio, incluindo componentes como:
- ORM
- Motor de modelo de volts
- Contêiner de injeção de dependência (DI)
- Cache
- Exploração madeireira
- Sistemas de roteamento
- Bloqueio de segurança
- Carregador automático
- Módulo de formulários
Isso é só para citar alguns. Para encurtar a história, o PhalconPHP tem tudo o que você precisa para construir um grande aplicativo corporativo, como uma API RESTful para um sistema de alta carga.
Mais uma coisa legal sobre o Phalcon é seu estilo minúsculo - basta comparar o Phalcon ORM e o enorme Doctrine 2.
Vamos dar uma olhada na criação de um projeto PhalconPHP.
Dois Tipos de Projetos Phalcon: Full-stack e Micro
Geralmente, existem dois tipos de frameworks MVC: frameworks full-stack (como Symfony, Yii) e micro frameworks (como Lumen, Slim, Silex).
Os frameworks full-stack são uma boa escolha para um grande projeto, pois fornecem mais funcionalidades, mas precisam de um pouco mais de qualificação e tempo para serem executados.
Micro frameworks permitem que você crie protótipos leves muito rapidamente, mas eles não têm funcionalidade e, portanto, geralmente é melhor evitar usá-los em projetos grandes. Uma vantagem dos micro frameworks, no entanto, é o seu desempenho. Eles geralmente são muito mais rápidos que os full-stack (por exemplo, o framework Yaf é inferior em desempenho apenas ao PHP bruto).
PhalconPHP suporta ambos: Você pode criar um full-stack ou um micro aplicativo. Melhor ainda, quando você desenvolve seu projeto no PhalconPHP como um micro aplicativo, você ainda tem acesso à maioria dos recursos poderosos do Phalcon e seu desempenho ainda permanece mais rápido do que como um aplicativo full-stack.
Em um trabalho anterior, minha equipe precisava construir um sistema RESTful de alta carga. Uma das coisas que fizemos foi comparar o desempenho do protótipo entre um aplicativo full-stack no Phalcon e um micro aplicativo Phalcon. Descobrimos que os microaplicativos do PhalconPHP tendiam a ser muito mais rápidos. Não posso mostrar nenhum benchmark por motivos de NDA, mas na minha opinião, se você quiser aproveitar ao máximo o desempenho do Phalcon, use microaplicativos. Embora seja menos conveniente codificar um micro aplicativo do que um full-stack, os microaplicativos do PhalconPHP ainda têm tudo o que você precisa para seu projeto e melhor desempenho. Para ilustrar, vamos escrever um micro aplicativo RESTful muito simples no Phalcon.
Construindo uma API RESTful
Quase todas as permutações de uma aplicação RESTful têm uma coisa em comum: uma entidade User
. Portanto, para nosso projeto de exemplo, criaremos um pequeno aplicativo REST para criar, ler, atualizar e excluir usuários (também conhecido como CRUD).
Você pode ver este projeto totalmente concluído no meu repositório GitLab. Existem duas ramificações lá porque eu decidi dividir este projeto em duas partes: a primeira ramificação, master
, contém apenas a funcionalidade básica sem nenhum recurso específico do PhalconPHP, enquanto a segunda, logging-and-cache
, contém a funcionalidade de logging e cache do Phalcon. Você pode compará-los e ver como é fácil implementar tais funções no Phalcon.
Instalação
Não vou falar sobre a instalação: você pode usar qualquer banco de dados, qualquer sistema operacional e qualquer servidor web que desejar. Está bem descrito na documentação oficial de instalação, portanto, basta seguir as instruções, dependendo do seu sistema operacional.
As notas de instalação do servidor Web também estão disponíveis na documentação oficial do Phalcon.
Observe que sua versão do PHP não deve ser inferior a 5.6.
Eu uso Ubuntu 16.10, PostgreSQL 9.5.6, Nginx 1.10.0, PHP 7 e Phalcon 3.0. Incluí um exemplo de configuração do Nginx e um arquivo de despejo do PostgreSQL no projeto, portanto, sinta-se à vontade para usá-los. Se você preferir outra configuração, não será difícil alterá-la.
Estrutura e configuração do projeto
Em primeiro lugar, crie a estrutura inicial do projeto.
Enquanto o Phalcon permite que você use qualquer estrutura que desejar, a estrutura que escolhi para este exercício implementa parcialmente um padrão MVC. Não temos views porque é um projeto RESTful, mas temos controllers e models, cada um com sua própria pasta e serviços. Serviços são as classes que implementam a lógica de negócios do projeto, dividindo a parte “modelo” do MVC em duas partes: modelos de dados (que se comunicam com o banco de dados) e modelos de lógica de negócios.
index.php
, localizado na pasta public
, é um arquivo bootstrap que carrega todas as partes e configurações necessárias. Observe que todos os nossos arquivos de configuração são colocados na pasta config
. Poderíamos colocá-los no arquivo bootstrap (e esta é a maneira mostrada na documentação oficial), mas, na minha opinião, isso fica ilegível em grandes projetos, então prefiro a separação de pastas desde o início.
Criando index.php
Nossa primeira passagem no index.php
carregará as classes de configuração e autoload, então inicializará as rotas, um container de injeção de dependência e o micro aplicativo PhalconPHP. Em seguida, ele passará o controle para esse núcleo de microaplicativo, que tratará as solicitações de acordo com as rotas, executará a lógica de negócios e retornará os resultados.
Vamos dar uma olhada no 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 }
Configurando um objeto \Phalcon\Config
Existem várias maneiras de armazenar arquivos de configuração no Phalcon:
- Um arquivo YAML
- Um arquivo JSON
- Um arquivo INI
- Uma matriz PHP
Armazenar sua configuração em um array PHP é a opção mais rápida e, como estamos escrevendo um aplicativo de alta carga e não precisamos diminuir nosso desempenho, é isso que faremos. Especificamente, usaremos um objeto \Phalcon\Config
para carregar nossas opções de configuração no projeto. Teremos um objeto de configuração muito curto:
<?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 arquivo contém duas configurações básicas, uma para o banco de dados e outra para o aplicativo. Obviamente, a configuração do banco de dados é usada para conectar-se ao banco de dados, e quanto ao array de application
, precisaremos dele mais tarde, pois é usado pelas ferramentas do sistema do Phalcon. Você pode ler sobre as configurações do Phalcon com mais detalhes na documentação oficial.
Configurando loader.php
Vejamos nosso próximo arquivo de configuração, loader.php
. O arquivo loader.php
registra namespaces com diretórios correspondentes por meio do objeto \Phalcon\Loader
. É ainda mais simples:
<?php $loader = new \Phalcon\Loader(); $loader->registerNamespaces( [ 'App\Services' => realpath(__DIR__ . '/../services/'), 'App\Controllers' => realpath(__DIR__ . '/../controllers/'), 'App\Models' => realpath(__DIR__ . '/../models/'), ] ); $loader->register();
Agora todas as classes desses namespaces serão carregadas e disponibilizadas automaticamente. Se você quiser adicionar um novo namespace e diretório, basta adicionar uma linha neste arquivo. Você também pode evitar o uso de namespaces registrando diretórios ou arquivos específicos. Todas essas possibilidades estão descritas na documentação do carregador PhalconPHP.
Configurando o contêiner de injeção de dependência
Como muitos outros frameworks contemporâneos, o Phalcon implementa um padrão de injeção de dependência (DI). Os objetos serão inicializados no contêiner DI e poderão ser acessados a partir dele. Da mesma forma, o container DI é conectado ao objeto da aplicação, e estará acessível a partir de todas as classes que herdam da classe \Phalcon\DI\Injectable
, como nossos controllers e services.
O padrão DI da Phalcon é muito poderoso. Considero este componente um dos mais importantes deste framework e recomendo fortemente que você leia toda a sua documentação para entender como ele funciona. Ele fornece a chave para muitas das funções do Phalcon.
Vamos dar uma olhada em alguns deles. Nosso arquivo di.php
ficará assim:
<?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 você pode ver, nosso arquivo de injeção de dependência (DI) é um pouco mais complicado e há alguns detalhes que você deve conhecer. Primeiro, considere a string de inicialização: $di = new \Phalcon\DI\FactoryDefault();
. Criamos um objeto FactoryDefault
que herda \Phalcon\Di
(Phalcon permite que você crie qualquer fábrica de DI que desejar). De acordo com a documentação, FactoryDefault
“registra automaticamente todos os serviços fornecidos pelo framework. Graças a isso, o desenvolvedor não precisa registrar cada serviço individualmente fornecendo um framework full stack.” Isso significa que serviços comuns, como Request
e Response
, estarão acessíveis nas classes de estrutura. Você pode ver a lista completa de tais serviços na documentação do serviço Phalcon.
A próxima coisa importante é o processo de configuração: Existem várias maneiras de registrar algo no contêiner DI, e todas elas são descritas completamente na documentação de registro do PhalconPHP. Em nosso projeto, porém, usamos três maneiras: uma função anônima, uma variável e uma string.
A função anônima nos permite fazer muitas coisas enquanto inicializamos a classe. Neste projeto especificamente, primeiro substituímos um objeto Response
para definir o content-type
como JSON
para todas as respostas do projeto e, em seguida, inicializamos um adaptador de banco de dados, usando nosso objeto de configuração.
Como mencionei antes, este projeto usa o PostgreSQL. Se você decidir usar outro mecanismo de banco de dados, basta alterar o adaptador de banco de dados na função db
set. Você pode ler mais sobre os adaptadores de banco de dados disponíveis e sobre a camada de banco de dados na documentação do banco de dados do PhalconPHP.
A terceira coisa a ser observada é que eu registro uma variável $config
que implementa o serviço \Phalcon\Config
. Embora não seja realmente usado em nosso projeto de exemplo, decidi incluí-lo aqui porque é um dos serviços mais usados; outros projetos podem precisar de acesso à configuração em quase todos os lugares.
A última coisa interessante aqui é o próprio método setShared
. Chamar isso torna um serviço “compartilhado”, o que significa que ele começa a agir como um singleton. De acordo com a documentação: “Uma vez que o serviço é resolvido pela primeira vez, a mesma instância dele é retornada toda vez que um consumidor recupera o serviço do contêiner”.
Configurando routes.php
…ou não
O último arquivo incluído é o routes.php
. Vamos deixá-lo vazio por enquanto - vamos preenchê-lo junto com nossos controladores.
Implementando um núcleo RESTful
O que torna um projeto web RESTful? De acordo com a Wikipedia, existem três partes principais em um aplicativo RESTful: - Uma URL base - Um tipo de mídia da Internet que define os elementos de dados de transição de estado - Métodos HTTP padrão ( GET
, POST
, PUT
, DELETE
) e códigos de resposta HTTP padrão (200, 403, 400, 500, etc).
Em nosso projeto, as URLs base serão colocadas no arquivo routes.php
e outros pontos mencionados serão descritos agora.
Receberemos os dados da solicitação como application/x-www-form-urlencoded
e enviaremos os dados de resposta como application/json
. Embora eu não acredite que seja uma boa ideia usar x-www-form-urlencoded
em um aplicativo real (já que você terá dificuldades para enviar estruturas de dados complexas e matrizes associativas com x-www-form-urlencoded
), eu decidiu implementar este padrão para simplificar.
Se você se lembra, já definimos nosso cabeçalho JSON de resposta em nosso arquivo DI:
$di->setShared( 'response', function () { $response = new \Phalcon\Http\Response(); $response->setContentType('application/json', 'utf-8'); return $response; } );
Agora temos que configurar os códigos de resposta e um formato de resposta. O tutorial oficial sugere que formemos respostas JSON em todos os métodos, mas não acho que seja uma boa ideia. É muito mais universal retornar os resultados do método do controlador como matrizes e depois convertê-los em respostas JSON padrão. Também é mais sensato formar códigos de resposta HTTP em um único local dentro do projeto; vamos fazer isso em nosso arquivo index.php
.
Para fazer isso, vamos utilizar a habilidade do Phalcon de executar código antes e depois do tratamento de requisições com os métodos $app->before()
e $app->after()
. Vamos colocar um callback no método $app->after()
para nosso 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(); }
Aqui obtemos o valor de retorno e transformamos o array em JSON. Se tudo estivesse OK, mas o valor de retorno estivesse vazio (por exemplo, se tivéssemos adicionado com sucesso um novo usuário), daríamos um código HTTP 204 e não enviaríamos nenhum conteúdo. Em todos os outros casos, lançamos uma exceção.
Tratamento de exceções
Um dos aspectos mais importantes de uma aplicação RESTful são as respostas corretas e informativas. Aplicativos de alta carga geralmente são grandes e erros de vários tipos podem ocorrer em todos os lugares: erros de validação, erros de acesso, erros de conexão, erros inesperados etc. Queremos transformar todos esses erros em códigos de resposta HTTP unificados. Isso pode ser feito facilmente com a ajuda das exceções.
Em meu projeto, decidi usar dois tipos diferentes de exceções: existem exceções “locais” - classes especiais herdadas da classe \RuntimeException
, separadas por serviços, modelos, adaptadores e assim por diante (essa divisão ajuda a tratar cada nível do modelo MVC como um separado)- então existem HttpExceptions
, herdadas da classe AbstractHttpException
. Essas exceções são consistentes com os códigos de resposta HTTP, portanto, seus nomes são Http400Exception
, Http500Exception
e assim por diante.
A classe AbstractHttpException
tem três propriedades: httpCode
, httpMessage
e appError
. As duas primeiras propriedades são substituídas em seus herdeiros e contêm informações básicas de resposta, como httpCode: 400
e httpMessage: Bad request
. A propriedade appError
é uma matriz das informações detalhadas do erro, incluindo a descrição do erro.

Nossa versão final do index.php
irá capturar três tipos de exceções: AbstractHttpExceptions
, conforme descrito acima; Exceções de solicitação do Phalcon, que podem ocorrer durante a análise de uma solicitação; e todas as outras exceções imprevistas. Todos eles são convertidos para um formato JSON bonito e enviados ao cliente por meio da classe padrão do Phalcon Response:
<?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(); }
Criando Modelos com o Phalcon Dev Tools
Se você usa um IDE contemporâneo, provavelmente está acostumado a codificar realce e conclusão. Da mesma forma, em um framework PHP típico, você pode incluir uma pasta com um framework para acessar uma declaração de função com apenas um clique. Visto que o Phalcon é uma extensão, não temos essa opção automaticamente. Felizmente, existe uma ferramenta que preenche essa lacuna chamada “Phalcon Dev Tools”, que pode ser instalada via Composer (se você ainda não sabe o que é, chegou a hora de conhecer esse incrível gerenciador de pacotes). O Phalcon Dev Tools consiste em stubs de código para todas as classes e funções no Phalcon, e fornece alguns geradores de código com versões de console e GUI, documentados no site do PhalconPHP. Essas ferramentas podem ajudar na criação de todas as partes do padrão MVC, mas abordaremos apenas a geração do modelo.
OK, vamos instalar o Phalcon Dev Tools via Composer. Nosso arquivo composer.json
ficará assim:
{ "require": { "php": ">=5.6.0", "ext-phalcon": ">=3", "ext-pgsql": "*" }, "require-dev": { "phalcon/devtools": "3.*.*@dev" } }
Como você pode ver, exigimos PHP 5.6, Phalcon 3 e a extensão pgsql
(que você pode alterar para sua extensão de banco de dados ou excluir completamente).
Certifique-se de ter as versões de extensão PHP, Phalcon e DB corretas e execute composer:
$ composer install
O próximo passo é criar nosso banco de dados. É muito simples e consiste apenas em uma tabela, users
. Embora eu tenha incluído um arquivo pg_dump
no projeto, aqui está o SQL no dialeto 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 );
Agora que o banco de dados está criado, podemos prosseguir para o processo de geração do modelo. O Phalcon Dev Tools usa uma pasta .phalcon
vazia para detectar se um aplicativo é um projeto Phalcon, então você terá que criar essa pasta vazia na raiz do seu projeto. Ele também usa algumas configurações do arquivo de configuração que criamos - todas as variáveis armazenadas na seção do application
e o adapter
da seção do database
de dados. Para gerar nosso modelo, precisamos executar o seguinte comando na pasta raiz do projeto:
$ php vendor/phalcon/devtools/phalcon.php model users --namespace="App\Models" --get-set
Se todas as etapas anteriores foram feitas corretamente, você obterá um arquivo de modelo funcional, Users.php
, em sua pasta de models
, já colocado em um namespace com getters e setters conforme a linha de comando indicada. Em seguida é o controlador.
Controladores e roteamento
Como nosso aplicativo apenas CRUDs (cria, lê, atualiza e exclui) usuários, criaremos apenas um controlador, o controlador Users
com as seguintes operações:
- Adicionar usuário
- Mostrar lista de usuários
- Atualizar usuário
- Deletar usuário
Embora os controladores possam ser criados com a ajuda do Phalcon Dev Tools, faremos isso manualmente e implementaremos AbstractController
e seu filho, UsersController
.
Criar o AbstractController
é uma boa decisão para o Phalcon porque podemos colocar todas as classes necessárias que obteremos da injeção de dependência no bloco PHPDoc. Isso ajudará com a função de preenchimento automático do IDE. Também podemos programar algumas constantes de erro que são comuns a todos os potenciais controladores.
Por enquanto, nosso controlador abstrato ficará assim:
<?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; }
Apenas uma classe injetável extends
simples, conforme especificado pela sintaxe de extensões, nada mais. Em seguida, vamos criar o esqueleto 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) { } }
No momento, é apenas uma classe com ações vazias que eventualmente conterão solicitações HTTP correspondentes.
Agora é hora de preencher o arquivo routes.php
. Nas microaplicações Phalcon criamos coleções, uma para cada controlador, e adicionamos todas as requisições tratadas como métodos get
, post
, put
, delete
, que recebem um padrão de rota e uma função de procedimento como argumentos. Observe que uma função de procedimento deve ser uma função anônima ou um nome de método do controlador. Veja como nosso arquivo routes.php
se parece:
<?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; } );
Também definimos um controlador de manipulação e um prefixo de URI. Para nosso exemplo, um URI será semelhante a http://article.dev/user/add
e deve ser uma solicitação de post
. Se quisermos alterar os dados do usuário, o URI deve ser uma solicitação put
e será semelhante a http://article.dev/user/12
para alterar os dados do usuário com um ID de 12
. Também definimos um manipulador de URL não encontrado, que gera um erro. Para obter mais informações, consulte a documentação do PhalconPHP para rotas em um aplicativo de pilha completa e para rotas em um microaplicativo.
Vamos passar para o corpo do controlador, e especificamente para o método addAction
(todos os outros são semelhantes; você pode vê-los no código da aplicação). Um método de controlador faz cinco coisas:
- Obtém e valida parâmetros de solicitação
- Prepara dados para o método de serviço
- Chama o método de serviço
- Lida com exceções
- Envia a resposta
Vamos percorrer cada etapa, começando com a validação. Embora o Phalcon tenha um componente de validação poderoso, é muito mais conveniente validar os dados de maneira antiga neste caso, portanto, nosso bloco de validação ficará assim:
$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'; }
Aqui verificamos se o parâmetro post
é uma string que corresponde a uma expressão regular. Todos os valores são colocados no array $data
, que é então passado para a classe UsersService
. Todos os erros são colocados no array $errors
, que então é adicionado ao array de detalhes do erro dentro de Http400Exception
, onde será transformado na resposta detalhada vista em index.php
:
Aqui está o código completo do método addAction
com toda a sua validação, que inclui uma chamada para o método createUser
em UsersService
(que ainda não criamos):
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 você pode ver, tratamos de duas exceções conhecidas nessa última seção: user already exists
e unable to create user
devido a algum problema interno, como um erro de conexão com o banco de dados. Por padrão, exceções desconhecidas serão lançadas como HTTP 500
(erro interno do servidor). Embora não forneçamos detalhes ao usuário final, é altamente recomendável armazenar todos os detalhes do erro (incluindo rastreamento) no log.
E, por favor, não se esqueça de use
todas as classes necessárias, emprestadas dos outros 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;
Logíca de negócios
A última parte a ser criada é a lógica de negócios. Assim como com os controladores, criaremos uma classe de serviço abstrata:
<?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; }
A ideia é completamente a mesma do bloco do controlador, então não vou comentar. Aqui está o esqueleto da nossa classe 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) { } }
E o próprio 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 é tão fácil quanto pode ser. Nós apenas criamos um novo objeto de modelo, chamamos seus setters (que retorna o próprio objeto; isso nos permite fazer uma cadeia de chamadas) e lançamos um ServiceException
no caso de um erro. É isso! Agora podemos prosseguir com os testes.
Teste
Agora vamos ver os resultados usando o Postman. Vamos testar alguns dados de lixo primeiro:
Solicitação:
POST http://article.dev/user/add login:1 password:1 first_name:Name last_name:Sourname
Resposta (400: Solicitação inválida):
{ "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" } }
Isso confere. Agora alguns dados corretos:
Solicitação:
POST http://article.dev/user/add login:user4 password:password4 first_name:Name last_name:Sourname
Resposta (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.
Desvantagens
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.