- PVSM.RU - https://www.pvsm.ru -

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

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

Привет. Меня зовут Антон Титов, CTO компании Spiral Scout. Сегодня я хотел бы рассказать вам про нашего PHP-слона. А точнее про вторую версию опен-сорсного full-stack PHP/Go фреймворка — Spiral [1].

Spiral — это компонентный full-stack фреймворк, разрабатываемый нашей компанией более одиннадцати лет и обслуживающий под сотню реальных проектов. Программный пакет основан на множестве открытых и собственных библиотек, включая RoadRunner [2] и Cycle ORM [3].

Фреймворк совместим с большинством 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.

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

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

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

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

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

Для нас производительность является побочным эффектом выбранной архитектуры. Мы уверены, что при должной конфигурации и заменe PSR-7 на более легкую абстракцию, производительность можно поднять на 50-80% (это доказывает пример ubiquity-roadrunner [8]).

О Swoole. Swoole имеет меньший оверхед чем RoadRunner, так как работает с PHP в рамках одного процесса и написан на С++. В отдельных задачах можно выжать намного больше, чем используя сервер на Go. Плюс у вас появляются корутины, что трудно игнорировать.

С другой стороны RoadRunner менее инвазивен (внешние зависимости не требуются), есть больше готовых инструментов, он проще расширяется и работает под Windows.

Код работающий под Spiral будет прекрасно работать на Swoole, так что всегда можно переехать!

PSR-* и Компоненты

Большинство компонентов Spiral не являются обязательными для вашей сборки, разница между micro- и full- сборкой заключается только в содержимом composer.json. При необходимости можно воспользоваться интерфейсами для замены стандартных библиотек на альтернативные реализации.

HTTP слой фреймворка написан с учетом стандартов PSR-7/15/17, можно смело менять роутер [9], реализацию сообщений и т.д.

Большинство библиотек фреймворка можно использовать вне фреймворка. Так, например, RoadRunner прекрасно работает с Symfony [10] и Laravel [11], а 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 [3]. Это 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 работает быстрее [12]Doctrine на выборках, но медленнее на persist. Движок поддерживает сложные запросы с несколькими стратегиями загрузки, прокси классы, embeddings и предоставляет переносимые транзакции вместо глобального EntityManager.

Больше деталей можно будет услышать на PHP Russia 2020 [13].

Основная “фишка” ORM — возможность менять маппинг данных и связей в рантайм. Говоря простыми словами, вы можете позволить пользователям самостоятельно определять схему данных (DBAL поддерживает интроспекцию и декларирования схем баз данных).

Больше о сравнение Cycle, Eloquent и Doctrine 2 можно прочитать тут [14].

Быстрое прототипирование

Spiral включает несколько инструментов для ускорения и упрощения разработки. Основными являются авто-инъекция зависимостей, авто-конфигурация и автоматический поиск моделей, используя статический анализ. Консольные команды позволяют генерировать большинство необходимых классов и легко кастомизируются.

Для интеграции в IDE есть система быстрого прототипирования. Используя магический PrototypeTrait можно получить быстрый доступ к подсказкам в IDE.

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

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

Безопасность

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

Вам будут доступны компоненты для валидации сложных запросов (Request Filters), CSRF и шифрования (на основе defuse/php-encryption [16]). Работа с cookies и session поддерживает aнти-тамперинг и подписывание данных на стороне сервера.

Фреймворк предоставляет компонент аутентификации [17] на основе истекающих токенов. В качестве драйвера можно использовать сессии, базу данных либо вовсе чистый JWT.

Либо все типы токенов одновременно, если к вам внезапно прилетело требование “срочно подключите SAML/SSO/2FA!” :(

Авторизация доступа выполняется через RBAC компонент [18] с некоторыми доработками, позволяющими работу в режиме DAC и ABAC. Есть поддержка множества ролей, аннотаций для защиты методов контроллеров и система правил.

Работа с доменным слоем происходит через промежуточный интерцептор-слой [19]. Так можно создавать особые ограничения на группу контроллеров, пред-валидировать данные и оборачивать ошибки.

Шаблонизатор

Если вам нравится только 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 [20]. Telegram-канал [21].

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

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

Автор: Антон Титов

Источник [24]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/php-2/351875

Ссылки в тексте:

[1] Spiral: https://spiral.dev/

[2] RoadRunner: https://roadrunner.dev/

[3] Cycle ORM: https://github.com/cycle/orm

[4] тут: https://habr.com/ru/company/badoo/blog/434272/

[5] тут: https://habr.com/ru/company/oleg-bunin/blog/461827/

[6] тут: https://www.techempower.com/benchmarks/#section=test&runid=02692910-4c3f-4c56-a9dc-f0167a4280a4&hw=ph&test=fortune&l=zik073-1r&c=6&d=c&o=e

[7] тут: https://github.com/the-benchmarker/web-frameworks

[8] ubiquity-roadrunner: https://www.techempower.com/benchmarks/#section=test&runid=c7152e8f-5b33-4ae7-9e89-630af44bc8de&hw=ph&test=fortune&p=zik0zj-zik0zj-zik0zj-hra0hr-v

[9] менять роутер: https://spiral.dev/docs/http-psr-15

[10] Symfony: https://roadrunner.dev/docs/integration-symfony

[11] Laravel: https://roadrunner.dev/docs/integration-laravel

[12] быстрее : https://github.com/adrianmiu/forked-php-orm-benchmark

[13] PHP Russia 2020: https://phprussia.ru/moscow/2020/

[14] тут: https://github.com/cycle/docs/issues/3

[15] PHP-Parser: https://github.com/nikic/PHP-Parser

[16] defuse/php-encryption: https://github.com/defuse/php-encryption

[17] компонент аутентификации: https://spiral.dev/docs/security-authentication

[18] RBAC компонент: https://spiral.dev/docs/security-rbac

[19] интерцептор-слой: https://spiral.dev/docs/cookbook-domain-core

[20] Discord: https://discord.gg/kmmfk7M

[21] Telegram-канал: https://t.me/spiralphp

[22] GitHub фреймворка: https://github.com/spiral/framework

[23] GitHub стандратного приложения: https://github.com/spiral/app

[24] Источник: https://habr.com/ru/post/495224/?utm_source=habrahabr&utm_medium=rss&utm_campaign=495224