Async Expressive

Matthew Weier O'Phinney

Terminology

  • PHP-FIG (or FIG): Framework Interop Group (standards body)
  • PSR-7: HTTP Message interfaces
  • PSR-15: HTTP Request Handler interfaces
  • PSR-17: HTTP Message Factory interfaces
  • PSR-14: Event Dispatcher interfaces

Goals

Demonstrate how async enables

  • Application-specific servers
  • Better performance, generally due to...
  • Deferred processing.

Node.js

Node.js

event loop

Event Loop; image copyright Bartolomeo Sorrentino

Why?

Shared Nothing

Shared Nothing

Message Queues

Message Queue; image copyright Stackify

Parallel Processing

Parallel Processing; image copyright Michael Kloran

Async Programming

Callbacks


executeSomeAsyncProcess(
    $withData,
    $andA,
    $callbackToExecuteOnCompletion
);
          

function ($error, $result)
{
    if ($error) {
    }
}
            

Event Dispatcher


$dispatcher->addListener(function (SomeEvent $e) {
    // do work
});

// later:
$dispatcher->trigger($someEvent);
          

Callback Hell


doFirst($payload, function ($err, $result) {
    doSecond($result, function ($err, $result) {
        doThird($result, function ($err, $result) {
            // finally got what we needed
        });
    });
});
          

Promises


$promise = someAsyncOperationReturningAPromise($with, $data);
$promise
    ->then($someCallbackToExecuteOnResolution)
    ->then($someAdditionalCallbackToExecuteOnResolutionOfPrevious)
    ->catch($someCallbackToExecuteOnRejection);
          

where:


function then($successCallback = null, $rejectionCallback = null);
            

and catch() is a shortcut for:


$promise->then(null, $someCallbackToExecuteOnRejection);
            

async/await


async function ping() {
  const res = await fetch('/api/ping');
  return await res.json();
}

let ack = ping();
          

Coroutines


$result = $statement->execute($data);
          

Swoole provides...

  • an event loop
  • async HTTP, network, and socket clients
  • network servers

Swoole features

  • Spawning multiple workers per server
  • Spawning separate task workers
  • Daemonization of servers
  • Coroutine support for many TCP/UDP and socket operations

A Basic Web Server


use Swoole\Http\Server as HttpServer;

$server = new HttpServer('127.0.0.1', 9000);
$server->on('start', function ($server) {
    echo "Server started at http://127.0.0.1:9000\n";
});
$server->on('request', function ($request, $response) {
    $response->header('Content-Type', 'text/plain');
    $response->end("Hello World\n");
});
$server->start();
          

$response->end() is problematic

If you forget to call it:

  • the connection will remain open until a network timeout occurs;
  • the current process will remain open; which means
  • no next tick of the event loop.

Deferment


$server->defer(function () {
    // work to defer
});
          

Task Workers

Prepare the server
for tasks

  • Configure the number of task workers to use (required!)
  • Register a listener to handle incoming tasks.
  • Register a listener to execute on task completion.

Registering task workers


$server->set(['task_worker_num' => 4]);
$server->on('task', function ($server, $taskId, $data) {
    // Handle task

    // Finish task:
    $server->finish('');
});
$server->on('finish', function ($server, $taskId, $returnValue) {
    // Task is complete
});
          

Triggering a task


$server->task($someData);
          

A Basic TaskWorker


class Task {
    public $handler;   // callable
    public $arguments; // array
}

$server->on('task', function ($server, $taskId, $task) {
    if (! $task instanceof Task) {
        $server->finish('');
        return;
    }

    ($task->handler)(...$task->arguments);
    $server->finish('');
});
          

A note on debugging

Coroutine support in Swoole is incompatible with XDebug and XHProf!

What is Expressive?

Expressive is a middleware application framework built on PSR-15


namespace Psr\Http\Server;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;

interface RequestHandlerInterface {
    public function handle(Request $request) : Response;
}

interface MiddlewareInterface {
    public function process(
        Request $request,
        RequestHandlerInterface $handler
    ) : Response;
}
          

Middleware Flow

Middleware; image copyright Sergey Zhuk

Expressive also provides...

  • Dependency Injection wiring abstraction
  • Routing abstraction
  • Template abstraction
  • Error handling
  • Application and per-route pipelines

Middleware + Swoole


$server->on('request', function ($request, $response) use ($app) {
    $appResponse = $app->handle(transformRequest($request));
    transformResponse($response, $appResponse);
});
          

Codified in zend-expressive-swoole

Starting your Expressive+Swoole server


$ ./vendor/bin/zend-expressive-swoole start
          

Async Operations

Coroutines


$result = $mysqli->query($sql);
while ($data = $result->fetch_assoc()) {
    // ...
}
          

Deferment


$server->defer($callback);
          

Task Workers


$server->task($someData);
          

phly/phly-swoole-taskworker


use Phly\Swoole\TaskWorker\Task;

$server->task(new Task(function ($event) {
    // handle the event
}, $event));
          

Listener deferment


$listener = new QueueableListener($server, $listener);

// Where QueueableListener is equivalent to:
function (object $event) use ($server, $listener) : void {
    $server->task(new Task($listener, $event));
}
          

In your own code:


$this->dispatcher->dispatch($someEvent);
            

Daemonization


return [
    'zend-expressive-swoole' => [
        'enable_coroutine' => true,
        'swoole-http-server' => [
            'mode' => SWOOLE_PROCESS,
        ],
    ],
];
          

$ ./vendor/bin/zend-expressive-swoole start --daemonize
          

Dockerization


# DOCKER-VERSION        1.3.2

FROM mwop/phly-docker-php-swoole:7.2-alpine

# Project files
COPY . /var/www/

# Reset "local"/development config files
WORKDIR /var/www
RUN rm -f config/development.config.php && \
  rm config/autoload/*.local.php && \
  mv config/autoload/local.php.dist config/autoload/local.php

# Overwrite entrypoint
RUN echo "#!/usr/bin/env bash
  (cd /var/www ; ./vendor/bin/zend-expressive-swoole start)
" > /usr/local/bin/entrypoint
          

Pitfalls

Pitfalls; image copyright Activision

Stateful Services

Stateful Services: Templating


$template->addDefaultParam('*', 'user', $user);
          

$metadata = $resourceGenerator
    ->getMetadataMap()
    ->get(Collection::class);
$metadata->setQueryStringArguments(array_merge(
    $metadata->getQueryStringArguments(),
    ['query' => $query]
));
          

Stateful Services: Validation


if (! $validator->isValid($value)) {
    $messages = $validator->getMessages();
}
          

echo implode("
", $validator->getMessages())

Stateful Services: Auth


return $handler(
    $request->withAttribute('user', $auth->getIdentity())
);
          

Resolving State Issues

Decoration


class StatelessVariant implements SomeInterface
{
    private $proxy;

    public function __construct(SomeInterface $proxy)
    {
        $this->proxy = $proxy;
    }
}
          

public function morphState($data) : void
{
    throw new CannotChangeStateException();
}
          

Extension


class StatelessVariant extends OriginalClass
{
    public function morphState($data) : void
    {
        throw new CannotChangeStateException();
    }
}
          

Factories as Services


public function __construct(SomeClass $dependency) : void
          

becomes:


public function __construct(SomeClassFactory $factory) : void
            

and we then:


$dependency = ($this->factory)();
            

Stateful Messages

Pass stateful data to the service:


$allowed = $this->acl->isUserAllowed(
    $request->getAttribute('user')
);
          

or the request:


$allowed = $this->acl->isRequestAllowed($request);
            

Request Attributes

Instead of:


$template->addDefaultParam(/* ... */);
          

We use a request attribute


public function process(
    Request $request,
    RequestHandler $handler
) : Response {
    return $handler->handle(
        $request->withAttribute('template_params', (object) [])
    );
}
            

Request Attributes (cont.)

to which we push data:


$params = $request->getAttribute('template_params');
$params->user = $this->deriveUserFromRequest($request);
return $handler->handle($request);
          

Request Attributes (cont.)


$templateParams = $request->getAttribute('template_params');
return new HtmlResponse($this->renderer->render(
    'some::template',
    array_merge((array) $templateParams, [
        // more specific parameters
    ])
));
          

Sessions

Use zend-expressive-session

and its zend-expressive-session-cache adapter

Wrap Up

Benefits

  • Single bootstrap
  • Deferment
  • Single container services

Practices

  • Abstract async-specific details.
  • Make container services stateless
  • Aggregate state in the requst
  • Avoid the session extension

Resources

Thank You!

Contact me at mwop.net

I tweet @mwop