Tempest 3.x Usage Guide

by Marty Wallace

Installation

You can install Tempest as a composer package:

$ composer require martywallace/tempest

Setup

The initial setup of a Tempest project involves defining your main application class. This class is responsible for binding your application services and initialising any other application level behaviour (e.g. how exceptions are handled).

Please note that this tutorial is for the 3.x branch of Tempest.

Once you've declare your application class, you should instantiate it as the first action in your index.php file (or whichever file your web server is configured to hit when a request comes through). When you instantiate the application, you must provide the path to the root directory of your application (where your public folder, vendor folder etc live) and a path to a .php configuration file relative to the root.

$app = App::instantiate(realpath(__DIR__ . '/../'), '/config.php');

This will bootstrap your application and its components. The final step is to then to start() the application, which boots the router with the routes you've configured.

$app->start();

Configuration

Tempest applications can be configured by environment using a central configuration file. The file should be a PHP file that returns your configuration as an array.

The configuration you provide can be accessed in your application via the Configuration class, attached to the main app class as config, so assuming a config file that looks like this:

<?php

return [
  'someProperty' => 'abc123'
];

The value abc123 can be accessed like:

$value = $app->config->get('someProperty');

You can use dot (.) delimited property name to access values of nested arrays in the configuration e.g. $app->config->get('keys.mandrill').

To enable multi-environment configuration, the top level of the configuration array must contain a key * whose value is an array containing your default configuration set, e.g.

<?php

return [
  '*' => [
    'someProperty' => 'abc123'
  ]
];

The Environment class contains constants for common environments as well as this special default environment (*) available as Environment::ALL.

Once the default set is defined, you are able to define additional sets to override the default values in alternate environments. The set to use is based on an environment variable called TEMPEST_ENV which you can define in the virtual host for your application.

Assuming a config file like:

<?php

use Tempest\Environment;

return [
  Environment::ALL => [
    'key' => 'dev_key'
  ],
  Environment::PROD => [
    'key' => 'live_key'
  ]
];

The following with yield live_key if the TEMPEST_ENV is set to prod, otherwise returning dev_key.

$value = $app->config->get('key');
The default environment if one is not set is dev. You can change the environment variable used using Environment::setEnvironmentVarName('X') if needed as well.

Routing

Routes in your application are defined in your application configuration as an array using the routes key. Each item in the routes array should be an array containing one of the following combinations of information:

  • The route to match and the controller method.
  • The route to match, the HTTP method required by the request and the controller method.
  • The route to match, the HTTP method required by the request, one or more middleware methods and the controller method.

Controller and middleware methods are strings in the format ClassName::methodName.

If ::methodName is omitted, ::index is assumed, so Controller is equivalent to Controller::index

Examples:

<?php

return [
  'routes' => [
    ['/', 'Controller'],
    ['/login', 'POST', 'Controller::login'],
    ['/admin', 'GET', 'AuthMiddleware::isAdmin', 'Controller::admin']
  ]
];

Route parameters can be defined using the {param} syntax, e.g.

/users/{id}

The router sits over the popular FastRoute router; more information about how to define routes and route parameters can be found on their GitHub page.

If no route is defined, Tempest will attempt to load a template in your templates directory with the same name as the route being requested. For example:

/about

Will look for a template about.html in your templates directory if there is no route for /about defined in your configuration. This is convenient for serving up pages easily without needing to define a route and a controller method.

In the case of a template that might depend on some data being injected by your controller, you obviously won't want to be able to visit /yourtemplate and view the incomplete file. If the template or any of the ancestor directories that the template sits in begins with an underscore (_), the behaviour is omitted and you will fall back to a 404 Not Found response.

Note: This behaviour follows templates in subdirectories of your templates directory, so /about/john will attempt to load a template /yourtemplates/about/john.html.

If your application returns a HTTP status outside of the 2xx range and contains no body (null, false or an empty string), it will attempt to load a template named after the status code - first in your template root directory, falling back to possible internal templates. For example, if you create a 404.html in your templates directory, that will be served as the body for all 404 Not Found responses. This allows you to easily create, for example, a 403.html to display on all Forbidden resources.

Controllers and Middleware

Controllers and Middleware handle incoming requests to your application. Both are simply objects that contain methods that the router can reference as described above, which could look like:

<?php

use Tempest\Http\Controller;
use Tempest\Http\Request;
use Tempest\Http\Response;

class MyController extends Controller {
  public function index(Request $request, Response $response) {
    return 'Hello!';
  }
}

All controller and middleware methods called by your router are provided a Request and Response object. The Request object allows you to capture request data and headers, whereas the Response object allows you to set response content and headers.

To access GET or POST data you can use:

$request->data('example');

And to access dynamic route values defined by route parameters (explained above), you can use:

$request->named('id');

In a Controller, you can set the response body by either assigning it directly to the body of the Response or by simply returning it from the controller method:

public function index(Request $request, Response $response) {
  // Either is valid:
  $response->body = 'Hello!'; // or simply:
  return 'Hello';
}

Note: If the response body can be serialized into JSON, it is encoded as JSON and the content-type is automatically set to application/json, meaning you can easily return arrays or class instances without needing to use json_encode() on them first.

Middleware should return true or false. If any middleware attached to a route returns false, the series is terminated (subsequent middleware and the controller are not executed) and the current state of the Response returned immediately. This is useful for things like this simple authentication middleware:

<?php

use Tempest\Http\Middleware;
use Tempest\Http\Request;
use Tempest\Http\Response;
use Tempest\Http\Status;

class AuthMiddleware extends Middleware {
  public function auth(Request $request, Response $response) {
    if ($request->data('auth') !== 'validpassword') {
      $response->status = Status::UNAUTHORIZED;
      $response->body = 'Invalid password.'

      return false;
    }

    return true;
  }
}

Services

Services are a core concept of Tempest. They provide the "meat" of your application and represent where the bulk of your application logic lives. In an ideal scenario, controllers will simply parse services data from the request made to the application and output the result of calling those service methods.

To define a service you simply create a class that implements the Service contract:

<?php

use Tempest\Services\Service;

class MyService implements Service {
  public function setup() { }

  public function doSomething() {
    echo 'It works!'
  }
}

The setup method is called the first time your service is accessed in the lifetime of the application. This is allows you to execute expensive setup code here rather than in the constructor for each service, saving resources on services that aren't used during certain requests.

Once you've defined your service, you can attach it to your application instance by including it in the list of services that it defines in the services() function:

<?php

use Tempest\Tempest;

class App extends Tempest {
  protected function services() {
    return [
      'myservice' => new MyService()
    ];
  }
}

Now your service can be accessed directly through your app instance via the key associated with the service:

$app->myservice->doSomething();

Adding a @property-read declarations to your App class will provide valuable code hints for your services in IDEs that support them e.g. @property-read MyService $myservice in this case.

Templates

Twig is used as the template language in Tempest. All Twig templates automatically have a global reference to your application instance as app, meaning you can easily access your services directly in any of your templates, e.g.

{% for post in app.blog.getPosts() %}
  <article>
    <h3>{{ post.title }}</h3>
    <p>{{ post.body }}</p>
  </article>
{% endfor %}

You can render Twig templates from your controllers using the twig service automatically bound to your Tempest applications:

public function index(Request $request, Response $response) {
  return App::get()->twig
    ->render('template.html', ['data' => 'somevalue']);
}

If you'd like to create your own set of Twig extensions, you can declare your set of extensions and add them in your application's setup() method easily via $this->twig->addExtension(...).

There are a handful of helpful Twig filters and functions built into Tempest. They can be reviewed by looking at the Tempest\Extensions\TwigExtensions class.

  • php
  • framework