Spiral: высокопроизводительный PHP/Go фреймворк

1_kugualputdvwq2gxujopusshg.png

Привет, Хабр. Меня зовут Антон Титов, 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.

88-ofwfx3p83cjgqkkwuqmkuxk0.png

Больше о гибридной модели можно прочитать тут и тут.


Сервер отвечает за всю инфраструктурную часть: 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 и несколько микро-фреймворков.

nwa5tzv_osg63cq6l2ha2xpbs2c.png

Полные бенчмарки доступны тут и тут.

Для нас производительность является побочным эффектом выбранной архитектуры. Мы уверены, что при должной конфигурации и замен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.

339u_ac_8gelzkarpyljtgtisck.gif

Трейт автоматом находит репозитории 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 на стороне сервера.

Мы используем этот компонент для описания сложных интерфейсов, используя простые примитивы и правила.





  User, {{ $user->name }}



  settings }}>
    {{ $key }}
    {{ $value }}
  


Поддерживается авто-экранирование с поддержкой контекста (например вывод PHP внутри JS блока автоматически преобразует данные в JSON), source-maps для работы с ошибками. Шаблоны компилируются в оптимизированный PHP код и после отдаются напрямую из памяти приложения.

Stemper может полноценно работать с DOM документа (хоть и медленнее, если использовать специализированные инструменты).


Развитие Фреймворка


Само собой, более чем за десятилетие разработки данного инструмента мы совершили множество ошибок. Часть из них удалось поправить к релизу второй версии, другая же часть требует пересмотра концепции некоторых компонентов.

Не будем также отрицать, что некоторые вещи потребуют допиливания напильником. А какие-то вопросы могут звучать для нас впервые. Документация хоть и старается покрыть максимум компонентов, все же может провисать в отдельных местах.

Мы открыты к критике и предложениям, так как сами активно пользуемся данным инструментом. Огромный бэклог улучшений потихоньку разбирается нашей командой и комьюнити.

Много работы потребуется для улучшения и ускорения Cycle, а также переписывания Request Filters согласно последним RFC. В процессе разработки находится компонент для работы и эмуляции Key-Value баз данных.

Многие вещи мы просто не успели перевести с первой версии. В планах восстановить пакеты ODM, панель администрирования, написать хороший профилировщик и т.д.

Комьюнити и Ссылки


Вы можете зайти в наше небольшое комьюнити на Discord. Telegram-канал.
Весь код распространяется по MIT лицензии и не имеет каких-либо ограничений для коммерческого использования.

Спасибо за внимание. Я надеюсь, наши инструменты пригодятся вам в проектах!

© Habrahabr.ru