Outpost is an object-oriented web framework written in PHP. It was designed to power the public-facing frontend in a decoupled website environment.
Outpost is developed by Pixo, in Urbana, IL.
Routing. Outpost uses Phroute to route an incoming request to the correct responder.
HTTP. Outpost provides a common Guzzle client for fetching resources from other websites.
Caching. Each Outpost site has a Stash instance for storing resources between requests.
Templates. Outpost provides no default support for templating, and places no restrictions on what templating engines it can be used with.
Outpost requires PHP 5.5+, and the following libraries:
Create a new directory for your Outpost installation. From inside this directory, use Composer to install Outpost:
composer require pixo/outpost
You should now have composer.json
and composer.lock
files, and a vendor
directory containing Outpost and its dependencies.
Create a new directory called public
, and in that directory make a new file called index.php
with the following contents:
<?php
# Get the Composer autoloader
require_once __DIR__ . '/../vendor/autoload.php';
# Get the incoming request
$request = Symfony\Component\HttpFoundation\Request::createFromGlobals();
# Create a Site object
$site = new Outpost\Site();
# Add an example route
$site->route('GET', '/', function () { print "Hello."; });
# Send a response
$site->respond($request);
Start a local PHP development server and point it at the public
directory:
php -S 0.0.0.0:8080 -t public
Once the server is running, you should be able to visit http://localhost:8080/ and see the following:
Hello.
Outpost received a request for the home page, and it routed the request to a function that printed "Hello."
When you visited http://localhost:8080/, the server ran the index.php
script, which first included the autoloader generated by Composer. It then used the HttpFoundation library to make a new Request object, using the request information from the server.
The script next created an Outpost Site object, and added one routing instruction: when a visitor asks for the home page, run this function. Functions or other callables used as the targets of routes are called Responders.
Finally, the new Site's respond()
method was called. The router used the Request object to find the right Responder, which here was a function that printed "Hello."
Site objects have two primary purposes:
- They respond to incoming requests.
- They provide resources to other objects.
Though it's rare to use more than one Site object at a time, it's common for one Outpost installation to have several Site classes, each tailored to a specific environment. You might start with a base class:
abstract class Site extends \Outpost\Site {
protected function hideErrors() {
error_reporting(0);
ini_set('display_errors', false);
}
protected function showErrors() {
error_reporting(E_ALL);
ini_set('display_errors', true);
}
# This is the domain for the real site content
protected function getRealContentDomain() {
return 'https://cms.example.com/';
}
# This domain has fake content for testing
protected function getFakeContentDomain() {
return 'https://fake.cms.example.com/';
}
}
With this foundation in place, you can create as many Site subclasses as you need:
class ProductionSite extends Site {
public function __construct() {
$this->hideErrors();
}
# Override the default options for the web client
protected function getClientOptions() {
return ['base_url' => $this->getRealContentDomain()];
}
}
class StagingSite extends ProductionSite {
public function __construct() {
$this->showErrors();
}
}
class DevelopmentSite extends Site {
public function __construct() {
$this->showErrors();
}
protected function getClientOptions() {
return ['base_url' => $this->getFakeContentDomain()];
}
}
You can use the Request object to pick a site based on the requested domain:
switch ($request->getHttpHost()) {
case 'example.local':
$site = new DevelopmentSite();
break;
case 'staging.example.com':
$site = new StagingSite();
break;
case 'example.com':
default:
$site = new ProductionSite();
}
$site->respond($request);
An Outpost installation may define any number of Resource classes, to be consulted and combined while preparing responses. Resources are retrieved using the Site's get()
method. The simplest resource is just a callable:
$resource = function () { return 1; }
print $site->get($resource);
The get()
method invokes the callable and returns the result, so the output would be:
1
Resources that extend the SiteResource
class have access to the Site object from which they were requested:
class ExampleSiteResource extends \Outpost\Resources\SiteResource {
public function __invoke() {
# Use `getSite()` to access the current `Site` object:
$site = $this->getSite();
# Fetch additional resources
# The `get()` method is an alias for `getSite()->get()`:
$articles = $this->get(new ArticlesResource());
}
}
Outpost provides some prebuilt Resource classes for fetching content from remote services:
# Returns a Guzzle Response object
$site->get(new \Outpost\Resources\RemoteResource('http://example.com/api'));
# Returns parsed JSON content
$site->get(new \Outpost\Resources\RemoteJsonResource('http://example.com/api.json'));
# Returns a SimpleXML object
$site->get(new \Outpost\Resources\RemoteXmlResource('http://example.com/api.xml'));
Default options for the web client can be changed by overriding the getClientOptions()
method of the Site class:
class DecoupledSite extends \Outpost\Site {
protected function getClientOptions() {
return ['base_url' => 'http://cms.example.com/'];
}
}
The underlying Guzzle object is available via the Site::getClient()
method:
$site->getClient()->createRequest('GET', 'http://example.com/');
Resources that implement CacheableInterface
can be stored in the site cache, and are only invoked when the cached resource is missing or stale. Cacheable resources have a unique key, and specify the number of seconds they may be cached before a refresh is required.
class ExampleExpensiveResource implements \Outpost\Cache\CacheableInterface {
public function __invoke() {
# Something that takes a long time, then...
return $this;
}
public function getCacheKey() {
return 'examples/expensive';
}
public function getCacheLifetime() {
return 3600; # 1 hour
}
}
The first time this resource is requested, it is invoked, and the return value is stored in the site cache. For subsequent requests, the cached copy is returned, until the copy is older than the value of getCacheLifetime()
.
# Nothing in the cache for this call, so Outpost invokes the Resource
# and caches the return value.
$fresh = $site->get(new ExampleExpensiveResource());
# This time the Resource is in the cache, so Outpost returns the cached Resource.
$cached = $site->get(new ExampleExpensiveResource());
# An hour passes...
# Now the cached copy is stale, so Outpost will invoke the Resource again,
# and replace the cached copy.
$fresh = $site->get(new ExampleExpensiveResource());
The Site::getCache()
method provides access to the underlying Stash cache object.
# Clear a specific key
$site->getCache()->clear('cache/key');
# Clear a range of keys
$site->getCache()->clear('things/id/*');
# Clear the whole cache
$site->getCache()->clear();
# Flush the cache
$site->getCache()->flush();
Responders are a specialized class of resource, designed to act as router callbacks. Responders are expected to output a response when invoked. Their return values are disregarded.
In addition to all SiteResource
methods, Responders have access to the current request via the getRequest()
method:
class HomePage extends Responder {
public function __invoke() {
$content = $this->getContent();
print $this->render('home.tpl', $content);
}
protected function getContent() {
return $this->get(new HomePageContent());
}
protected function render($template, array $variables) {
return $this->getTemplateParser()->render($template, $variables);
}
The respond()
method accepts a string (or compatible variable) and sends a complete response:
class HomePage extends Responder {
public function __invoke() {
$this->respond('Home page');
}
}
Responder routes can be created using the site's route()
method:
$site->route('GET', '/about', new AboutPageResponder());
$site->route('GET', '/calendar', new CalendarPageResponder());
$site->route('GET', '/news', new NewsPageResponder());
You can also define routes within the site class, by overriding the getRouter()
method:
class ExampleSite extends \Outpost\Site
public function getRouter() {
# Gets a Phroute RouteCollector object
$router = parent::getRouter();
$router->get('/article/{id:i}', new ArticlePage());
$router->post('/subscribe', new SubscriptionResponder());
return $router;
}
}
You can listen for Site events using the subscribe()
method. It receives a callable, which will be invoked and passed Event
objects as they occur.
$listener = function ($event) { print $event; };
$site->subscribe($listener);
The LoggerListener
class provides an easy way to record events in a PSR-3-compatible logger:
$log = new Logger();
$listener = new \Outpost\Events\LoggerListener($log);
$site->subscribe($listener);