shroophp/framework

A framework for HTTP applications.

v3.0.1 2018-12-05 23:30 UTC

README

A framework for HTTP applications.

Installation

composer require 'shroophp/framework ^3.0'

Example Usage

Hello, world!

In the following example, every request will be responded to by printing the string 'Hello, world!'.

It is assumed that the method and path of the request will be read from $_SERVER['REQUEST_METHOD'] and $_SERVER['REQUEST_URI'] respectively.

<?php

use ShrooPHP\Core\Request;
use ShrooPHP\Framework\Application;
use ShrooPHP\Framework\Request\Responses\Response;
use ShrooPHP\Framework\Requests\PhpRequest;

require 'vendor/autoload.php';

$app = new Application;
$app->push(Response::string('Hello, world!'));
$app->run();

Content Types

To change the format of the response, a content type can be specified as the second parameter of the response string.

Response::string('Hello, world!', 'text/plain');

URLs

It is also possible to match against specific request paths. In the following example, only requests to the path /hello/world will be responded to.

$app->path('/hello/world', Response::string('Hello, world!'));

Request Methods

Request methods can also be matched against. In following example, only requests using the GET method will be responded to.

$app->method('GET', Response::string('Hello, world!'));

Multiple methods can be matched by specifying them as an array, as in the following example (which will respond to either GET or POST).

$app->method(array('GET', 'POST'), Response::string('Hello, world!'));

Dynamic URLs

It is possible to dynamically match request paths using a simple curly-brace syntax. Each specified key will equate to a lazy regular expression group (i.e. (.*?)).

In the following example, any path starting with /hello/ will be responded to.

$app->path('/hello/{subject}', Response::string('Hello, subject!'));

Dynamic Responses

The response to the matched request can be determined dynamically. To achieve this, a series of callbacks may be specified.

In the following example, the matched request is responded to by printing Hello, '<subject>'!, where <subject> is the suffix of the request path.

$app->path('/hello/{subject}', function (Application $app) {

    $app->push(function (Request $request) {

        $subject = $request->args()->get('subject');

        return Response::string("Hello, '{$subject}'!", 'text/plain');
    });
});

The \ShrooPHP\Framework\Application::toModifier() helper method is aliased as an underscore in order to make the previous example more succinct, as in the following example.

$app->path('/hello/{subject}', $app->_(function (Request $request) {

    $subject = $request->args()->get('subject');

    return Response::string("Hello, '{$subject}'!", 'text/plain');
}));

Logic Chaining

The logic of the application can be chained using nested modifiers.

The application in the following example will match all GET requests to the path /chained.

$app->path('/chained', function (Application $app) {

    $app->method('GET', Response::string('Chained!', 'text/plain'));
});

Evaluation

An application may consist of multiple handlers. The application will run through each of them sequentially until a request is handled, at which point it will halt.

In the following example, the application will print two, as the second handler is the first to respond.

$app->path('first',  Response::string('one',   'text/plain'))
    ->path('second', Response::string('two',   'text/plain'))
    ->path('third',  Response::string('three', 'text/plain'))
    ->handle(new PhpRequest('GET', 'second')); // Prints 'two'.

The application determines whether or not a handler has responded by evaluating the handler's return value. A return value of FALSE or NULL informs the application that it should continue to iterate over the handlers, while TRUE or \ShrooPHP\Core\Request\Response will cause the application to present the response (if any) and halt.

In the following example, the application will print handled, as the first handler returns NULL.

$app->path('{path}', function (Application $app) {

    $app->push(function (Request $request) {
        return null;
    });
});

$app->path('{path}', Response::string('handled', 'text/plain'));

$app->handle(new PhpRequest('GET', 'path'));

Method Overriding

Traditionally, forms on the web are only capable of submitting HTTP requests with either the GET or POST method.

To overcome this, an overriding method can be sent in the request body and detected by the application as in the following example.

<?php use ShrooPHP\Framework\Application; ?>
<!-- method.php -->
<!DOCTYPE html>
<html>
    <head>
        <title>Method Overriding</title>
    </head>
    <body>
        <form action="" method="POST">
        <p>
            <label for="method">Method</label>
            <input id="method" type="text"
                name="<?= htmlentities(Application::METHOD); ?>"
                value="PUT">
        </p>
        <p>
            <input type="submit">
        </p>
        </form>
    </body>
</html>

<?php

use ShrooPHP\Core\Request;
use ShrooPHP\Framework\Application;
use ShrooPHP\Framework\Request\Responses\Response;

require 'vendor/autoload.php';

$app = new Application;

$app->web(function (Application $app) {

    $app->method('PUT', function (Application $app) {

        $app->push(function (Request $request) {

            $dump = var_export($request->data()->getArrayCopy(), true);
            return Response::string($dump, 'text/plain');
        });
    });

    ob_start();
    require 'method.php';
    $app->push(Response::string(ob_get_clean(), 'text/html'));
});

$app->run();

Preventing Cross-Site Request Forgery (CSRF)

A common method for preventing cross-site request forgery is to validate requests against the target origin and a token associated with the session. This is achieved in the following example.

<?php use ShrooPHP\Framework\Application; ?>
<!-- token.php -->
<!DOCTYPE html>
<html>
    <head>
        <title>Preventing Cross-Site Request Forgery (CSRF)</title>
    </head>
    <body>
        <form action="" method="POST">
            <p>
                <label for="token">Token</label>
                <input id="token" type="text"
                    name="<?= htmlentities(Application::TOKEN); ?>"
                    value="<?= htmlentities($request->token()->expected()); ?>">
            </p>
            <p>
                <input type="submit">
            </p>
        </form>
    </body>
</html>

<?php

use ShrooPHP\Core\Request;
use ShrooPHP\Framework\Application;
use ShrooPHP\Framework\Request\Responses\Response;

require 'vendor/autoload.php';

$app = new Application;

$app->token(function (Application $app) {

    $app->origin('https://example.org/', function (Application $app) {

        $app->valid(Response::string('The token is valid.', 'text/plain'));
    });

    $app->push(function (Request $request) {

        ob_start();
        require 'token.php';
        return Response::string(ob_get_clean(), 'text/html');
    });
});

$app->run();

Configuration

The recommended configuration is to rewrite all requests to a single PHP file (most commonly named index.php), with the request path being passed via the server API to $_SERVER['REQUEST_URI'].

PHP Built-in Web Server

The PHP built-in web server is ideal for development, testing, and demonstration purposes.

Do not use the PHP built-in web server in production environments.

php -S localhost:80 index.php

Apache

Add a virtual host to your Apache web server that resembles the following.

<VirtualHost *:80>
    DocumentRoot "/path/to/directory"
    ServerName example.org
    <Directory "/path/to/directory">
        Require all granted
        <IfModule mod_rewrite.c>
            RewriteEngine on
            RewriteCond %{REQUEST_FILENAME} !index.php
            RewriteRule .* index.php [QSA,L]
        </IfModule>
    </Directory>
</VirtualHost>

nginx

Ensure that your nginx configuration resembles the following (assuming you are using FastCGI).

worker_processes auto;
worker_cpu_affinity auto;
pcre_jit on;

events {
    worker_connections 2048;
}

http {

    include mime.types;
    default_type application/octet-stream;

    index index.php;

    server {
        listen 80;
        server_name example.org;
        root /path/to/directory;
        rewrite .* /index.php last;
        location ~ \.php$ {
            fastcgi_pass unix:/run/php-fpm/php-fpm.sock;
            fastcgi_index index.php;
            include fastcgi.conf;
        }
    }
}