PhalconPHP: решение для RESTful API с высокой нагрузкой
Опубликовано: 2022-03-11Предположим, вам нужно создать высоконагруженный проект на базе PHP MVC framework. Вы, вероятно, будете использовать кэширование везде, где это возможно. Возможно, вы соберете проект в один файл, а может быть, даже напишете свой MVC-фреймворк с минимальной функциональностью или перепишете какие-то части другого фреймворка. Хотя, да, это работает, это немного сложно, не так ли? К счастью, есть еще одно решение, делающее большинство этих манипуляций ненужными (разве что за исключением кеша), и это решение называется фреймворком PhalconPHP.
Что такое PhalconPHP?
PhalconPHP — это среда MVC для PHP, написанная на C и поставляемая в виде скомпилированного расширения PHP. Это то, что делает его одним из самых быстрых доступных фреймворков (честно говоря, самым быстрым из них является Yaf, но это микро-фреймворк и имеет гораздо более ограниченную функциональность, чем Phalcon). PhalconPHP не требует долгих операций с PHP-файлами и его не нужно интерпретировать при каждом запросе — он загружается в оперативную память один раз при запуске вашего веб-сервера и потребляет очень мало ресурсов.
Фреймворки MVC долгое время считались лучшей практикой в веб-разработке — к настоящему времени это своего рода профессиональный стандарт, поэтому большинство веб-разработчиков знакомы по крайней мере с одним фреймворком MVC для PHP: Symfony, Yii, Laravel, CodeIgniter, Zend. Фреймворк и т. д. У каждого из них есть свои преимущества и недостатки, но что у них общего? Все они написаны на PHP и состоят из множества включенных PHP-файлов с огромным количеством логики, которую интерпретатор должен запускать при каждом запросе, каждый раз, когда запускается ваш код. Хотя это обеспечивает большую прозрачность, мы платим за производительность. Большое количество кода и множество включенных файлов требуют много памяти и времени, особенно в PHP (поскольку он интерпретируется, а не компилируется). Да, в PHP 7 ситуация стала намного лучше, но многое еще нужно улучшить, и PhalconPHP предлагает эти улучшения.
Давайте взглянем на некоторые тесты.
Тесты PhalconPHP
Официальным эталонным тестам уже пять лет — они слишком старые, чтобы быть действительными сейчас, но даже тогда вы можете увидеть, что отличает PhalconPHP. Посмотрим что-нибудь поновее. В сравнении 2016 года Phalcon занимает пятерку лучших — явный лидер среди профессиональных фреймворков, уступая лишь чистому PHP и некоторым микрофреймворкам.
Итак, Phalcon быстр. Необработанный PHP также быстр, но нам нужны все возможности, которые может предложить среда MVC, и Phalcon справляется с этой задачей, включая такие компоненты, как:
- ОРМ
- Вольт шаблон Двигатель
- Контейнер внедрения зависимостей (DI)
- Кэширование
- логирование
- Системы маршрутизации
- Блок безопасности
- Автозагрузчик
- Модуль форм
Это лишь некоторые из них. Короче говоря, PhalconPHP имеет все необходимое для создания большого корпоративного приложения, такого как RESTful API для высоконагруженной системы.
Еще одна приятная особенность Phalcon — это его крошечный стиль — просто сравните Phalcon ORM и огромную Doctrine 2.
Давайте рассмотрим создание проекта PhalconPHP.
Два типа проектов Phalcon: Full-stack и Micro
Как правило, существует два типа фреймворков MVC: фреймворки с полным стеком (такие как Symfony, Yii) и микрофреймворки (такие как Lumen, Slim, Silex).
Фреймворки с полным стеком — хороший выбор для большого проекта, поскольку они предоставляют больше функциональных возможностей, но требуют немного большей квалификации и времени для запуска.
Микрофреймворки позволяют очень быстро создавать легкие прототипы, но им не хватает функциональности, поэтому их лучше не использовать для крупных проектов. Однако одним из преимуществ микрофреймворков является их производительность. Обычно они намного быстрее, чем полностековые (например, фреймворк Yaf уступает по производительности только сырому PHP).
PhalconPHP поддерживает и то, и другое: вы можете создать полнофункциональное или микроприложение. Еще лучше, когда вы разрабатываете свой проект в PhalconPHP как микроприложение, у вас по-прежнему есть доступ к большинству мощных функций Phalcon, и его производительность по-прежнему остается выше, чем у полнофункционального приложения.
На прошлой работе моей команде нужно было создать высоконагруженную систему RESTful. Одной из наших задач было сравнение производительности прототипа приложения с полным стеком в Phalcon и микроприложения Phalcon. Мы обнаружили, что микроприложения PhalconPHP работают намного быстрее. Я не могу показать вам какие-либо тесты из-за NDA, но, на мой взгляд, если вы хотите максимально использовать производительность Phalcon, используйте микроприложения. Хотя кодировать микроприложение менее удобно, чем полностековое, микроприложения PhalconPHP по-прежнему имеют все, что вам может понадобиться для вашего проекта, и более высокую производительность. Для иллюстрации давайте напишем очень простое микроприложение RESTful на Phalcon.
Создание RESTful API
Почти все перестановки приложения RESTful имеют одну общую черту: сущность User
. Итак, для нашего примерного проекта мы создадим крошечное приложение REST для создания, чтения, обновления и удаления пользователей (также известное как CRUD).
Полностью завершенный проект можно увидеть в моем репозитории GitLab. Там есть две ветки, потому что я решил разделить этот проект на две части: первая ветка, master
, содержит только базовый функционал без каких-либо специфических возможностей PhalconPHP, а вторая, logging-and-cache
, содержит функции логирования и кэширования Phalcon. Вы можете сравнить их и убедиться, насколько легко реализовать такие функции в Phalcon.
Установка
Я не буду вдаваться в подробности установки: вы можете использовать любую базу данных, любую операционную систему и любой веб-сервер, какой пожелаете. Это хорошо описано в официальной документации по установке, поэтому просто следуйте инструкциям в зависимости от вашей операционной системы.
Примечания по установке веб-сервера также доступны в официальной документации Phalcon.
Обратите внимание, что ваша версия PHP не должна быть ниже 5.6.
Я использую Ubuntu 16.10, PostgreSQL 9.5.6, Nginx 1.10.0, PHP 7 и Phalcon 3.0. Я включил в проект образец конфигурации Nginx и файл дампа PostgreSQL, так что не стесняйтесь их использовать. Если вы предпочитаете другую конфигурацию, изменить ее не составит труда.
Структура и конфигурация проекта
Прежде всего, создайте начальную структуру проекта.
Хотя Phalcon позволяет вам использовать любую структуру, которую вы хотите, структура, которую я выбрал для этого упражнения, частично реализует шаблон MVC. У нас нет представлений, потому что это проект RESTful, но у нас есть контроллеры и модели, каждая со своей папкой и службами. Сервисы — это классы, которые реализуют бизнес-логику проекта, разделяя «модельную» часть MVC на две части: модели данных (которые взаимодействуют с базой данных) и модели бизнес-логики.
index.php
, расположенный в public
папке, представляет собой загрузочный файл, который загружает все необходимые части и конфигурацию. Обратите внимание, что все наши файлы конфигурации находятся в папке config
. Мы могли бы поместить их в загрузочный файл (так и показано в официальной документации), но, на мой взгляд, это становится нечитаемым в больших проектах, поэтому я предпочитаю разделение папок с самого начала.
Создание index.php
Наш первый проход в index.php
загрузит конфигурацию и классы автозагрузки, затем инициализирует маршруты, контейнер внедрения зависимостей и микроприложение PhalconPHP. Затем он передаст управление этому ядру микроприложения, которое будет обрабатывать запросы в соответствии с маршрутами, запускать бизнес-логику и возвращать результаты.
Давайте посмотрим на код:
<?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 }
Настройка объекта \Phalcon\Config
Есть несколько способов хранения конфигурационных файлов в Phalcon:
- YAML-файл
- JSON-файл
- INI-файл
- PHP-массив
Хранение вашей конфигурации в массиве PHP — это самый быстрый вариант, и, поскольку мы пишем приложение с высокой нагрузкой и нам не нужно снижать производительность, мы так и поступим. В частности, мы будем использовать объект \Phalcon\Config
для загрузки параметров конфигурации в проект. У нас будет очень короткий объект конфигурации:
<?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' => "/", ], ] );
Этот файл содержит две основные конфигурации: одну для базы данных и одну для приложения. Очевидно, что конфигурация базы данных используется для подключения к базе данных, а что касается массива application
, он нам понадобится позже, так как он используется системными инструментами Phalcon. Более подробно о конфигурациях Phalcon можно прочитать в официальной документации.
Настройка loader.php
Давайте посмотрим на наш следующий файл конфигурации, loader.php
. Файл loader.php
регистрирует пространства имен с соответствующими каталогами через объект \Phalcon\Loader
. Это еще проще:
<?php $loader = new \Phalcon\Loader(); $loader->registerNamespaces( [ 'App\Services' => realpath(__DIR__ . '/../services/'), 'App\Controllers' => realpath(__DIR__ . '/../controllers/'), 'App\Models' => realpath(__DIR__ . '/../models/'), ] ); $loader->register();
Теперь все классы из этих пространств имен будут автоматически загружены и доступны. Если вы хотите добавить новое пространство имен и каталог, просто добавьте строку в этот файл. Вы также можете избежать использования пространств имен, зарегистрировав определенные каталоги или определенные файлы. Все эти возможности описаны в документации загрузчика PhalconPHP.
Настройка контейнера внедрения зависимостей
Как и многие другие современные фреймворки, Phalcon реализует шаблон внедрения зависимостей (DI). Объекты будут инициализированы в DI-контейнере и доступны из него. Точно так же DI-контейнер подключается к объекту приложения, и он будет доступен из всех классов, наследуемых от класса \Phalcon\DI\Injectable
, таких как наши контроллеры и сервисы.
Шаблон DI Phalcon очень мощный. Я считаю этот компонент одним из самых важных в этом фреймворке и настоятельно рекомендую вам прочитать всю его документацию, чтобы понять, как он работает. Он предоставляет ключ ко многим функциям Phalcon.
Давайте взглянем на некоторые из них. Наш файл di.php
будет выглядеть так:
<?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;
Как видите, наш файл внедрения зависимостей (DI) немного сложнее, и есть некоторые особенности, о которых вам следует знать. Во-первых, рассмотрим строку инициализации: $di = new \Phalcon\DI\FactoryDefault();
. Мы создаем объект FactoryDefault
, который наследует \Phalcon\Di
(Phalcon позволяет вам создать любую фабрику DI, которую вы хотите). Согласно документации, FactoryDefault
«автоматически регистрирует все сервисы, предоставляемые фреймворком. Благодаря этому разработчику не нужно регистрировать каждый сервис по отдельности, предоставляя полную структуру стека». Это означает, что общие службы, такие как Request
и Response
, будут доступны в классах фреймворка. Полный список таких сервисов вы можете увидеть в сервисной документации Phalcon.
Следующим важным моментом является процесс настройки: есть несколько способов зарегистрировать что-то в DI-контейнере, и все они полностью описаны в документации по регистрации PhalconPHP. Однако в нашем проекте мы используем три способа: анонимную функцию, переменную и строку.
Анонимная функция позволяет нам делать много вещей при инициализации класса. В частности, в этом проекте мы сначала переопределяем объект Response
, чтобы установить content-type
как JSON
для всех ответов проекта, а затем инициализируем адаптер базы данных, используя наш объект конфигурации.
Как я упоминал ранее, в этом проекте используется PostgreSQL. Если вы решите использовать другой движок базы данных, просто измените адаптер базы данных в функции db
set. Вы можете узнать больше о доступных адаптерах базы данных и уровне базы данных в документации по базе данных PhalconPHP.
Третье замечание: я регистрирую переменную $config
, которая реализует службу \Phalcon\Config
. Хотя он фактически не используется в нашем примерном проекте, я решил включить его сюда, потому что это один из наиболее часто используемых сервисов; другим проектам может понадобиться доступ к конфигурации практически везде.
Последняя интересная вещь здесь — это сам метод setShared
. Вызов этого делает сервис «общим», что означает, что он начинает действовать как синглтон. Согласно документации: «После того, как служба разрешена в первый раз, один и тот же ее экземпляр возвращается каждый раз, когда потребитель извлекает службу из контейнера».
Настройка routes.php
…или нет
Последний включенный файл — routes.php
. Давайте пока оставим его пустым — мы заполним его вместе с нашими контроллерами.
Внедрение ядра RESTful
Что делает веб-проект RESTful? Согласно Википедии, приложение RESTful состоит из трех основных частей: — базовый URL-адрес — тип интернет-медиа, который определяет элементы данных перехода состояния — стандартные методы HTTP ( GET
, POST
, PUT
, DELETE
) и стандартные коды ответов HTTP (200, 403, 400, 500 и др.).
В нашем проекте базовые URL будут помещены в файл routes.php
, а остальные упомянутые моменты будут описаны сейчас.
Мы будем получать данные запроса как application/x-www-form-urlencoded
и отправлять данные ответа как application/json
. Хотя я не считаю хорошей идеей использовать x-www-form-urlencoded
в реальном приложении (поскольку вам будет сложно отправлять сложные структуры данных и ассоциативные массивы с помощью x-www-form-urlencoded
), я решил реализовать этот стандарт для простоты.
Если вы помните, мы уже установили JSON-заголовок ответа в файле DI:
$di->setShared( 'response', function () { $response = new \Phalcon\Http\Response(); $response->setContentType('application/json', 'utf-8'); return $response; } );
Теперь нам нужно настроить коды ответа и формат ответа. В официальном руководстве предлагается формировать ответы JSON в каждом отдельном методе, но я не думаю, что это хорошая идея. Гораздо универсальнее возвращать результаты метода контроллера в виде массивов, а затем преобразовывать их в стандартные ответы JSON. Также разумнее формировать коды ответов HTTP в одном месте внутри проекта; мы собираемся сделать это в нашем файле index.php
.
Для этого мы собираемся использовать способность Phalcon выполнять код до и после обработки запросов с помощью $app->before()
и $app->after()
. Для нашей цели мы поместим обратный вызов в метод $app->after()
:
// 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(); }
Здесь мы получаем возвращаемое значение и преобразуем массив в JSON. Если бы все было в порядке, но возвращаемое значение было пустым (например, если мы успешно добавили нового пользователя), мы бы давали HTTP-код 204 и не отправляли контент. Во всех остальных случаях мы выбрасываем исключение.
Обработка исключений
Одним из наиболее важных аспектов приложения RESTful являются правильные и информативные ответы. Высоконагруженные приложения обычно большие, и везде могут возникать ошибки различного типа: ошибки проверки, ошибки доступа, ошибки соединения, непредвиденные ошибки и т. д. Мы хотим преобразовать все эти ошибки в унифицированные коды ответов HTTP. Это легко сделать с помощью исключений.
В своем проекте я решил использовать два разных вида исключений: есть «локальные» исключения — специальные классы, унаследованные от класса \RuntimeException
, разделенные сервисами, моделями, адаптерами и т. д. (такое деление помогает обрабатывать каждый уровень модели MVC как отдельное) — то есть HttpExceptions
, унаследованные от класса AbstractHttpException
. Эти исключения соответствуют кодам ответа HTTP, поэтому их имена — Http400Exception
, Http500Exception
и т. д.
Класс AbstractHttpException
имеет три свойства: httpCode
, httpMessage
и appError
. Первые два свойства переопределяются в своих наследниках и содержат базовую информацию ответа, такую как httpCode: 400
и httpMessage: Bad request
. Свойство appError
представляет собой массив подробной информации об ошибке, включая описание ошибки.
Наша окончательная версия index.php
будет перехватывать три типа исключений: AbstractHttpExceptions
, как описано выше; Исключения запроса Phalcon, которые могут возникнуть при разборе запроса; и все другие непредвиденные исключения. Все они конвертируются в красивый формат JSON и отправляются клиенту через стандартный класс 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(); }
Создание моделей с помощью Phalcon Dev Tools
Если вы используете современную IDE, вы, вероятно, привыкли к подсветке и завершению кода. Точно так же в типичном PHP-фреймворке можно включить папку с фреймворком, чтобы перейти к объявлению функции всего в один клик. Поскольку Phalcon является расширением, мы не получаем эту опцию автоматически. К счастью, существует инструмент, который заполняет этот пробел под названием «Phalcon Dev Tools», который можно установить через Composer (если вы до сих пор не знаете, что это такое, самое время познакомиться с этим удивительным менеджером пакетов). Phalcon Dev Tools состоит из заглушек кода для всех классов и функций в Phalcon и предоставляет некоторые генераторы кода как с консольной версией, так и с графическим интерфейсом, задокументированные на веб-сайте PhalconPHP. Эти инструменты могут помочь в создании всех частей шаблона MVC, но мы рассмотрим только создание модели.
Хорошо, давайте установим Phalcon Dev Tools через Composer. Наш файл composer.json
будет выглядеть так:
{ "require": { "php": ">=5.6.0", "ext-phalcon": ">=3", "ext-pgsql": "*" }, "require-dev": { "phalcon/devtools": "3.*.*@dev" } }
Как видите, нам требуется PHP 5.6, Phalcon 3 и расширение pgsql
(которое вы можете изменить на расширение своей базы данных или вообще исключить).
Убедитесь, что у вас есть правильные версии расширений PHP, Phalcon и DB, и запустите композитор:
$ composer install
Следующим шагом будет создание нашей базы данных. Он очень прост и состоит только из одной таблицы users
. Хотя я включил в проект файл pg_dump
, вот SQL на диалекте 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 );
Теперь, когда база данных создана, мы можем перейти к процессу генерации модели. Phalcon Dev Tools использует пустую папку .phalcon
, чтобы определить, является ли приложение проектом Phalcon, поэтому вам придется создать эту пустую папку в корне вашего проекта. Он также использует некоторые настройки из созданного нами файла конфигурации — все переменные, хранящиеся в разделе application
, и adapter
из раздела database
. Чтобы сгенерировать нашу модель, нам нужно выполнить следующую команду из корневой папки проекта:
$ php vendor/phalcon/devtools/phalcon.php model users --namespace="App\Models" --get-set
Если все предыдущие шаги были выполнены правильно, вы получите рабочий файл модели Users.php
в папке ваших models
, уже помещенный в пространство имен с геттерами и сеттерами, как указано в командной строке. Дальше контроллер.
Контроллеры и маршрутизация
Поскольку наше приложение только CRUD (создает, читает, обновляет и удаляет) пользователей, мы создадим только один контроллер, контроллер Users
со следующими операциями:
- Добавить пользователя
- Показать список пользователей
- Обновить пользователя
- Удалить пользователя
Хотя контроллеры можно создавать с помощью Phalcon Dev Tools, мы сделаем это вручную и реализуем AbstractController
и его дочерний UsersController
.
Создание AbstractController
— хорошее решение для Phalcon, потому что мы можем поместить все необходимые классы, которые мы получим в результате внедрения зависимостей, в блок PHPDoc. Это поможет с функцией автозаполнения IDE. Мы также можем запрограммировать некоторые константы ошибок, которые являются общими для всех потенциальных контроллеров.
На данный момент наш абстрактный контроллер будет выглядеть так:
<?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; }
Просто простой внедряемый класс Phalcon, как указано в синтаксисе extends
, и ничего более. Далее создадим скелет 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) { } }
На данный момент это просто класс с пустыми действиями, которые в конечном итоге будут содержать соответствующие HTTP-запросы.
Теперь пришло время заполнить файл routes.php
. В микроприложениях Phalcon мы создаем коллекции, по одной для каждого контроллера, и добавляем все обработанные запросы в виде методов get
, post
, put
, delete
, которые принимают шаблон маршрута и обрабатывающую функцию в качестве аргументов. Обратите внимание, что работающая функция должна быть либо анонимной функцией, либо именем метода контроллера. Вот как выглядит наш файл 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; } );
Мы также устанавливаем контроллер обработки и префикс URI. В нашем примере URI будет выглядеть как http://article.dev/user/add
, и это должен быть post
запрос. Если мы хотим изменить данные пользователя, URI должен быть запросом на put
и выглядеть как http://article.dev/user/12
, чтобы изменить данные для пользователя с идентификатором 12
. Мы также определяем обработчик ненайденных URL-адресов, который выдает ошибку. Для получения дополнительной информации обратитесь к документации PhalconPHP для маршрутов в приложении с полным стеком и для маршрутов в микроприложении.
Перейдем к телу контроллера, а конкретно к методу addAction
(все остальные аналогичны, их можно увидеть в коде приложения). Метод контроллера делает пять вещей:
- Получает и проверяет параметры запроса
- Подготавливает данные для сервисного метода
- Вызывает метод службы
- Обрабатывает исключения
- Отправляет ответ
Давайте пройдемся по каждому шагу, начиная с проверки. Несмотря на то, что в Phalcon есть мощный компонент валидации, в данном случае гораздо целесообразнее валидировать данные по старинке, поэтому наш блок валидации будет выглядеть так:
$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'; }
Здесь мы проверяем, является ли параметр post
строкой, соответствующей регулярному выражению. Все значения помещаются в массив $data
, который затем передается классу UsersService
. Все ошибки помещаются в массив $errors
, который затем добавляется в массив сведений об ошибках внутри Http400Exception
, где он будет преобразован в подробный ответ, показанный в index.php
:
Вот полный код метода addAction
со всей его проверкой, которая включает в себя вызов метода createUser
в UsersService
(который мы еще не создали):
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 **/ }
Как видите, в этом последнем разделе мы обрабатываем два известных исключения: user already exists
и unable to create user
из-за какой-то внутренней проблемы, такой как ошибка подключения к базе данных. По умолчанию неизвестные исключения будут выдаваться как HTTP 500
(внутренняя ошибка сервера). Хотя мы не предоставляем никаких сведений конечному пользователю, настоятельно рекомендуется сохранять все сведения об ошибках (включая трассировку) в журнале.
И, пожалуйста, не забудьте use
все необходимые классы, позаимствованные из других пространств имён:
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;
Бизнес-логика
Последняя часть, которую нужно создать, — это бизнес-логика. Как и в случае с контроллерами, мы создадим абстрактный сервисный класс:
<?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; }
Идея полностью та же, что и в блоке контроллера, поэтому комментировать не буду. Вот скелет нашего класса 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) { } }
И сам метод 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); } } }
Этот метод настолько прост, насколько это возможно. Мы просто создаем новый объект модели, вызываем его сеттеры (которые возвращают сам объект; это позволяет нам сделать цепочку вызовов) и в случае ошибки выбрасываем ServiceException
. Вот и все! Теперь мы можем перейти к тестированию.
Тестирование
Теперь давайте посмотрим на результаты с помощью Postman. Давайте сначала проверим некоторые мусорные данные:
Запрос:
POST http://article.dev/user/add login:1 password:1 first_name:Name last_name:Sourname
Ответ (400: неверный запрос):
{ "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" } }
Это подтверждается. Теперь немного правильных данных:
Запрос:
POST http://article.dev/user/add login:user4 password:password4 first_name:Name last_name:Sourname
Ответ (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.
Недостатки
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.