New Release: Spiral Framework 2.0

New Release: Spiral Framework 2.0

We officially have a pre-teen. Our original open-source product, Spiral Framework, is a full-fledged 11-year-old, and we are celebrating with the release of the new and improved version 2.0. A modern full-stack framework written in PHP and Golang, Spiral currently powers hundreds of real-life projects used by millions of people around the globe every day.The software package is based on several community and open-source libraries, including two products also released by Spiral Scout, RoadRunner and Cycle ORM. Compatible with most PSR recommendations, Spiral supports MVC, and in benchmarking tests, has performed 5 to 10 times faster than Laravel and Symfony.

We’re taking a deep dive into this highly unique PHP/Go framework today, including its history, how it works, key integrations for rapid application development, and more.

About Spiral Framework

Spiral Framework 2.0

The development of Spiral began in 2008 but really took off after 2010 when Spiral Scout formalized as a software outsourcing and IT staff augmentation company, and we started improving our entire stack.

The framework transformed from a simple portable kernel for freelance applications into a set of powerful independent components united by a common integration layer.

The response after Spiral Framework's very first introduction to the public in 2017 was a mixed bag. It soon became clear that it wasn't winning over any of its competitors' users. We incorporated tons of feedback and paired it with our experience from working on more complex projects where we were able to use Spiral Framework. This helped us finish a more robust and user-friendly second version in May 2019.

After a year of documentation and running it on real projects, we are excited to present version 2.0 of Spiral Framework to the public.

The main difference of Spiral 2.0 from its previous generation and, perhaps, from all other existing PHP frameworks, is an integrated application server called RoadRunner, as well as the unique adaptation of the architecture for a long runtime model (daemon mode).

Hybrid Runtime

The basic concept of the framework is the symbiosis between the application server written in Golang and the PHP core. The PHP application code is loaded into memory only once - this way, you get significant resource savings, though you lose the ability to run WordPress. Read more about the hybrid model here.

Hybrid Runtime

The application server is responsible for the whole infrastructure part: communication with queue brokers, GRPC, WebSockets, Pub/Sub, Cache, metrics, real-time communications, etc.

Writing code for Spiral is practically the same as for any other framework. However, the principles of SOLID need to be treated with a greater degree of responsibility. To put it bluntly, bring your coding 'A' game, and you'll hit homers with Spiral Framework. Sloppy code strikes out.

While Spiral includes integrations based in Golang, for example, you can embed a full-test search engine or write your own Kafka drive, it truly was developed as a pure PHP framework. Knowledge of Golang could be an added bonus for reaping even more benefits from Spiral, but using PHP alone, you'll be more than equipped for rapid application development.

The framework provides a set of tools such as IoC scopes/closures, middleware, domain layer interceptors, and immutable services. Below is an example of running the domain code in a given user context.

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

Although this approach does impose an additional overhead, the ability not to unload the program core, database connections, and sockets from memory more than makes up for these perceived limitations.

Performance

In the full-stack PHP framework class, Spiral's competitors are mainly Swoole-based builds and several micro-frames. Other frameworks (even Phalcon!) are behind.

Spiral Performance

You can see full performance benchmarks from TechEmpower here and Github here.

For our team, the enhanced performance is a positive side effect of the chosen architecture. We believe that with proper configuration and replacing the PSR-7 with a lighter abstraction, performance can be improved by 50-80% (this is proved by the example of ubiquity-roadrunner), thus lending itself to scaling quickly and efficiently.

Without having to worry about performance, we can now focus more on architecture and business flows.

Note
Swoole has less overhead than RoadRunner because it works with PHP in a single process and is written in C++. In individual tasks, you can squeeze a lot more out of that process than when you use the server on Go. Plus you get a lot of coroutine, which is hard to ignore.

On the other hand, RoadRunner is less invasive (no external dependencies required), there are more ready-made tools, and it is easier to expand and run on Windows.

The code running under Spiral will work fine on Swoole, so making the move is easy!

PSR-* and Components

Most Spiral components are not mandatory for your build; the only difference between micro and full builds is the content of composer.json. If necessary, you can use interfaces to replace standard libraries with alternative implementations. This essentially makes Spiral a fullstack and micro framework at the same time.

The HTTP layer of the framework is written with PSR-7/15/17 in mind so you can safely change the router, message implementation, etc. Theoretically, the kernel can even work based on Symfony/HttpKernel without significant changes to the domain layer.

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

Server Components

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

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

From the box, there is monitoring on Prometheus and health-check points, hot reboot, and memory usage restriction. For distributed projects, there is a GRPC server and a client.

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			

Web sockets can be authorized from a PHP application and connected to the pub-sub bus (in memory or on Redis). Currently, Key-Value drivers are in the works.

The set of out-of-the-box components allows you to create a horizontally scalable application without any external library or long configuration. It’s just there.

Portability

The framework does not require PHP-FPM and nginx, and all components have drivers to work without external dependencies. So you can use queues, websockets, and metrics without having to install external brokers or programs.

./spiral serve -v -d

Spiral works seamlessly for writing both huge distributed applications and small websites that send Contact information in the background. Either way, you can use the same tools, unifying the behavior of local and production environments.

Since the HTTP layer is not mandatory, you can also write console applications that process data in the background using a batch of queues. Our team uses such applications to migrate data.

Cycle ORM

Out of the box, Cycle ORM is a Data Mapper engine very similar to Doctrine in functionality, but very different in architecture.

Like Doctrine, Cycle can handle pure domain models by generating migrations and allocating external keys itself. The mapping scheme can be described in code or assembled from annotations. But instead of DQL, classic Query Builders are used.

php
$users = $orm->getRepository(User::class)
    ->select()
    ->where('active', true)
    ->load('orders', [
        'method' => Select::SINGLE_QUERY,
        '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 ORM has been shown to work faster than Doctrine on samples, but slower on persistence. The engine supports complex queries with multiple loading strategies, proxy classes, and embeddings; and it provides portable transactions instead of global EntityManager.

More details on this subject can be heard when our CTO, Anton “JD” Titov, presents at PHPRussia 2020 in May 2020.

The main benefit of the ORM is the ability to change the mapping of data and connections in time, which allows users to define the data schema themselves (DBAL supports database schema introspection and declaration). You can read more about the comparison of Cycle, Eloquent, and Doctrine 2 here. Essentially you are getting your own mini-Drupal.

Rapid Prototyping

Spiral includes several tools to accelerate and simplify development. The main ones include the auto-injection of dependencies, autoconfiguration, and automatic model search using static analysis. Console commands allow you to generate most of the necessary classes and easily extend them.

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

Rapid prototyping

However, all you need to do is run the php app.php prototype:inject -r command, and the prototyping system will automatically remove all the magic:

php
namespace AppController;

use AppDatabaseRepositoryUserRepository;
use SpiralViewsViewsInterface;

class HomeController
{
    /** @var ViewsInterface */
    private $views;
    
    /** @var UserRepository */
    private $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')
        ]);
    }
}
>			

Note
Php-parser is used under the hood*

Security

Since most of our applications are developed for the B2B segment, security issues are taken very seriously.

Spiral gives developers access to components for validation of complex requests (Request Filters), CSRF, and encryption (based on defuse/php-encryption). It protects a users’ cookies and their session using anti-tampering algorithms and encryption.

The framework additionally provides an authentication component based on expiring tokens. Session, database, or pure JWT can be used as a driver.

Access authentication is performed through the RBAC component with some modifications that allow DAC and ABAC mode. There is support for multiple roles, annotations to protect controller methods, and a rule system.

Work with the domain layer is done through an intermediate interceptor layer (Domain Core). This allows you to create special restrictions on the group of controllers, pre-validate data, and handle errors.

Template Engine

If you are a fan of using Twig, just install the extension and use the tools you know and love with Spiral.

The Stemper template engine is also available out-of-the-box providing 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). Stemper can fully work with DOM documents (although it may perform slower if you use specialized tools).

It is possible to parse several nested grammars. For example, you can use Laravel Blade directives and your own DSL (as HTML tags) markup inside one template. This results in some web-components on the server side.

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

<admin.layout.tabs title="User Information"></admin.layout.tabs>
<bundle path="admin/bundle"></bundle>
<tab id="info" title="Information">
  User, {{ $user->name }}
</tab>
<tab id="data" title="User Settings">
  <table for="{{">settings }}>
    <cell title="Key">{{ $key }}</cell>
    <cell title="Value">{{ $value }}</cell>
  </table>
</tab>			

Context-enabled auto-screening is supported (e.g. PHP output inside a JS block automatically converts data to JSON) as are source-maps to handle errors. Templates are compiled into optimized PHP code and then given directly from application memory.

Framework Development

It goes without saying that we have learned many lessons during more than a decade of developing Spiral Framework. Key elements have been fixed, modified, and enhanced for the release of the second version to drive faster, cleaner, superior software development. We hope you like it!