Привет, Хабр. Меня зовут Антон Титов, 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 App\Controller;
use App\Database\Repository\UserRepository;
use Spiral\Views\ViewsInterface;
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 лицензии и не имеет каких-либо ограничений для коммерческого использования.
Спасибо за внимание. Я надеюсь, наши инструменты пригодятся вам в проектах!
untilx
После прочтения статьи остались вопросы. Скорость на синтетических тестах — это, конечно, хорошо, но как обстоят дела с памятью и отказоустойчивостью? Вот, например, разработчики Phoenix говорят, что не имеют проблем при работе с огромными (порядка миллиона, если мне не изменяет память) количествами коннектов по вебсокету к одной ноде. С другой стороны у go могут начаться проблемы из-за сборки мусора.
Я так и не понял, в чём преимущество вашего фреймворка перед Symfony, Laravel или Phalcon? Symfony мне нравилась тем, что в ней всё работает, как в доках, пусть даже они и не всегда хорошие, а код на ней получается очень чистым, без лишнего хлама. Laravel простая, как пять копеек, даже не смотря на отвратительную производительность, полный треш в пакетах и чудачества eloquent, у неё всё равно есть своя область применения. У вас я открыл быстрый старт в документации, а там мне предлагают писать обработку аннотаций роутера или пользоваться каким-то странным классом с шаблонным кодом. Я что-то не так понял или и правда нужно нажимать так много лишних кнопок? Ну, да, пёс с ним, пользователи Лары как-то живут с её роутингом и ничего. Вы пишете, что в основном в фреймворк используется в b2b, но я так и не смог придумать ни одной причины, почему там нужно использовать интеграцию с го и не хватит просто php или питона, например. На каких задачах он сможет полноценно раскрыть свой потенциал?
EvgeniiR
Если вопрос в принципе про использование сервера приложений roadrunner, он убирает оверхед на создание инстанса приложения на каждый запрос, что даёт хороший буст производительности, и легко добавляется даже в уже существующие приложения, в т.ч. на Symfony/Laravel.
alekciy
Не понятно, за счет чего возникает «хороший» прирост производительности. Особенно если добавить это в существующее приложение на, допустим, laravel.
EvgeniiR
За счёт того что не нужно инициализировать всё приложение, зависимости, сервисы и т.п. на каждый запрос, работа идёт в скрипте-демоне.
Конечно, неэффективный код приложения, если таковой есть, не особо ускорится, ну и нужно будет учитывать при разработке, что процесс после запроса не завершится и не почистит всё за собой.
alekciy
Т.е. придется доработать приложение что бы оно за собой еще и подчищало данные? А потом всегда в голове это держать и не забыть зачищаться? Если да, то вижу я в этом усложнение процесса разработки и море багов связанных с этим.
Lachezis Автор
Именно так, придётся писать приложения как это принято в любом другом языке программирования.
Задача фреймворка как раз упростить разработку таких приложений и избавить вас от большинства багов.
alekciy
Не в любом другом. К примеру в erlang иммутабелен.
faiwer
C#, Java, Scala, NodeJS, да практически всё что угодно ? на них сервера пишут именно так.
Lachezis Автор
Спасибо за большой комментарий.
Все хорошо, мы написали rr как раз для того чтобы приложения выдерживали резкие нагрузки. Память зависит от количества воркеров и объема вашего приложения. Про поведение сервера под нагрузками можно почитать тут — https://habr.com/ru/post/431818/ (на ядре Symfony).
Мы больше склоняемся к модели Symfony, но используем работу как со стеком (middleware) Laravel. Код не сильно отличается от этих фреймворков, и, пожалуй, не должен отличаться. Плюсом можно назвать то что для запуска даже сложного приложения вам не потребуется ставить зоопарк инструментов.
Нет не нужно, просто аннотированный роутинг самый частый вопрос на который пока нет расширения из коробки, вот его и включили в обзорную статью. В самом начале упоминается что роутинг можно определять руками. Постараемся отполировать данную часть.
Сейчас мы пишем в основном распределенные проекты, в таких приложениях вы можете получить максимальную пользу от фреймворка за счет встроенных инструмент.
ainu
По поводу роутинга, у вас есть уникальная возможность перенести его в golang часть (только для Spiral), передавая в аттрибутах PSR-7 выбранный роут и параметры.
Lachezis Автор
Именно так оно и задумывалось. Плюс мы аунтифицируем пользователей еще до того как они достучатся до приложения.