Привет. Меня зовут Антон Титов, CTO компании Spiral Scout. Сегодня я хотел бы рассказать вам про нашего PHP-слона. А точнее про вторую версию опен-сорсного full-stack PHP/Go фреймворка — Spiral.
Spiral — это компонентный full-stack фреймворк, разрабатываемый нашей компанией более одиннадцати лет и обслуживающий под сотню реальных проектов. Программный пакет основан на множестве открытых и собственных библиотек, включая RoadRunner и Cycle ORM.
Фреймворк совместим с большинством PSR рекомендаций, поддерживает MVC и работает в 5-10 раз быстрее Laravel/Symfony.
Если вы никогда не слышали о Spiral и гадаете, что такое PHP/Go фреймворк и куда делась первая версия — добро пожаловать под кат.
О Фреймворке
Разработка Spiral была начата в 2008/09 годах в виде переносимого ядра для приложений, разрабатываемых под фриланс. В 2010 годы мы окончательно сформировали аутсорс компанию и с тех пор занимались улучшением нашего стека.
В итоге, фреймворк преобразовывался в набор независимых компонентов, объединенных общим интеграционным слоем.
Единственный публичный анонс первой версии произошел на Reddit в 2017 году и дал нам понять, что технически Spiral не особо выигрывала у конкурентов на тот момент. Мы учли полученный фидбек, опыт более сложных проектов и закончили вторую версию в мае 2019 года.
Спустя год документирования и обкатки на реальных проектах мы готовы представить данную разработку на суд общественности.
Основным отличием Spiral 2.0 от предыдущего поколения фреймворка и, пожалуй, от всех остальных существующих PHP-фреймворков является интегрированный сервер приложений RoadRunner, а также адаптация архитектуры под долгоживущую модель выполнения (режим демона).
Гибридный Рантайм
Основной концепцией фреймворка является симбиоз между сервером приложений, написанным на Golang, и PHP-ядром. Код PHP приложения загружается в память только один раз — так вы получаете значительную экономию ресурсов, но теряете возможность запускать WordPress.
Сервер отвечает за всю инфраструктурную часть: HTTP/FastCGI, общение с брокерами очередей, GRPC, WebSockets, Pub/Sub, кэш, метрики и т.д.
Написание кода под Spiral практически не отличается от кода под любой другой фреймворк. Однако к принципам SOLID придется относиться более ответственно. Если раньше кэширование данных пользователя в синглтоне вызывало лишь сильную боль на код-ревью, то сейчас такой код просто не будет работать корректно.
Знания Golang не являются обязательными для работы с платформой. Однако, выучив второй язык, можно создавать практически бесшовные интеграции c Golang библиотекам. Например встроить движок полнотекстового поиска или написать свой драйвер Kafka.
Чтобы выстрелить в ногу было чуть сложнее, было решено отказаться от системы ивентов и хуков и приоритезировать работу со стеком вызовов в явном виде.
Фреймворк предоставляет набор инструмент, таких как IoC замыкания, middleware, интерцепторы доменного слоя и immutable-сервисы.
$container->runScope(
[UserContext::class => $user],
function () use ($container) {
dump($container->get(UserContext::class);
}
);
Хотя такой подход и накладывают дополнительный оверхед, возможность не выгружать ядро программы, соединения с базой и сокеты из памяти с лихвой покрывает эти ограничения.
Производительность
В классе full-stack PHP фреймворков конкуренцию Spiral в основном составляют сборки на основе Swoole и несколько микро-фреймворков.
Полные бенчмарки доступны тут и тут.
Для нас производительность является побочным эффектом выбранной архитектуры. Мы уверены, что при должной конфигурации и заменe PSR-7 на более легкую абстракцию, производительность можно поднять на 50-80% (это доказывает пример ubiquity-roadrunner).
О Swoole. Swoole имеет меньший оверхед чем RoadRunner, так как работает с PHP в рамках одного процесса и написан на С++. В отдельных задачах можно выжать намного больше, чем используя сервер на Go. Плюс у вас появляются корутины, что трудно игнорировать.
С другой стороны RoadRunner менее инвазивен (внешние зависимости не требуются), есть больше готовых инструментов, он проще расширяется и работает под Windows.
Код работающий под Spiral будет прекрасно работать на Swoole, так что всегда можно переехать!
PSR-* и Компоненты
Большинство компонентов Spiral не являются обязательными для вашей сборки, разница между micro- и full- сборкой заключается только в содержимом composer.json
. При необходимости можно воспользоваться интерфейсами для замены стандартных библиотек на альтернативные реализации.
HTTP слой фреймворка написан с учетом стандартов PSR-7/15/17, можно смело менять роутер, реализацию сообщений и т.д.
Большинство библиотек фреймворка можно использовать вне фреймворка. Так, например, RoadRunner прекрасно работает с Symfony и Laravel, а Cycle ORM будет доступна в Yii3.
Компоненты сервера
Помимо PHP-компонентов, сборка RoadRunner включает несколько библиотек, написанных на Golang. Большинством сервисов сервера можно управлять из PHP.
В частности, есть компонент очередей, поддерживающий работу с брокерами AMQP, Amazon SQS и Beanstalk. Библиотека умеет корректно останавливаться, переподключаться и распределять любое количество входящих очередей на несколько воркеров.
Из коробки идет мониторинг на Prometheus и health-check точки, горячая перезагрузка и ограничение по использованию памяти. Для распределенных проектов есть GRPC сервер и клиент.
INFO[0154] 10.42.5.55:51990 Ok {2.28ms} /images.Service/GetFiles
INFO[0155] 10.42.3.95:50926 Ok {11.3ms} /images.Service/GetFiles
INFO[0156] 10.42.5.55:52068 Ok {3.60ms} /images.Service/GetFiles
INFO[0158] 10.42.5.55:52612 Ok {2.30ms} /images.Service/GetFiles
INFO[0166] 10.42.5.55:52892 Ok {2.23ms} /images.Service/GetFiles
INFO[0167] 10.42.3.95:49938 Ok {2.37ms} /images.Service/GetFiles
INFO[0169] 10.42.5.55:52988 Ok {2.22ms} /images.Service/GetFiles
Есть вебсокеты, их можно авторизовать из PHP приложения и подключать к pub-sub шине (в памяти или на Redis). На текущий момент производится обкатка Key-Value драйверов.
Портативность
Фреймворк не требует наличия PHP-FPM и NGINX. А все Golang-компоненты имеют драйверы для работы без внешних зависимостей. Таким образом, вы можете использовать очереди, websockets, метрики, не устанавливая внешние брокеры или программы.
./spiral serve -v -d
Spiral не важно, пишите ли вы огромное распределенное приложение или небольшой сайт, посылающий «напишите нам» в фоне. В любом случае вы можете использовать одинаковые инструменты, унифицируя поведение локального и продакшн окружений.
Так как HTTP-слой не является обязательным, можно писать консольные приложения, обрабатывающие данные в фоне, используя пакет очередей. Мы используем такие программы для миграций данных.
Cycle ORM
В качестве ORM из коробки идет Cycle ORM. Это Data Mapper движок очень похожий на Doctrine функционально, но сильно отличающийся архитектурно.
Как и Doctrine, Cycle может работать с чистыми доменными моделями, самостоятельно генерируя миграции и расставляя внешние ключи. Схему маппинга можно описывать кодом либо собирать из аннотаций. А вот вместо DQL используются классические Query Builders.
// загрузить всех активных пользователей
// и выбрать все оплаченные заказы отсортированные по дате
$users = $orm->getRepository(User::class)
->select()
->where('active', true)
->load('orders', [
'method' => Select::SINGLE_QUERY, // force LEFT JOIN
'load' => function($query) {
$query->where('paid', true)->orderBy('timeCreated', 'DESC');
}
])
->fetchAll();
$transaction = new Transaction($orm);
foreach($users as $user) {
$transaction->persist($user);
}
$transaction->run();
Cycle работает быстрее Doctrine на выборках, но медленнее на persist. Движок поддерживает сложные запросы с несколькими стратегиями загрузки, прокси классы, embeddings и предоставляет переносимые транзакции вместо глобального EntityManager.
Больше деталей можно будет услышать на PHP Russia 2020.
Основная “фишка” ORM — возможность менять маппинг данных и связей в рантайм. Говоря простыми словами, вы можете позволить пользователям самостоятельно определять схему данных (DBAL поддерживает интроспекцию и декларирования схем баз данных).
Больше о сравнение Cycle, Eloquent и Doctrine 2 можно прочитать тут.
Быстрое прототипирование
Spiral включает несколько инструментов для ускорения и упрощения разработки. Основными являются авто-инъекция зависимостей, авто-конфигурация и автоматический поиск моделей, используя статический анализ. Консольные команды позволяют генерировать большинство необходимых классов и легко кастомизируются.
Для интеграции в IDE есть система быстрого прототипирования. Используя магический PrototypeTrait можно получить быстрый доступ к подсказкам в IDE.
Трейт автоматом находит репозитории ORM, стандартные компоненты и умеет индексировать ваши сервисы по аннотациям. Под капотом используется магический метод __get, что гарантирует вам быстрое прохождение код ревью.
Достаточно запустить команду `php app.php prototype:inject -r`, и система прототипирования автоматически удалит всю магию:
namespace AppController;
use AppDatabaseRepositoryUserRepository;
use SpiralViewsViewsInterface;
class HomeController
{
/** @var ViewsInterface */
private $views;
/** @var UserRepository */
private $users;
/**
* @param ViewsInterface $views
* @param UserRepository $users
*/
public function __construct(ViewsInterface $views, UserRepository $users)
{
$this->users = $users;
$this->views = $views;
}
public function index()
{
return $this->views->render('profile', [
'user' => $this->users->findByName('Antony')
]);
}
}
Под капотом используется PHP-Parser.
Безопасность
Поскольку большинство наших приложений разрабатывается под B2B сегмент, к вопросам безопасности приходится относится серьезьно.
Вам будут доступны компоненты для валидации сложных запросов (Request Filters), CSRF и шифрования (на основе defuse/php-encryption). Работа с cookies и session поддерживает aнти-тамперинг и подписывание данных на стороне сервера.
Фреймворк предоставляет компонент аутентификации на основе истекающих токенов. В качестве драйвера можно использовать сессии, базу данных либо вовсе чистый JWT.
Либо все типы токенов одновременно, если к вам внезапно прилетело требование “срочно подключите SAML/SSO/2FA!” :(
Авторизация доступа выполняется через RBAC компонент с некоторыми доработками, позволяющими работу в режиме DAC и ABAC. Есть поддержка множества ролей, аннотаций для защиты методов контроллеров и система правил.
Работа с доменным слоем происходит через промежуточный интерцептор-слой. Так можно создавать особые ограничения на группу контроллеров, пред-валидировать данные и оборачивать ошибки.
Шаблонизатор
Если вам нравится только Twig — можете пролистать данный раздел, просто установите расширение и пользуйтесь знакомыми инструментами. :)
Из коробки идет шаблонизатор Stemper, а точнее, библиотека для создания собственных DSL разметок. В частности присутствует полноценный лексер, несколько грамматик, парсер и доступ к AST (по аналогии с PHP-Parser Никиты).
Есть возможность парсинга нескольких вложенных грамматик. Так, например, вы можете использовать директивы Laravel Blade и собственный DSL (в виде HTML тегов) разметки внутри одного шаблона. Получается что-то вроде web-components на стороне сервера.
Мы используем этот компонент для описания сложных интерфейсов, используя простые примитивы и правила.
<extends:admin.layout.tabs title="User Information"/>
<use:bundle path="admin/bundle"/>
<ui:tab id="info" title="Information">
User, {{ $user->name }}
</ui:tab>
<ui:tab id="data" title="User Settings">
<grid:table for={{ $user->settings }}>
<grid:cell title="Key">{{ $key }}</grid:cell>
<grid:cell title="Value">{{ $value }}</grid:cell>
</grid:table>
</ui:tab>
Поддерживается авто-экранирование с поддержкой контекста (например вывод PHP внутри JS блока автоматически преобразует данные в JSON), source-maps для работы с ошибками. Шаблоны компилируются в оптимизированный PHP код и после отдаются напрямую из памяти приложения.
Stemper может полноценно работать с DOM документа (хоть и медленнее, если использовать специализированные инструменты).
Развитие Фреймворка
Само собой, более чем за десятилетие разработки данного инструмента мы совершили множество ошибок. Часть из них удалось поправить к релизу второй версии, другая же часть требует пересмотра концепции некоторых компонентов.
Не будем также отрицать, что некоторые вещи потребуют допиливания напильником. А какие-то вопросы могут звучать для нас впервые. Документация хоть и старается покрыть максимум компонентов, все же может провисать в отдельных местах.
Мы открыты к критике и предложениям, так как сами активно пользуемся данным инструментом. Огромный бэклог улучшений потихоньку разбирается нашей командой и комьюнити.
Много работы потребуется для улучшения и ускорения Cycle, а также переписывания Request Filters согласно последним RFC. В процессе разработки находится компонент для работы и эмуляции Key-Value баз данных.
Многие вещи мы просто не успели перевести с первой версии. В планах восстановить пакеты ODM, панель администрирования, написать хороший профилировщик и т.д.
Комьюнити и Ссылки
Вы можете зайти в наше небольшое комьюнити на Discord. Telegram-канал.
Весь код распространяется по MIT лицензии и не имеет каких-либо ограничений для коммерческого использования.
Спасибо за внимание. Я надеюсь, наши инструменты пригодятся вам в проектах!
Автор: Антон Титов