Spiral: high-performance PHP / Go framework



Hello, Habr. My name is Anton Titov, CTO of Spiral Scout. Today I would like to tell you about our PHP elephant. Or rather, about the second version of the open-source full-stack PHP / Go framework - Spiral .

Spiral is a component full-stack framework developed by our company for more than eleven years and serving a hundred of real projects. The software package is based on many open and proprietary libraries, including RoadRunner and Cycle ORM .

The framework is compatible with most PSR recommendations, supports MVC and runs 5-10 times faster than Laravel / Symfony.

If you have never heard of Spiral and are wondering what the PHP / Go framework is and where the first version went, then welcome to cat.

About the framework


Spiral development began in 2008/09 as a portable kernel for freelance applications. In 2010, we finally formed an outsourcing company and have since been engaged in improving our stack.

As a result, the framework was transformed into a set of independent components, united by a common integration layer.

The only public announcement of the first version occurred on Reddit in 2017 and made it clear to us that, technically, Spiral did not really beat the competitors at that time. We took into account the feedback we received, the experience of more complex projects and completed the second version in May 2019.

After a year of documentation and running on real projects, we are ready to submit this development to the public.

The main difference between Spiral 2.0 from the previous generation of the framework and, perhaps, from all other existing PHP frameworks is the integrated application server RoadRunner, as well as the adaptation of the architecture for a long-lived execution model (daemon mode).

Hybrid Rantheim


The main concept of the framework is the symbiosis between the application server written in Golang and the PHP core. PHP application code is loaded only once into memory - this way you get significant resource savings, but lose the ability to run WordPress.



You can read more about the hybrid model here and here .

The server is responsible for the entire infrastructure part: HTTP / FastCGI, communication with queue brokers, GRPC, WebSockets, Pub / Sub, cache, metrics, etc.

Writing code for Spiral is practically no different from code for any other framework. However, SOLID principles will have to be more responsible. If previously caching user data in singleton caused only severe pain for the code review, now such a code simply will not work correctly.

Golang knowledge is not required to work with the platform. However, having learned a second language, you can create almost seamless integrations with Golang libraries. For example, embed a full-text search engine or write your own Kafka driver.

To shoot in the leg was a little more difficult, it was decided to abandon the system of events and hooks and prioritize work with the call stack in an explicit form.

The framework provides a set of tools, such as IoC closures, middleware, domain layer spoilers and immutable services.

$container->runScope(
    [UserContext::class => $user],
    function () use ($container) {
        dump($container->get(UserContext::class);
    }
);

Although such an approach imposes an additional overhead, the ability not to unload the core of the program, connections to the database and sockets from memory more than covers these limitations.

Performance


In the full-stack class of PHP frameworks, Spiral mainly competes with Swoole-based assemblies and several micro-frameworks.



Full benchmarks are available here and here .

For us, performance is a side effect of the chosen architecture. We are sure that with proper configuration and replacing the PSR-7 with a lighter abstraction, productivity can be increased by 50-80% (this is proved by the example of ubiquity-roadrunner ).

Swoole. Swoole RoadRunner, PHP ++. , Go. , .

RoadRunner ( ), , Windows.

Spiral Swoole, !

PSR-*


Most Spiral components are optional for your build, the difference between micro and full build is only in the content composer.json. If necessary, you can use the interfaces to replace standard libraries with alternative implementations.

The HTTP layer of the framework is written taking into account the PSR-7/15/17 standards, you can safely change the router , the implementation of messages, etc.

Most framework libraries can be used outside the framework. For example, RoadRunner works great with Symfony and Laravel , and Cycle ORM will be available in Yii3.

Server components


In addition to the PHP components, the RoadRunner assembly includes several libraries written in Golang. Most server services can be managed from within PHP.

In particular, there is a queue component that supports work with brokers AMQP, Amazon SQS and Beanstalk. The library can correctly stop, reconnect and distribute any number of incoming queues to several workers.

Out of the box there is monitoring on Prometheus and health-check points, a hot reboot and a memory limit. For distributed projects, there is a GRPC server and client. There are web sockets, they can be authorized from a PHP application and connected to the pub-sub bus (in memory or on Redis). Key-Value drivers are currently being tested.

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




Portability


The framework does not require PHP-FPM and NGINX. And all Golang components have drivers for working without external dependencies. Thus, you can use queues, websockets, metrics without installing external brokers or programs.

./spiral serve -v -d

Spiral doesn’t matter if you write a huge distributed application or a small site sending “write to us” in the background. In any case, you can use the same tools, unifying the behavior of local and production environments.

Since the HTTP layer is optional, you can write console applications that process data in the background using a queue package. We use such programs for data migration.

Cycle ORM


As an ORM out of the box comes Cycle ORM . This Data Mapper engine is very similar in function to Doctrine, but very architecturally different.

Like Doctrine, Cycle can work with pure domain models, independently generating migrations and arranging foreign keys. The mapping scheme can be described by code or collected from annotations. But instead of DQL, classic Query Builders are used.

//    
//        
$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 runs faster than Doctrine on selections, but slower on persist. The engine supports complex queries with several loading strategies, proxy classes, embeddings and provides portable transactions instead of the global EntityManager.

More details can be heard at PHP Russia 2020 .

The main “feature” of ORM is the ability to change the mapping of data and links in runtime. In simple words, you can allow users to independently define the data schema (DBAL supports introspection and declaration of database schemas).

You can read more about comparing Cycle, Eloquent, and Doctrine 2 here .

Rapid Prototyping


Spiral includes several tools to speed up and simplify development. The main ones are auto-dependency injection, auto-configuration and automatic model search using static analysis. Console commands allow you to generate most of the required classes and are easily customizable.

For integration into the IDE there is a rapid prototyping system. Using the magic PrototypeTrait, you can get quick access to tips in the IDE.



The trait automatically finds ORM repositories, standard components and can index your services by annotations. Under the hood, the magic __get method is used , which guarantees you a quick code review.

Just run the command `php app.php prototype: inject -r`, and the prototyping system will automatically remove all the magic:

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')
        ]);
    }
}

Under the hood, PHP-Parser is used .

Safety


Since most of our applications are developed for the B2B segment, security matters have to be taken seriously.

Components for validating complex requests (Request Filters), CSRF and encryption (based on defuse / php-encryption ) will be available to you . Work with cookies and session supports anti-tampering and signing of data on the server side.

The framework provides an expiring token based authentication component . As a driver, you can use sessions, a database, or even pure JWT.

Or all types of tokens at the same time, if the requirement “urgently connect SAML / SSO / 2FA!” Suddenly flies to you :(

Access authorization is performed through the RBAC component with some improvements that allow operation in DAC and ABAC mode. There is support for many roles, annotations to protect controller methods, and a system of rules.

Work with the domain layer occurs through an intermediate interceptor layer . So you can create special restrictions on a group of controllers, pre-validate data and wrap errors.

Template Engine


If you like only Twig - you can scroll through this section, just install the extension and use familiar tools. :)

Out of the box comes the Stemper template engine, or rather, a library for creating your own DSL markup. In particular, there is a full-fledged lexer, several grammars, a parser and access to AST (similar to Nikita's PHP-Parser).

It is possible to parse several nested grammars. So, for example, you can use Laravel Blade directives and your own DSL (in the form of HTML tags) markup within the same template. It turns out something like web-components on the server side.

We use this component to describe complex interfaces using simple primitives and rules.

<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>

It supports auto-escaping with context support (for example, the output of PHP inside the JS block automatically converts data to JSON), source-maps for working with errors. Templates are compiled into optimized PHP code and then given directly from the application memory.
Stemper can fully work with the DOM of the document (albeit slower if you use specialized tools).

Framework Development


Of course, in more than a decade of developing this tool, we have made many mistakes. Some of them were corrected for the release of the second version, while the other part requires a revision of the concept of some components.

We will also not deny that some things require a file. And some questions may sound for us for the first time. Although the documentation tries to cover the maximum of components, it can still sag in some places.

We are open to criticism and suggestions, since we ourselves are actively using this tool. The huge backlog of improvements is slowly being sorted out by our team and the community.

A lot of work will be required to improve and speed up Cycle, as well as rewriting Request Filters according to the latest RFCs. Under development is a component for working and emulating Key-Value databases.

We simply did not manage to translate many things from the first version. The plans are to restore ODM packages, admin panel, write a good profiler, etc.

Community and Links


You can log in to our small Discord community . Telegram channel .


All code is distributed under the MIT license and does not have any restrictions for commercial use.

Thank you for the attention. I hope our tools come in handy in your projects!

All Articles