Skip to content

Getting Started

Jakob Tapuć edited this page Jun 29, 2022 · 29 revisions

Introduction

Greetings weary traveler. Before we dive deep into Empress's features, let's just dip our toes first. As you probably already know, Empress is a fully async microframework for PHP 8. We will not discuss how it works under the hood because its internals are really just a layer on top of amphp/http-server. You can refer to its documentation for more details. In this short guide, we'll learn how to install Empress and we'll write our first hello world app. To make things a little bit more interesting, we'll stream our response in intervals.

Installing Empress

First of all, create a Composer project and then add Empress as a dependency:

$ composer init
$ composer require empress-php/empress

This will install the latest version of Empress along with the dependencies we need.

Entry Point

Next, we need to create an entry point for our application that we'll use to run it. Go ahead and create a bin/app.php file with the following contents:

<?php

use Empress\Application;

require_once __DIR__ . '/../vendor/autoload.php';

$app = Application::create(9000);

return $app;

Code layout

Empress is unopinionated when it comes to code layout. Basically, the only thing you need is the bootstrapping code. In theory, you could put all of your code in one file, should you so wish. The bootstrapping file needs to return an instance of Application. Make sure you set up autoloading for your Composer project. Otherwise loading vendor/autoload.php will fail.

Running the app

Now, open up your terminal and run the app using this command:

$ vendor/bin/empress bin/app.php

You will be greeted with this error:

    ______                                   
   / ____/___ ___  ____  ________  __________
  / __/ / __ `__ \/ __ \/ ___/ _ \/ ___/ ___/
 / /___/ / / / / / /_/ / /  /  __(__  |__  ) 
/_____/_/ /_/ /_/ .___/_/   \___/____/____/  
               /_/
Starting Empress... 
Server startup error: Router start failure: no routes registered

An app that cannot respond to any requests is a pretty useless one. Now let's see how we can add some routes to it to fix this.

Collecting Routes

Basics

In Empress, we register routes using the routes() method on the application object:

$app->routes(function (Routes $routes) {
    $routes->get('/', fn (Context $ctx) => $ctx->html('<h1>Hello, Empress!</h1>'));
});

Let's try running the app again to see if that works:

<?php

use Empress\Application;
use Empress\Context;
use Empress\Routing\Routes;

require_once __DIR__ . '/../vendor/autoload.php';

$app = Application::create(9000);

$app->routes(function (Routes $routes) {
    $routes->get('/', fn (Context $ctx) => $ctx->html('<h1>Hello, Empress!</h1>'));
});

return $app;
$ vendor/bin/empress bin/app.php

    ______                                   
   / ____/___ ___  ____  ________  __________
  / __/ / __ `__ \/ __ \/ ___/ _ \/ ___/ ___/
 / /___/ / / / / / /_/ / /  /  __(__  |__  ) 
/_____/_/ /_/ /_/ .___/_/   \___/____/____/  
               /_/
Starting Empress...
[2021-05-04 16:21:07] Empress.info: Listening on http://0.0.0.0:9000/  
[2021-05-04 16:21:07] Empress.info: Listening on http://[::]:9000/  

Yay, we're up and running our first app! Go ahead and open up your browser at http://localhost:9000:

In fact, routes() accepts any callable as its parameter as long as it can be passed a Routes object.

/**
 * @param callable(Routes): void $collector
 */
public function routes(callable $collector): self

Armed with this knowledge we can create our own collector objects. They only need to implement the magic __invoke method.

<?php

use Empress\Application;
use Empress\Context;
use Empress\Routing\Routes;

require_once __DIR__ . '/../vendor/autoload.php';

class MyCollector
{
    public function __invoke(Routes $routes): void
    {
        $routes->get('/', [$this, 'index']);
    }

    public function index(Context $ctx): void
    {
        $ctx->html('<h1>Hello, Empress!</h1>');
    }
}

$app = Application::create(9000);

$app->routes(new MyCollector());

return $app;

If you restart the app you should see the same output in your browser. Wait - restart? Shouldn't it just work after we refresh the page? The answer is no. We're working in CLI mode which means that our code is loaded into memory and stays there for the lifetime of the process. It's important to remember this detail as it'll save you a lot of headaches later on.

Collecting routes with attributes

Since PHP 8 supports native attributes, Empress also provides a trait that you can use to have route collectors that look like controllers in more conventional PHP frameworks.

class MyCollector
{
    use AnnotatedRouteCollectorTrait;

    #[Route('GET', '/')]
    public function index(Context $ctx): void
    {
        $ctx->html('<h1>Hello, Empress!</h1>');
    }
}

You're not forced to extend any class to create a route collector, they are plain old PHP objects. This also makes it very easy to use a dependency injection solution of your choice. One of Empress's design principles is to not get in your way of doing things.

Route types

You can create handlers for all HTTP verbs that amphp/http-server has support for. All attribute routes are compatible with corresponding Routes methods:

$routes->get('/', /* ... */); // corresponds to
#[Route('GET', '/')

$routes->post('/form', /* ... */); // corresponds to
#[Route('POST', '/form')

For more information about handlers, check out Basic Handlers, Handler Groups, and Filters. In this guide, we're going to focus on the most basic GET verb.

The Context Object

In the examples above we used a Context object in our handlers to render HTML output. Accessing requests and responses is done by calling methods on this object.

// Setters
$ctx->response($stringOrStream); // sets the response body
$ctx->html($stringOrSteam); // sets the response body and the content type to HTML
$ctx->json($value); // sets the response body to $value by encoding it as JSON

// Getters
$ctx->cookie($name); // gets a request cookie
$ctx->method(); // gets the request method
$ctx->userAgent(); // gets the user agent

The setters form a fluent interface:

$ctx
    ->html('<h1>Not found</h1>')
    ->status(Status::NOT_FOUND);

Hello, async world!

Even though you're a weary traveler you're also very astute so you must have noticed that response body setters accept either a string or a stream. In Amp, a stream is an abstraction that lets you work with async I/O easily. For more information check out its docs. Meanwhile, let's use a stream that generates some strings over time.

<?php

use Amp\ByteStream\InputStream;
use Amp\ByteStream\IteratorStream;
use Amp\Delayed;
use Amp\Producer;
use Empress\Application;
use Empress\Context;
use Empress\Routing\RouteCollector\AnnotatedRouteCollectorTrait;
use Empress\Routing\RouteCollector\Attribute\Route;

require_once __DIR__ . '/../vendor/autoload.php';

class MyCollector
{
    use AnnotatedRouteCollectorTrait;

    #[Route('GET', '/')]
    public function index(Context $ctx): void
    {
        $ctx->html($this->createStream());
    }

    private function createStream(): InputStream
    {
        return new IteratorStream(new Producer(function (callable $emit) {
            for ($i = 0; $i < 10; $i++) {
                yield $emit("Hello, Empress! Line: #$i\n");

                yield new Delayed(500);
            }
        }));
    }
}

$app = Application::create(9000);

$app->routes(new MyCollector());

return $app;

Now restart the server and then check out the response with curl:

$ curl localhost:9000
Hello, Empress! Line: #0
Hello, Empress! Line: #1
Hello, Empress! Line: #2
Hello, Empress! Line: #3
Hello, Empress! Line: #4
Hello, Empress! Line: #5
Hello, Empress! Line: #6
Hello, Empress! Line: #7
Hello, Empress! Line: #8
Hello, Empress! Line: #9

You should see values appearing on your screen in 0.5-sec intervals. We've just created our first Empress app that takes advantage of asynchronous response streaming. Phew!

Further Reading

Now that was a lot of stuff to process, right? Empress is so much more than this. Paired with such a powerful solution that Amp is, you can do awesome things with it. Make sure to check out the rest of the docs to find out more. Maybe continue with Basic Handlers?