PhalconPHP : une solution pour les API RESTful à forte charge

Publié: 2022-03-11

Supposons que vous ayez besoin de créer un projet à charge élevée basé sur un framework PHP MVC. Vous utiliserez probablement la mise en cache dans la mesure du possible. Vous pourriez peut-être construire le projet dans un seul fichier, ou peut-être même écrire votre propre framework MVC avec un minimum de fonctionnalités, ou réécrire certaines parties d'un autre framework. Bien que, oui, cela fonctionne, c'est un peu délicat, n'est-ce pas ? Heureusement, il existe une autre solution qui rend la plupart de ces manipulations inutiles (sauf pour le cache, peut-être), et cette solution s'appelle le framework PhalconPHP.

Qu'est-ce que PhalconPHP ?

PhalconPHP est un framework MVC pour PHP écrit en C et fourni sous la forme d'une extension PHP compilée. C'est ce qui en fait l'un des frameworks les plus rapides disponibles (pour être tout à fait honnête, le plus rapide est Yaf, mais c'est un micro framework et ses fonctionnalités sont beaucoup, beaucoup plus limitées que Phalcon). PhalconPHP n'a pas besoin de longues opérations avec les fichiers PHP et il n'a pas besoin d'être interprété à chaque requête - il est chargé dans la RAM une fois au démarrage de votre serveur Web et consomme très peu de ressources.

Les frameworks MVC sont depuis longtemps considérés comme les meilleures pratiques en matière de développement Web. Il s'agit désormais d'une sorte de norme professionnelle. La plupart des développeurs Web connaissent donc au moins un framework MVC pour PHP : Symfony, Yii, Laravel, CodeIgniter, Zend. Cadre, etc. Ils ont chacun leurs avantages et leurs inconvénients, mais qu'ont-ils en commun ? Tous sont écrits en PHP et consistent en de nombreux fichiers PHP inclus avec une énorme quantité de logique qui doit être exécutée par l'interpréteur à chaque requête, à chaque fois que votre code s'exécute. Bien que cela permette une grande transparence, nous payons avec la performance. De grandes quantités de code et de nombreux fichiers inclus coûtent beaucoup de mémoire et de temps, en particulier en PHP (puisqu'il est interprété et non compilé). Oui, la situation s'est bien améliorée dans PHP 7, mais il reste encore beaucoup à améliorer, et PhalconPHP apporte ces améliorations à la table.

Jetons un coup d'œil à quelques repères.

Benchmarks PhalconPHP

Les repères officiels datent de cinq ans, trop vieux pour être valides maintenant, mais même alors, vous pouvez voir de façon spectaculaire ce qui distingue PhalconPHP. Regardons quelque chose de plus récent. Dans une comparaison de 2016, Phalcon se classe parmi les cinq premiers, un leader évident parmi les frameworks professionnels, et ne concédant qu'au PHP brut et à certains micro-frameworks.

Benchmarks PhalconPHP

Ainsi, Phalcon est rapide. Le PHP brut est également rapide, mais nous avons besoin de toutes les fonctionnalités qu'un framework MVC peut offrir, et Phalcon relève le défi, y compris des composants tels que :

  • ORM
  • Moteur de modèle Volt
  • Conteneur d'injection de dépendance (DI)
  • Mise en cache
  • Enregistrement
  • Systèmes de routage
  • Bloc de sécurité
  • Chargeur automatique
  • Module Formulaires

C'est juste pour n'en nommer que quelques-uns. Pour faire court, PhalconPHP a tout ce dont vous avez besoin pour créer une grande application d'entreprise telle qu'une API RESTful pour un système à forte charge.

Une autre bonne chose à propos de Phalcon est son style minuscule - comparez simplement Phalcon ORM et l'énorme Doctrine 2.

Jetons un coup d'œil à la création d'un projet PhalconPHP.

Deux types de projets Phalcon : Full-stack et Micro

Généralement, il existe deux types de frameworks MVC : les frameworks full-stack (comme Symfony, Yii) et les micro frameworks (comme Lumen, Slim, Silex).

Les frameworks full-stack sont un bon choix pour un gros projet car ils offrent plus de fonctionnalités, mais ils ont besoin d'un peu plus de qualification et de temps pour s'exécuter.

Les micro-frameworks permettent de créer très rapidement des prototypes légers, mais ils manquent de fonctionnalités, et il est donc généralement préférable d'éviter de les utiliser pour de gros projets. Un avantage des micro-frameworks, cependant, est leur performance. Ils sont généralement beaucoup plus rapides que ceux à pile complète (par exemple, le framework Yaf n'a de performances inférieures qu'au PHP brut).

PhalconPHP prend en charge les deux : vous pouvez soit créer une pile complète, soit une micro-application. Mieux encore, lorsque vous développez votre projet dans PhalconPHP en tant que micro-application, vous avez toujours accès à la plupart des fonctionnalités puissantes de Phalcon et ses performances restent toujours plus rapides qu'en tant qu'application full-stack.

Lors d'un précédent emploi, mon équipe avait besoin de créer un système RESTful à charge élevée. L'une des choses que nous avons faites a été de comparer les performances d'un prototype entre une application full-stack dans Phalcon et une micro-application Phalcon. Nous avons constaté que les micro-applications de PhalconPHP avaient tendance à être beaucoup plus rapides. Je ne peux pas vous montrer de benchmarks pour des raisons NDA, mais à mon avis, si vous voulez tirer le meilleur parti des performances de Phalcon, utilisez des micro-applications. Bien qu'il soit moins pratique de coder une micro-application qu'une pile complète, les micro-applications de PhalconPHP ont toujours tout ce dont vous pourriez avoir besoin pour votre projet et de meilleures performances. Pour illustrer cela, écrivons une micro-application RESTful très simple dans Phalcon.

Construire une API RESTful

Presque toutes les permutations d'une application RESTful ont une chose en commun : une entité User . Ainsi, pour notre exemple de projet, nous allons créer une petite application REST pour créer, lire, mettre à jour et supprimer des utilisateurs (également appelée CRUD).

Vous pouvez voir ce projet entièrement terminé sur mon dépôt GitLab. Il y a deux branches ici parce que j'ai décidé de diviser ce projet en deux parties : la première branche, master , ne contient que les fonctionnalités de base sans aucune fonctionnalité PhalconPHP spécifique, tandis que la seconde, logging-and-cache , contient les fonctionnalités de journalisation et de mise en cache de Phalcon. Vous pouvez les comparer et voir à quel point il est facile d'implémenter de telles fonctions dans Phalcon.

Installation

Je ne vais pas revenir sur l'installation : vous pouvez utiliser n'importe quelle base de données, n'importe quel système d'exploitation et n'importe quel serveur Web de votre choix. Il est bien décrit dans la documentation d'installation officielle, il vous suffit donc de suivre les instructions en fonction de votre système d'exploitation.

Les notes d'installation du serveur Web sont également disponibles dans la documentation officielle de Phalcon.

Notez que votre version de PHP ne doit pas être inférieure à 5.6.

J'utilise Ubuntu 16.10, PostgreSQL 9.5.6, Nginx 1.10.0, PHP 7 et Phalcon 3.0. J'ai inclus un exemple de configuration Nginx et un fichier de vidage PostgreSQL dans le projet, alors n'hésitez pas à les utiliser. Si vous préférez une autre configuration, il ne sera pas difficile de la changer.

Structure et configuration du projet

Tout d'abord, créez la structure initiale du projet.

Bien que Phalcon vous permette d'utiliser n'importe quelle structure, la structure que j'ai choisie pour cet exercice implémente en partie un modèle MVC. Nous n'avons pas de vues car il s'agit d'un projet RESTful, mais nous avons des contrôleurs et des modèles, chacun avec son propre dossier et ses propres services. Les services sont les classes qui implémentent la logique métier du projet, divisant la partie « modèle » de MVC en deux parties : les modèles de données (qui communiquent avec la base de données) et les modèles de logique métier.

index.php , situé dans le dossier public , est un fichier d'amorçage qui charge toutes les parties et la configuration nécessaires. Notez que tous nos fichiers de configuration sont placés dans le dossier config . Nous pourrions les mettre dans le fichier bootstrap (et c'est la manière indiquée dans la documentation officielle), mais, à mon avis, cela devient illisible dans les gros projets, donc je préfère la séparation des dossiers dès le début.

Création index.php

Notre premier passage sur index.php chargera les classes de configuration et de chargement automatique, puis initialisera les routes, un conteneur d'injection de dépendances et la micro application PhalconPHP. Il passera ensuite le contrôle à ce noyau de micro-application, qui traitera les demandes en fonction des routes, exécutera la logique métier et retournera les résultats.

Jetons un œil au 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 }

Configuration d'un objet \Phalcon\Config

Il existe plusieurs manières de stocker les fichiers de configuration dans Phalcon :

  • Un fichier YAML
  • Un fichier JSON
  • Un fichier INI
  • Un tableau PHP

Stocker votre configuration dans un tableau PHP est l'option la plus rapide, et puisque nous écrivons une application à charge élevée et que nous n'avons pas besoin de ralentir nos performances, c'est ce que nous allons faire. Plus précisément, nous utiliserons un objet \Phalcon\Config pour charger nos options de configuration dans le projet. Nous aurons un objet de configuration très court :

 <?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' => "/", ], ] );

Ce fichier contient deux configurations de base, une pour la base de données et une pour l'application. Évidemment, la configuration de la base de données est utilisée pour se connecter à la base de données, et quant au tableau d' application , nous en aurons besoin plus tard car il est utilisé par les outils système de Phalcon. Vous pouvez en savoir plus sur les configurations Phalcon dans la documentation officielle.

Configuration loader.php

Regardons notre prochain fichier de configuration, loader.php . Le fichier loader.php enregistre les espaces de noms avec les répertoires correspondants via l'objet \Phalcon\Loader . C'est encore plus 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();

Désormais, toutes les classes de ces espaces de noms seront automatiquement chargées et disponibles. Si vous souhaitez ajouter un nouvel espace de noms et un nouveau répertoire, ajoutez simplement une ligne dans ce fichier. Vous pouvez également éviter d'utiliser des espaces de noms en enregistrant des répertoires spécifiques ou des fichiers spécifiques. Toutes ces possibilités sont décrites dans la documentation du loader PhalconPHP.

Configuration du conteneur d'injection de dépendances

Comme beaucoup d'autres frameworks contemporains, Phalcon implémente un modèle d'injection de dépendance (DI). Les objets seront initialisés dans le conteneur DI et seront accessibles depuis celui-ci. De même, le conteneur DI est connecté à l'objet application, et il sera accessible depuis toutes les classes qui héritent de la classe \Phalcon\DI\Injectable , comme nos contrôleurs et nos services.

Le modèle DI de Phalcon est très puissant. Je considère ce composant comme l'un des plus importants dans ce framework et je vous recommande vivement de lire l'intégralité de sa documentation pour comprendre son fonctionnement. Il fournit la clé de nombreuses fonctions de Phalcon.

Jetons un coup d'œil à quelques-uns d'entre eux. Notre fichier di.php ressemblera à ceci :

 <?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;

Comme vous pouvez le voir, notre fichier d'injection de dépendances (DI) est un peu plus compliqué et il y a quelques détails dont vous devez être conscient. Tout d'abord, considérez la chaîne d'initialisation : $di = new \Phalcon\DI\FactoryDefault(); . Nous créons un objet FactoryDefault qui hérite de \Phalcon\Di (Phalcon vous permet de créer n'importe quelle usine DI que vous voulez). Selon la documentation, FactoryDefault « enregistre automatiquement tous les services fournis par le framework. Grâce à cela, le développeur n'a pas besoin d'enregistrer chaque service individuellement, fournissant un cadre de pile complet. Cela signifie que les services communs tels que Request et Response seront accessibles dans les classes du framework. Vous pouvez consulter la liste complète de ces services dans la documentation des services Phalcon.

La prochaine chose importante est le processus de configuration : il existe plusieurs façons d'enregistrer quelque chose dans le conteneur DI, et toutes sont complètement décrites dans la documentation d'enregistrement de PhalconPHP. Dans notre projet, cependant, nous utilisons trois méthodes : une fonction anonyme, une variable et une chaîne.

La fonction anonyme nous permet de faire beaucoup de choses lors de l'initialisation de la classe. Dans ce projet en particulier, nous remplaçons d'abord un objet Response pour définir le content-type JSON pour toutes les réponses du projet, puis initialisons un adaptateur de base de données à l'aide de notre objet de configuration.

Comme je l'ai mentionné précédemment, ce projet utilise PostgreSQL. Si vous décidez d'utiliser un autre moteur de base de données, changez simplement l'adaptateur de base de données dans la fonction db set. Vous pouvez en savoir plus sur les adaptateurs de base de données disponibles et sur la couche de base de données dans la documentation de la base de données de PhalconPHP.

La troisième chose à noter est que j'enregistre une variable $config qui implémente le service \Phalcon\Config . Bien qu'il ne soit pas réellement utilisé dans notre exemple de projet, j'ai décidé de l'inclure ici car c'est l'un des services les plus couramment utilisés. d'autres projets peuvent avoir besoin d'accéder à la configuration presque partout.

La dernière chose intéressante ici est la méthode setShared elle-même. Appeler cela rend un service "partagé", ce qui signifie qu'il commence à agir comme un singleton. Selon la documentation : "Une fois que le service est résolu pour la première fois, la même instance de celui-ci est renvoyée chaque fois qu'un consommateur récupère le service à partir du conteneur."

Configurer routes.php …ou pas

Le dernier fichier inclus est routes.php . Laissons-le vide pour l'instant, nous le remplirons avec nos contrôleurs.

Implémentation d'un noyau RESTful

Qu'est-ce qui rend un projet Web RESTful ? Selon Wikipedia, une application RESTful comporte trois parties principales : - Une URL de base - Un type de média Internet qui définit les éléments de données de transition d'état - Des méthodes HTTP standard ( GET , POST , PUT , DELETE ) et des codes de réponse HTTP standard (200, 403, 400, 500, etc.).

Dans notre projet, les URL de base seront placées dans le fichier routes.php et les autres points mentionnés seront décrits maintenant.

Nous recevrons les données de demande en tant que application/x-www-form-urlencoded et enverrons les données de réponse en tant que application/json . Bien que je ne pense pas que ce soit une bonne idée d'utiliser x-www-form-urlencoded dans une application réelle (puisque vous aurez du mal à envoyer des structures de données complexes et des tableaux associatifs avec x-www-form-urlencoded ), j'ai décidé d'implémenter cette norme par souci de simplicité.

Si vous vous en souvenez, nous avons déjà défini notre en-tête JSON de réponse dans notre fichier DI :

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

Nous devons maintenant configurer des codes de réponse et un format de réponse. Le tutoriel officiel suggère que nous formions des réponses JSON dans chaque méthode, mais je ne pense pas que ce soit une bonne idée. Il est beaucoup plus universel de renvoyer les résultats de la méthode du contrôleur sous forme de tableaux, puis de les convertir en réponses JSON standard. Il est également plus judicieux de former des codes de réponse HTTP à un seul endroit dans le projet ; nous allons le faire dans notre fichier index.php .

Pour ce faire, nous allons utiliser la capacité de Phalcon à exécuter du code avant et après la gestion des requêtes avec les $app->before() et $app->after() . Nous allons placer un rappel dans la $app->after() pour notre objectif :

 // 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(); }

Ici, nous obtenons la valeur de retour et transformons le tableau en JSON. Si tout allait bien, mais que la valeur de retour était vide (par exemple, si nous avions réussi à ajouter un nouvel utilisateur), nous donnerions un code HTTP 204 et n'enverrions aucun contenu. Dans tous les autres cas, nous levons une exception.

Gestion des exceptions

L'un des aspects les plus importants d'une application RESTful est des réponses correctes et informatives. Les applications à forte charge sont généralement volumineuses et des erreurs de différents types peuvent se produire partout : erreurs de validation, erreurs d'accès, erreurs de connexion, erreurs inattendues, etc. Nous souhaitons transformer toutes ces erreurs en codes de réponse HTTP unifiés. Cela peut être facilement fait à l'aide des exceptions.

Dans mon projet, j'ai décidé d'utiliser deux types d'exceptions différents : il existe des exceptions "locales" - des classes spéciales héritées de la classe \RuntimeException , séparées par des services, des modèles, des adaptateurs, etc. (une telle division permet de traiter chaque niveau du modèle MVC en tant que modèle séparé) - puis il y a HttpExceptions , hérité de la classe AbstractHttpException . Ces exceptions sont cohérentes avec les codes de réponse HTTP, donc leurs noms sont Http400Exception , Http500Exception , etc.

La classe AbstractHttpException a trois propriétés : httpCode , httpMessage et appError . Les deux premières propriétés sont remplacées dans leurs héritiers et contiennent des informations de réponse de base telles que httpCode: 400 et httpMessage: Bad request . La propriété appError est un tableau des informations détaillées sur l'erreur, y compris la description de l'erreur.

Notre version finale de index.php interceptera trois types d'exceptions : AbstractHttpExceptions , comme décrit ci-dessus ; Phalcon Request Exceptions, qui peut survenir lors de l'analyse d'une requête ; et toutes les autres exceptions imprévues. Tous sont convertis dans un joli format JSON et envoyés au client via la classe Phalcon Response standard :

 <?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(); }

Création de modèles avec Phalcon Dev Tools

Si vous utilisez un IDE contemporain, vous êtes probablement habitué à la mise en évidence et à l'achèvement du code. De même, dans un framework PHP typique, vous pouvez inclure un dossier avec un framework pour accéder à une déclaration de fonction en un seul clic. Étant donné que Phalcon est une extension, nous n'obtenons pas cette option automatiquement. Heureusement, il existe un outil qui comble cette lacune appelé « Phalcon Dev Tools », qui peut être installé via Composer (si vous ne savez toujours pas ce que c'est, il est maintenant temps de faire connaissance avec cet incroyable gestionnaire de paquets). Phalcon Dev Tools se compose de stubs de code pour toutes les classes et fonctions de Phalcon, et fournit des générateurs de code avec des versions console et GUI, documentées sur le site Web PhalconPHP. Ces outils peuvent aider à créer toutes les parties du modèle MVC, mais nous ne couvrirons que la génération de modèles.

OK, installons Phalcon Dev Tools via Composer. Notre fichier composer.json ressemblera à ceci :

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

Comme vous pouvez le voir, nous avons besoin de PHP 5.6, de Phalcon 3 et de l'extension pgsql (que vous pouvez changer pour votre extension de base de données ou l'exclure complètement).

Assurez-vous que vous avez les bonnes versions d'extension PHP, Phalcon et DB, et exécutez composer :

 $ composer install

La prochaine étape consiste à créer notre base de données. Il est très simple et se compose d'une seule table, users . Bien que j'aie inclus un fichier pg_dump dans le projet, voici le dialecte SQL en 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 );

Maintenant que la base de données est créée, nous pouvons passer au processus de génération de modèle. Phalcon Dev Tools utilise un dossier .phalcon vide pour détecter si une application est un projet Phalcon, vous devrez donc créer ce dossier vide à la racine de votre projet. Il utilise également certains paramètres du fichier de configuration que nous avons créé - toutes les variables stockées dans la section application et l' adapter de la section database de données. Pour générer notre modèle, nous devons exécuter la commande suivante à partir du dossier racine du projet :

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

Si toutes les étapes précédentes ont été effectuées correctement, vous obtiendrez un fichier de modèle de travail, Users.php , dans votre dossier models , déjà placé dans un espace de noms avec des getters et des setters comme indiqué par la ligne de commande. Vient ensuite le contrôleur.

Contrôleurs et routage

Étant donné que notre application ne fait que CRUD (créer, lire, mettre à jour et supprimer) des utilisateurs, nous allons créer un seul contrôleur, le contrôleur Users avec les opérations suivantes :

  • Ajouter un utilisateur
  • Afficher la liste des utilisateurs
  • Mettre à jour l'utilisateur
  • Supprimer l'utilisateur

Bien que les contrôleurs puissent être créés à l'aide de Phalcon Dev Tools, nous le ferons manuellement et implémenterons AbstractController et son enfant, UsersController .

Créer le AbstractController est une bonne décision pour Phalcon car nous pouvons placer toutes les classes nécessaires que nous obtiendrons de l'injection de dépendances dans le bloc PHPDoc. Cela aidera avec la fonction de saisie semi-automatique de l'IDE. Nous pouvons également programmer certaines constantes d'erreur communes à tous les contrôleurs potentiels.

Pour l'instant, notre contrôleur abstrait ressemblera à ceci :

 <?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; }

Juste une simple classe injectable Phalcon, comme spécifié par la syntaxe des extends , rien de plus. Ensuite, créons le squelette 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) { } }

Pour le moment, c'est juste une classe avec des actions vides qui contiendra éventuellement les requêtes HTTP correspondantes.

Il est maintenant temps de remplir le fichier routes.php . Dans les micro-applications Phalcon, nous créons des collections, une pour chaque contrôleur, et ajoutons toutes les requêtes traitées sous forme de méthodes get , post , put , delete , qui prennent un modèle de route et une fonction de procédure comme arguments. Notez qu'une fonction de procédure doit être soit une fonction anonyme, soit le nom de la méthode d'un contrôleur. Voici à quoi ressemble notre fichier 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; } );

Nous définissons également un contrôleur de gestion et un préfixe URI. Pour notre exemple, un URI ressemblera à http://article.dev/user/add , et il doit s'agir d'une demande de post . Si nous voulons modifier les données de l'utilisateur, l'URI doit être une requête put et ressemblera à http://article.dev/user/12 pour modifier les données de l'utilisateur avec un ID de 12 . Nous définissons également un gestionnaire d'URL introuvable, qui génère une erreur. Pour plus d'informations, reportez-vous à la documentation PhalconPHP pour les routes dans une application full stack, et pour les routes dans une micro application.

Passons au corps du contrôleur, et plus précisément à la méthode addAction (toutes les autres sont similaires, vous pouvez les voir dans le code de l'application). Une méthode de contrôleur fait cinq choses :

  1. Obtient et valide les paramètres de la requête
  2. Prépare les données pour la méthode de service
  3. Appelle la méthode de service
  4. Gère les exceptions
  5. Envoie la réponse

Passons en revue chaque étape, en commençant par la validation. Bien que Phalcon dispose d'un composant de validation puissant, il est beaucoup plus rapide de valider les données à l'ancienne dans ce cas, donc, notre bloc de validation ressemblera à ceci :

 $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'; }

Ici, nous vérifions si le paramètre post est une chaîne qui correspond à une expression régulière. Toutes les valeurs sont placées dans le tableau $data , qui est ensuite passé à la classe UsersService . Toutes les erreurs sont placées dans le tableau $errors , qui est ensuite ajouté à un tableau de détails d'erreur dans Http400Exception , où il sera transformé en la réponse détaillée vue dans index.php :

Voici le code complet de la méthode addAction avec toute sa validation, qui inclut un appel à la méthode createUser dans UsersService (que nous n'avons pas encore créé) :

 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 **/ }

Comme vous pouvez le voir, nous traitons deux exceptions connues dans cette dernière section : user already exists et unable to create user en raison d'un problème interne tel qu'une erreur de connexion à la base de données. Par défaut, les exceptions inconnues seront levées en tant que HTTP 500 (erreur de serveur interne). Bien que nous ne donnions aucun détail à l'utilisateur final, il est fortement recommandé de stocker tous les détails de l'erreur (y compris la trace) dans le journal.

Et, s'il vous plaît, n'oubliez pas d' use toutes les classes nécessaires, empruntées aux autres espaces de noms :

 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;

Logique métier

La dernière partie à créer est la logique métier. Comme pour les contrôleurs, nous allons créer une classe de service abstraite :

 <?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; }

L'idée est complètement la même que dans le bloc du contrôleur, donc je ne la commenterai pas. Voici le squelette de notre 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) { } }

Et la méthode createUser elle-même :

 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); } } }

Cette méthode est aussi simple que possible. Nous créons simplement un nouvel objet modèle, appelons ses setters (qui renvoient l'objet lui-même ; cela nous permet de faire une chaîne d'appel) et lançons une ServiceException en cas d'erreur. C'est ça! Nous pouvons maintenant passer aux tests.

Essai

Examinons maintenant les résultats avec Postman. Testons d'abord quelques données inutiles :

Facteur avec des données invalides.

Demande:

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

Réponse (400 : Demande incorrecte) :

 { "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" } }

Cela vérifie. Maintenant, pour quelques données correctes :

Facteur avec des données valides.

Demande:

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

Réponse (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.

Désavantages

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.