5

Resonance – New Asynchronous PHP Framework

 8 months ago
source link: https://devm.io/php/asynchronous-php-resonance-001
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Asynchronous Where It Matters the Most

Resonance – New Asynchronous PHP Framework


Resonance is a new PHP framework designed from the ground up to facilitate interoperability and messaging between services in your infrastructure and beyond. I built it on top of Swoole, which grants it asynchronous capabilities and, in turn, support for WebSockets, RPC, parallel GraphQL field resolution, and other technologies that are native to PHP.

In this article, I will discuss the modern PHP state and asynchronous programming. Then, I will explain what benefits Resonance can bring to this ecosystem.

Modern PHP Context

PHP is popular because of how easy it is to use it to create new features, do prototyping, and expose APIs. It is no longer just a hypertext processor or a REST API engine. With the introduction of modern asynchronous features like coroutines, fibers, and event loops, it can be a central communication hub and entry point to your infrastructure.

Central Infrastructure Hub

Since it's easy to prototype and develop features in PHP, it's often a natural choice to start projects with it. Most projects start with some central service that expands and incorporates more and more features. At the same time, you can continue almost indefinitely with a monolith. Usually, the application morphs into a set of microservices, often written in multiple specialized languages.

After that, we have a central PHP service, from which all of the above stemmed, that has to make many API and RPC calls - which are still synchronous and usually less performant and maintainable.

Optimize the Code for Rewiring

What if you want to replace parts of your application with microservices or merge two services? Can we mix and match features as we need them? Is your PHP Markdown parser too slow? Why don't you use some Rust applications and make a gRPC call? Do you want to proxy a specific route to some different microservice? Just forward the request from a Controller.

Those are the types of tasks that consume the most time and resources – rewiring the code, expanding, merging, factoring – not just writing new features. What if we can optimize our workflow to handle that? We will need all the connectivity features – GraphQL, RPC, WebSockets, etc.

Asynchronous and Long-Running PHP

I sometimes encounter PHP developers who express concerns that asynchronous code makes PHP unnecessarily complex and more complicated to read and maintain – that it can produce some issues that we do not encounter with synchronous code.

That is valid because there are new issues we didn't have to deal with in PHP – memory leaks, Garbage Collector overhead, and connections pooling (instead of just opening one connection when a script starts). It wouldn't make sense to deliver into that without some great benefits.

Some Benefits of Long-Running Scripts

By long-running scripts, I mean PHP with a built-in server or event loop that can respond to multiple requests.

In the classic PHP model, in significant simplification, when a web server receives an HTTP request, it starts a PHP script, waits for it to generate the HTTP response, and then terminates that script. Rinse and repeat for each incoming request. Of course, there are lots of optimizations like JIT Compiling, OPCache, and FPM worker pools, but the general idea remains the same.

Asynchronous Is Often Simpler

During each request, the framework you use has to bootstrap its services every time – usually to read configuration files, autoload classes, build the Dependency Injection Container, and instantiate controllers. That is why caching became a second nature to many PHP developers – there is just no performant enough way to handle this other than to cache whatever is possible, to make the application's bootstrap phase as fast as possible – but no matter what we do, the overhead is still there.

If instead we will use a Long-Running script, we can achieve two things:

  1. We don't have to cache services, settings, or anything to make the application bootstrap faster. Even if it takes a second to start the script (which is not viable by any means with FPM, for example – because that would mean every HTTP request to that application takes a second longer), it doesn't make much real difference – we can make our code much more straightforward.
  2. There is no bootstrap overhead when handling HTTP requests, WebSocket messages, or RPC messages - your application can bootstrap just once and then run incoming connections.

So, considering we don't have to do as much caching, our code can be simpler. Another case is, for example, the popular webonyx/graphql-php library recommends lazy-loading of GraphQL types: https://webonyx.github.io/graphql-php/schema-definition/#lazy-loading-of-types. That is because PHP has to construct the entire schema during each GraphQL request – which may take a long time. To alleviate that, you have to implement lazy-loading of that schema's fields to load just the necessary parts during requests.

With a long-running PHP script, you can load the entire schema upfront during the application bootstrap phase.

That means the code is not necessarily more complicated than the synchronous approach; it's simpler.

Deferring Actions Until After the Response

Since Swoole runs in an event loop, it's a matter of scheduling a callback with its Event::defer function, which is similar to the Go Language defer statement. Contrary to Go, where such deferred statements execute after the current context is closed – in Swoole, they run in the next event loop tick. This way, you can schedule something just before sending the response back, and Swoole will execute that code after the response is sent. You do not always have to use queues and other mechanisms to perform more time-consuming tasks later.

You can achieve a similar effect with fastcgi_finish_request or similar functions with synchronous PHP, but they usually keep the current CGI worker alive, not allowing it to handle new requests. With Swoole, the processing of new requests won't be stopped.

Swoole's Transparent Upgrades

Swoole has a system of low-level hooks that take some PHP features (like PDO or Curl) that are typically synchronous and make them asynchronous. It does so transparently by using its internal scheduler.

For example, when you make an SQL query through PDO, it stops the PHP execution at that point but preserves its stack trace (similar to how Fibers work). Then, it switches to some different tasks, like handling some other API requests. When the SQL result comes back, it resumes the initial PHP stack trace and resumes the function execution. The same goes for most IO operations – parallelism is transparent.

You can still explicitly create coroutines, but even if you don't, you will still benefit from asynchronous features.

Some Asynchronous Code Challenges

Array Memory Usage

The most significant new challenge is dealing with memory management. What surprised me initially is – PHP arrays always hold up to their allocated memory, even if you remove their elements – so you should avoid using those with dynamic collections. Luckily, we have a wonderful Data Structures (https://www.php.net/manual/en/book.ds.php) extension. Its collections have excellent memory management, and they resize dynamically.

Garbage Collector Overhead

It's not an issue with a synchronous PHP because scripts are short-lived, and the Garbage Collector usually isn't a factor, but with long-running apps, it can be. PHP uses a synchronous algorithm for collecting circular object references, which runs periodically – it relies on the references buffer: if it gets too large, the Garbage Collector cycle starts. When it runs too often, it may cause minor but noticeable – and seemingly random - CPU usage spikes in the application. What can you do to mitigate it? I think the best way is not to try managing the Garbage Collector manually (turning it on/off, manually starting cycles, and doing similar shenanigans) but to design your code to be stateless instead: use readonly classes as much as possible, use WeakMap wherever possible to not create new object references.

Race Conditions

It might be surprising, but it's hardly a factor with Swoole, as it relies on explicitly using coroutines, so usually, it's enough to start a few coroutines in parallel and use a WaitGroup (which is a common concept) to wait until they finish. Swoole's scheduler is going to take care of the rest. To have race condition issues, you would have to start a coroutine or an event loop and not explicitly wait for it to finish its task, which is easy to notice and deal with.

Enter the Resonance

Resonance can bring multiple benefits to an asynchronous PHP environment. It is not only designed from the ground up to run in such environments but also offers productivity and features that can make your code both simpler to maintain and to make the development process more manageable.

Asynchronous Where It Matters

Resonance is designed from the ground up to work with Swoole, which means it takes care of most asynchronous code issues you might have. It keeps the things that are already simple, like writing HTTP Controllers or using Doctrine, still the same as with synchronous code.

Instead of reinventing whatever already works, it opens up new possibilities with asynchronous WebSocket message handlers or asynchronous resolution of GraphQL fields.

Memory Conscious Dependency Injection

The Dependency Injection Container only allows the application to have only one instance of a given service, which minimizes memory usage and enforces the services to be stateless. It usually uses WeakMap if it needs to store some data related to a current request.

That doesn't mean you have to use Singletons – you can make as many instances of a given service as you need, but the Container will use just one instance of a service. Thanks to that, you can be sure it won't rebuild the entire GraphQL schema or won't instantiate resource-demanding services during an HTTP request.

Dependencies are also topologically sorted, so it detects cyclic dependencies between services.

Minimal Configuration

There is only one configuration file used – with parameters like the database name or connection strings – there are no configuration files to handle the application logic. You can set up everything through the use of attributes. That is because the framework is optimized for rewiring and reorganizing content. Do you need to remove a few controllers? Just delete the controller files. You don't have to update the router or dependency injection configuration. Do you need to move a few endpoints in from a different microservice? Just cut and paste a few controller files – no extra setup required.

Transparent Handling of Connection Pools

Resonance provides a convenient wrapper around Swoole's connection pools, so it's unnecessary to manually manage them (borrowing connections from the pool and manually putting them back). Resonance also provides an integration with Doctrine.

GraphQL + PHP Promises

Resonance supports GraphQL out of the box with a code-first approach. You can construct the entire schema by combining types and registering root queries. Under the hood, it uses the popular https://webonyx.github.io/graphql-php/ library, with adjustments to work with the async environment.

Swoole Futures

Resonance implements Futures (partial Promise/A+ implementation) on top of Swoole's coroutines. Futures are thenable objects you can use in most frameworks' features, including GraphQL schemas.

For example, let's define the following schema field:

final class PingType extends ObjectType
{
    public function __construct()
    {
        parent::__construct([
            'name' => 'Ping',
            'description' => 'Test field',
            'fields' => [
                'message' => [
                    'type' => Type::string(),
                    'description' => 'Always responds with pong',
                    'resolve' => $this->resolveMessage(...),
                ],
            ],
        ]);
    }
    private function resolveMessage(): SwooleFuture
    {
        return new SwooleFuture(function () {
            sleep(1);
            return 'pong';
        });
    }
}

Then, this GraphQL query is going to take ~1 second (instead of 3 seconds) because Resonance executes all the GraphQL resolvers in parallel:

query MultiPing() {
    ping1: ping { message }
    ping2: ping { message }
    ping3: ping { message }
}

Reusing SQL Queries in GraphQL

Resonance can execute SQL queries once for each related field in the GraphQL query. For example, you can return SQL Query objects as a schema resolution field:

#[GraphQLRootField(name: 'blogPost')]
#[Singleton(collection: SingletonCollection::GraphQLRootField)]
final readonly class BlogPost implements GraphQLFieldableInterface
{
    public function __construct(
        private DatabaseConnectionPoolRepository $databaseConnectionPoolRepository,
        private BlogPostType $blogPostType,
    ) {}
    public function resolve(null $rootValue, array $args): GraphQLReusableDatabaseQueryInterface
    {
        return new SelectBlogPostBySlug(
            $this->databaseConnectionPoolRepository,
            $args['slug'],
        );
    }
    public function toGraphQLField(): array
    {
        return [
            'type' => $this->blogPostType,
            'args' => [
                'slug' => [
                    'type' => Type::nonNull(Type::string()),
                ],
            ],
            'resolve' => $this->resolve(...),
        ];
    }
}

Then, for example, the following GraphQL query is going to issue just two SQL queries (one for "foo" and the second one for "bar" blog post):

query Query {
    blogPost1: blogPost(slug: "foo") {
        title
    }
    blogPost2: blogPost(slug: "foo") {
        title
    }
    blogPost3: blogPost(slug: "foo") {
        title
    }
    blogPost4: blogPost(slug: "bar") {
        title
    }
}

WebSockets

Using WebSockets with asynchronous PHP opens up new possibilities – the same process can respond to HTTP and WebSocket requests. Resonance handles the handshake and subprotocol negotiation.

It comes with a bundled simple RPC protocol to which you can register handlers using attributes.

#[RespondsToWebSocketRPC(RPCMethod::Echo)]
#[Singleton(collection: SingletonCollection::WebSocketRPCResponder)]
final readonly class EchoResponder extends WebSocketRPCResponder
{
    protected function onRequest(
        WebSocketAuthResolution $webSocketAuthResolution,
        WebSocketConnection $webSocketConnection,
        RPCRequest $rpcRequest,
    ): void {
        $webSocketConnection->push(new RPCResponse(
            $rpcRequest->requestId,
            (string) $rpcRequest->payload,
        ));
    }
}

Enforced Security Features

Security is a must, so wherever possible, security features are enforced. For example, if you want to load a Doctrine entity as a controller parameter, specify an intent (Read, Update, or Delete). Using this information, Resonance will forward the request to authorization gates and determine if the current user has adequate permissions to act.

#[RespondsToHttp(
    method: RequestMethod::GET,
    pattern: '/blog/{blog_post_slug}',
    routeSymbol: HttpRouteSymbol::BlogPostShow,
)]
#[Singleton(collection: SingletonCollection::HttpResponder)]
final readonly class BlogPostShow extends HttpController
{
    public function handle(
        #[DoctrineEntityRouteParameter(
            from: 'blog_post_slug',
            intent: CrudAction::Read,
            lookupField: 'slug',
        )]
        BlogPost $blogPost,
    ): HttpInterceptableInterface {
        return new TwigTemplate('blog_post.twig', [
            'blog_post' => $blogPost,
        ]);
    }
}

The authorization gate:

#[DecidesCrudAction]
#[Singleton(collection: SingletonCollection::CrudActionGate)]
readonly class BlogPostGate extends CrudActionGate
{
    public function can(
        ?UserInterface $user,
        CrudActionSubjectInterface $subject,
        CrudAction $crudAction
    ): bool {
        return match ($crudAction) {
            CrudAction::Delete,
            CrudAction::Update => $this->isAdmin($user),
            CrudAction::Read => $subject->isPublished || $this->isAdmin($user),
        };
    }
    public function isAdmin(?UserInterface $user): bool
    {
        return true === $user?->getRole()->isAtLeast(Role::Admin);
    }
}

Built-In Static Site Generator

Resonance has a bundled static site generator. After developing features or starting a project (whether internal to the company or external), it's worthwhile to document and present it appropriately. To do so, you can use the technical stack and setup you already have, so you don't have to install additional dependencies to build docs with Jekyll or Docusaurus.

https://resonance.distantmagic.com/ is built with Resonance's static site generator.

What's Next

The above features make a solid foundation for Resonance. Currently, we are working on low-code and visual coding solutions using those technologies (that are not released yet), so we plan to support and expand Resonance far into the foreseeable future.

We encourage you to join our community and try the framework out, either in a personal project or by implementing some microservice to see how well asynchronous PHP can perform in various environments.

Additional Resources

Mateusz Charytoniuk
Mateusz Charytoniuk

Mateusz Charytoniuk is a seasoned software developer and architect with over 15 years of experience. Throughout his career, he has been creating scalable, high-quality software architectures for various organizations and startups, either as an architect or an independent consultant. He specializes in delivering solutions for complex technical problems, workflow automation, custom product development, and creating systems architectures from the ground up. Mateusz is currently leading the development of Resonance, an asynchronous PHP framework built on top of Swoole, designed for seamless interoperability and messaging between services in modern apps’ infrastructures.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK