Skip to content

Advanced Guide

Nana Axel edited this page Aug 8, 2020 · 10 revisions

Summary


WaterPipe configuration

If you want, you can customize the behavior of WaterPipe using the WaterPipeConfig class. Available options are:

  • queryStringEnabled: This option is a boolean defining if the use of query strings are enabled in your pipes. The default value is true ;
  • defaultCharset: This option is a string defining the default charset to use when processing responses. The default value is "utf-8" ;
  • errorLogger: This option allow you to define an IErrorLogger implementation to log errors and uncaught exceptions. Since you can implement your own error logger, there are predefined implementations bundled into WaterPipe:
    • StdErrorLogger: Which log errors to the STDERR output stream.
    • DefaultErrorLogger: Which log errors using the error_log PHP function. You can customize this logger using its constructor.

Deprecated options are:

  • useStderr: This option is a boolean defining if WaterPipe have to use the STDERR output channel to print errors and uncaught exceptions. The default value is false .

To configure WaterPipe, you just have to retrieve the singleton and define values, before creating pipes.

<?php

use \ElementaryFramework\WaterPipe\WaterPipeConfig;

// Edit configuration
$config = WaterPipeConfig::get();
$config->setUseStderr(false); // DEPRECATED since 1.3.0
$config->setErrorLogger(new DefaultErrorLogger()); // RECOMMENDED way to log errors since 1.4.0
$config->setDefaultCharset("ISO-8859-1");

// You can now create your pipes...

Creating Pipes

There are several ways to create routes with WaterPipe. Each of them has been designed to match common Web Design Patterns and architectures.

Using anonymous functions

This is the way you have seen in the Getting Started guide. Here you create your URI callback in an anonymous function, wrapping as parameters the Request and the Response of the current context.

$pipe->request('/hello/to/world', function (Request $req, Response $res) {
    $res->sendText("Hello, world !");
});

By this way, you have the ability to leave URIs and their callbacks at the same place. It can help you for maintenance, but can make the file hard to edit if many routes are created into it...

Using RouteActions

RouteAction help you wrap your URI callback in a specific class. This class must extend the abstract RouteAction class and implement the execute() method. Using RouteAction, you have to access the Request and the Response of the current context through RouteAction::_request and RouteAction::_response properties.

class RouteActionExample extends \ElementaryFramework\Routing\RouteAction
{
    public function execute()
    {
        $this->_response->sendText("Hello, world !");
    }
}

$pipe->request('/hello/to/world', RouteActionExample::class);
// -- OR --
$pipe->request('/hello/to/world', new RouteActionExample);

The main advantage of RouteAction is the code reuse when many URI have the same callback (or the callback change only according to URI parameters).

Using callable

Besides anonymous functions, WaterPipe support any kind of callable forms supported by PHP.

class HelloWorldController
{
    public static function helloToWorldStatic(Request $req, Response $res)
    {
        $res->sendText("Hello, world !");
    }

    public function helloToWorld(Request $req, Response $res)
    {
        $res->sendText("Hello, world !");
    }
}

function helloWorld(Request $req, Response $res)
{
    $res->sendText("Hello, world !");
}

// Using static method with callable array form
$pipe->request('/hello/to/world', [HelloWorldController::class, 'helloToWorldStatic']);

// Using instance method with callable array form
$pipe->request('/hello/to/world', [new HelloWorldController, 'helloToWorld']);

// Using function names
$pipe->request('/hello/to/world', 'helloWorld');

// Using closures
$pipe->request('/hello/to/world', \Closure::fromCallable(/* Any previous callable form as parameter */));

Note it is important that the called function in the callable form must have as parameters the Request and the Response.

Defining routes

When you create pipes, you have to define route on which the pipe will be executed.

$pipe->request('your_route_here', function (Request $req, Response $res) {
    // Execute...
});

There are many ways to define a route, according to your needs.

Static routes

Static routes doesn't change anymore during the pipe resolution process, for example /api/posts/all. They are resolved as is, and used as is.

Routes with named parameters

It's possible to create a route and provide to it a named parameter, for example /api/posts/:id, where :id is the parameter. It's possible to give to the route an infinite number of parameters.

Named parameters are resolved during the pipe resolution process, for example, incoming requests to /api/posts/1, /api/posts/92345, /api/posts/category_php, /api/posts/user5517, /api/posts/no-category, /api/posts/all.json will match the /api/posts/:id route. So, when using named parameters, it's important to parse the resolved value to be sure it contains what you want.

Regex-based routes

With WaterPipe, you can create routes based on Regular Expressions (RegEx). It can give you more flexibility and safety to process your requests. For example, a regex route like /api/posts/(\d+) will only be executed for incoming requests to /api/posts/10, /api/posts/8823, etc...

There is no restriction on the kind of regular expression used, for example, a regex route like /posts/(?<category>\w+)/(?<id>\d+)/(\d+)-(\k<category>)-(\k<id>).html will be executed for incoming requests to /posts/test/1727/123123123-test-1727.html or /posts/programming/33491/20191103-programming-33491.html, but not /posts/games/385/9914346-minecraft-385.html because it's doesn't match the regex.

Handling incoming Requests

The Request class manages in an OOP way all the data sent by the client. When you receive an HTTP request, an unique instance of this class (representing that request) can be accessed via Request::capture().

The Request class is composed of methods and properties allowing you retrieve information from the raw HTTP request:

  • Request::$uri: This property returns an instance of the RequestUri class.
  • Request::getMethod(): This method returns an integer, representing the HTTP request method. This integer represent values from the static class RequestMethod.
  • Request::getParams(): This method returns a RequestData instance, storing all information about HTTP GET parameters.
  • Request::getBody(): This method returns a RequestData instance, storing all information about the request body.
  • Request::getCookies(): This method returns a RequestData instance, storing all information about HTTP cookies associated to the request.
  • Request::getHeader(): This method returns an instance of the RequestHeader class, and allow you to retrieve HTTP headers associated to the request.
  • Request::isAjax(): This method is used to check if the request was sent using AJAX. It's helpful for REST services.

RequestUri

The URI of the incoming request is accessed via the Request::$uri property:

$pipe->request('your_route_here', function (Request $req, Response $res) {
    // Access to data about the URI via $req->uri
});

This will return an instance of the RequestUri class.

With this, you can access resolved values of variables in routes with named parameters or regex-based routes, using the RequestUri::getParams() method, or directly by indexing the RequestUri instance (the class implement the ArrayAccess interface).

The RequestUri::getParams() method is totally different than the Request::getParams() method. The first one returns URI parameters, and the second returns request parameters, formerly GET parameters.

$pipe->request('/api/posts/:id', function (Request $req, Response $res) {
    // URI named parameters are accessed with their name
    $id = $req->uri['id'];
});

$pipe->request('/api/users/(new|edit|ban)/(\d+)', function (Request $req, Response $res) {
    // URI regex-based parameters are accessed with their positions in the uri (the position is zero-based)
    $action = $req->uri[0]; // new,  edit, or ban
    $userId = $req->uri[1]; // Any integer
});

$pipe->request('/api/billing/:userId/(new|cancel)/:billId', function (Request $req, Response $res) {
    // The same previous rules apply when named parameters and regex-based parameters are used together
    $userId = $req->uri['userId']; // Any string
    $action = $req->uri[1]; // new or cancel (notice that the parameter is accessed at position 1, since it is the second parameter in the uri)
    $billId = $req->uri['billId']; // Any string
});

RequestMethod

Some times, you will need to check what request method the client used to connect to your endpoint. This is pretty helpful when you use the WaterPipe::request() method to create your pipes, since a pipe created with this method will be executed regardless of the request method.

Getting the request method of the current request is simple as calling Request::getMethod(), which returns an integer with a value equals to one of the static class (this class is likely an enumeration) RequestMethod.

$pipe->request('/api/posts/:id', function (Request $req, Response $res) {
    // Any request method can fall into this pipe if the request uri match, so to check which request method we are currently running
    switch ($req->getMethod()
    {
        case RequestMethod::GET:
            // Do something...
            break;
        case RequestMethod::POST:
            // Do something...
            break;
        case RequestMethod::PUT:
            // Do something...
            break;
        case RequestMethod::DELETE:
            // Do something...
            break;
        case RequestMethod::HEAD:
            // Do something...
            break;
        case RequestMethod::PATCH:
            // Do something...
            break;
        case RequestMethod::OPTIONS:
            // Do something...
            break;

        default:
        case RequestMethod::UNKNOWN:
            // Humm... Something surely went wrong...
            break;
    }
});

If you are using a one of the special methods to create your pipes (eg. WaterPipe::post(), WaterPipe::patch()), you can be sure that Request::getMethod() will always return the right request method.

RequestData

A RequestData class instance is, simply, a collection of data contained in a request. When a client send a request, there are many data we can collect from it. WaterPipe actually collect three (03) of them: GET parameters, request body, HTTP cookies.

GET parameters

Collecting GET parameters from a request is pretty simple:

// In this example, a client send a request to /api/posts/all?category=games&from=2020-01-01&to=2020-12-31
$pipe->get('/api/posts/all', function (Request $req, Response $res) {
    // Get the GET parameters dictionary
    $params = $req->getParams();

    // Access parameters
    $category = $params['category'];
    $from     = date_create($params['from']);
    $to       = date_create($params['to']);

    // Find your posts...
});

Request body

You can get the request body of a POST or PUT request with the Request::getBody() method. This method will return two types fo data:

  • An instance of RequestData, which means that the request body was successfully parsed by WaterPipe (eg. requests with JSON, XML, or url-encoded form content)
  • A string, which represent the raw request body content. In this case, WaterPipe was not able to automatically parse the request body. This can happen when the request body contains non-key-value-pair parsable value (eg. binary file content, raw text, etc...), so WaterPipe let you the right to do whatever you want with the raw content.
$pipe->post('/api/users/login', function (Request $req, Response $res) {
    // Get login details from form
    $data = $req->getBody(); // This will return a RequestData instance

    $username = $data['username'];
    $password = $data['password'];
    $remember = boolval($data['remember']);

    // Process login...
});

$pipe->put('/api/users/:id/avatar', function (Request $req, Response $res) {
    // Get the user id
    $userId = $req->uri['id'];

    // Retrieve raw image content
    $image = $req->getBody(); // This will return a string

    // Save image to disk, in the personal folder of the user with $userId...
});

HTTP cookies

Like request method and request body, HTTP cookies associated to the current request are captured by WaterPipe, and can be retrieved with the Request::getCookies() method, which also return a RequestData instance:

$pipe->get('/shop/cart/process', function (Request $req, Response $res) {
    // Get customer cart from cookies
    $cookies = $req->getCookies();
    $productIds = explode(',', $cookies['cart']); // Construct an array of product id from a string of comma-separated id

    // Get products data from database and output the payment form...
});

RequestHeader

When accepting a request, WaterPipe collect every header present in that request an offer you a OOP way to gather them. The RequestHeader class is a concrete implementation of the abstract Header class. It implements many methods, allowing you get or set a wide range of official HTTP header for a request (If you think an official header is missing, you can open an issue, or make a pull request).

$pipe->delete('/api/users/:id/avatar', function (Request $req, Response $res) {
    $header = $req->getHeader();

    $uToken = $header->getField("X-User-Token"); // Get the user token from header (custom header)
    $origin = $header->getOrigin(); // Get the request origin (Origin header)

    // Check if the request is coming from the right origin...
    // Check the user token...

    // Delete the user avatar
});

AJAX support

WaterPipe is primarily built to help developers quickly and easily produce REST services, for this fact, WaterPipe have handy functionalities. One of these is the AJAX support. When a client send a request, you can check if this request was sent using AJAX (with jQuery, React.js, Vue.js, or any other frontend or HTTP library) to decide which kind of data you can process or return. Checking this is simple as calling a single function, Request::isAjax():

$pipe->post('/api/users/login', function (Request $req, Response $res) {
    // Process login...

    // Login is success, check if the request was sent using AJAX
    if ($req->isAjax())
    {
        // Return JSON or XML with successful message...
    }
    else
    {
        // Redirect to the successful login page...
    }
});

Handling outgoing Responses

Like the Request, the Response class is responsible to manage all the data received by the client after a request. In contrast, the Response class doesn't have a singleton access, so you will always have to create a new instance manually, or use the instance provided by the current context, to send responses to clients.

The Response class comes with all you need to configure and send your HTTP response. A simple set of functions are:

Browse the Response class source code to see the complete list of functions.

  • Response::setHeader(): To define the ResponseHeader of the HTTP response.
  • Response::setStatus(): To define the ResponseStatus of the HTTP response.
  • Response::setBody(): To define the response body.
  • Response::send(): To send your response to the client and properly close the current request.

ResponseHeader

Like the RequestHeader class, the ResponseHeader class is a concrete implementation of the abstract Header class, designed for use in HTTP responses.

$pipe->get('/api/posts/:id', function (Request $req, Response $res) {
    // Get the post id
    $id = $req->uri['id'];

    // Fetch the post in database...
    $post = fetch_post_with_id($id);

    // We want to send JSON data here, so prepare the response
    $header = new ResponseHeader();
    $header->setContentType('application/json'); // Shortcut method for official HTTP response headers
    $header->setField('X-Post-Hash', hash_post($post)); // Custom header
    $res->setHeader($header); // We define response headers here

    // Send the response...
});

ResponseStatus

The ResponseStatus class is used to configure the HTTP response status code and message. Many HTTP status code are already registered by default into WaterPipe, you can browse them over static constants wrapped if the ResponseStatus class.

$pipe->get('/api/posts/:id', function (Request $req, Response $res) {
    // Get the post id
    $id = $req->uri['id'];

    // Fetch the post in database...
    $post = fetch_post_with_id($id);

    // We want to send JSON data here, so prepare the response
    $header = new ResponseHeader();
    $header->setContentType('application/json'); // Shortcut method for official HTTP response headers
    $header->setField('X-Post-Hash', hash_post($post)); // Custom header
    $res->setHeader($header); // We define response headers here

    // Everything is successful here, so we can define a status code of 200
    $status = new ResponseStatus(ResponseStatus::OkCode); // The ResponseStatus class constructor requires the status code
    $res->setStatus($status);

    // Send the response...
});

Response body

It's to you to define the response body. You can send any kind of data you want, the only requirement is that the data have to be parsed as string.

$pipe->get('/api/posts/:id', function (Request $req, Response $res) {
    // Get the post id
    $id = $req->uri['id'];

    // Fetch the post in database...
    $post = fetch_post_with_id($id);

    // We want to send JSON data here, so prepare the response
    $header = new ResponseHeader();
    $header->setContentType('application/json'); // Shortcut method for official HTTP response headers
    $header->setField('X-Post-Hash', hash_post($post)); // Custom header
    $res->setHeader($header); // We define response headers here

    // Everything is successful here, so we can define a status code of 200
    $status = new ResponseStatus(ResponseStatus::OkCode); // The ResponseStatus class constructor requires the status code
    $res->setStatus($status);

    // Format the post data to JSON string and define the response body
    $body = json_encode($post);
    $res->setBody($body);

    // Send the response...
});

Send the HTTP response - The middle way

When everything is OK and you have finished to configure your response, simply call Response::send() to transfer it to the client.

$pipe->get('/api/posts/:id', function (Request $req, Response $res) {
    // Get the post id
    $id = $req->uri['id'];

    // Fetch the post in database...
    $post = fetch_post_with_id($id);

    // We want to send JSON data here, so prepare the response
    $header = new ResponseHeader();
    $header->setContentType('application/json'); // Shortcut method for official HTTP response headers
    $header->setField('X-Post-Hash', hash_post($post)); // Custom header
    $res->setHeader($header); // We define response headers here

    // Everything is successful here, so we can define a status code of 200
    $status = new ResponseStatus(ResponseStatus::OkCode); // The ResponseStatus class constructor requires the status code
    $res->setStatus($status);

    // Format the post data to JSON string and define the response body
    $body = json_encode($post);
    $res->setBody($body);

    // Send the response...
    $res->send();

    // Nothing will be executed after this call!
});

NOTE

Previous seen methods can be chained, so you can prepare all your response data first, and send it in one line with $res->setHeader($header)->setStatus($status)->setBody($body)->send();

Send the HTTP response - The long way

With WaterPipe you have the ability to send parts of the response separately. For that, you can use:

  • Response::sendHeaders(): Send only response headers. This method will assume that you HAVE NOT sent the response body before.
  • Response::sendBody(): Send only the response body.
  • Response::close(): Close the response, and exit properly.

With this, you are able to define and send response data many times before close the connection, or even only send a part of the response (very useful for somme HTTP methods like HEAD, which only want headers, and no body content).

NOTE

When sending response parts separately, you are responsible to choose when to trigger the beforeSend WaterPipe event.

$pipe->get('/api/posts/:id', function (Request $req, Response $res) {
    // Get the post id
    $id = $req->uri['id'];

    // Fetch the post in database...
    $post = fetch_post_with_id($id);

    // We can trigger the beforeSend event here for example.
    // Note that it's to you to choose the right place to trigger the event.
    // You just have to trigger it BEFORE the first response part is sent.
    WaterPipe::triggerBeforeSendEvent($res);

    // We want to send JSON data here, so prepare the response
    $header = new ResponseHeader();
    $header->setContentType('application/json'); // Shortcut method for official HTTP response headers
    $header->setField('X-Post-Hash', hash_post($post)); // Custom header
    $res->setHeader($header); // We define response headers here

    // Everything is successful here, so we can define a status code of 200
    $status = new ResponseStatus(ResponseStatus::OkCode); // The ResponseStatus class constructor requires the status code
    $res->setStatus($status);

    // Send the response header FIRST (otherwise it will surely fails)
    // The response status MUST BE DEFINED when sending headers
    $res->sendHeaders();

    // Format the post data to JSON string and define the response body
    $body = json_encode($post);
    $res->setBody($body);

    // Send the response body...
    $res->sendBody();

    // Everything is OK, close the connection
    $res->close();

    // Nothing will be executed after this call!
});

NOTE

Previous seen methods can also be chained, so you can prepare all your response data first, and send it, as parts, in one line with $res->setHeader($header)->setStatus($status)->sendHeaders()->setBody($body)->sendBody()->close();

Send the HTTP response - The short way

You have surely noticed that send a response is very tricky, but let you customize everything. However, WaterPipe has handy methods to allow you quickly configure and send HTTP response in one call! These are:

sendHtml

sendHtml is used to, like his name explain, send an HTML content to the client. It will set the Content-Type header value to text/html, with the default charset defined in WaterPipe configuration. The method signature is:

sendHtml(string $body, int $status = ResponseStatus::OkCode)

$pipe->get('/', function (Request $req, Response $res) {
    $res->sendHtml("<h1>Welcome</h1><p>Click <a href=\"/login\">here</a> to login!</p>");
});

sendJsonString

sendJsonString is used to send a string in JSON format to the client. It will set the Content-Type header value to application/json, with the default charset defined in WaterPipe configuration. The method signature is:

sendJsonString(string $body, int $status = ResponseStatus::OkCode)

$pipe->post('/api/say/hello', function (Request $req, Response $res) {
    $res->sendJsonString('{"hello": "world"}');
});

sendJson

sendJson take as first parameter an array, which will be formatted to JSON using json_encode (without extra parameters) and sent to the client. It will set the Content-Type header value to application/json, with the default charset defined in WaterPipe configuration. The method signature is:

sendJson(array $json, int $status = ResponseStatus::OkCode)

$pipe->post('/api/say/hello', function (Request $req, Response $res) {
    $res->sendJson(["hello" => "world"]);
});

sendText

sendText just sends a raw text to the client. It will set the Content-Type header value to text/plain, with the default charset defined in WaterPipe configuration. The method signature is:

sendText(string $body, int $status = ResponseStatus::OkCode)

$pipe->get('/license', function (Request $req, Response $res) {
    $res->sendText("A long license text can go here...");
});

sendFile

sendFile sends a file content to the client. Here the Content-Type header value can be customized with the third parameter. The method signature is:

sendFile(string $path, int $status = ResponseStatus::OkCode, string $mime = null)

$pipe->get('/user/:id/avatar', function (Request $req, Response $res) {
    // Compute the path to the user's avatar
    $path = "/files/users/{$req->uri['id']}/avatar.png";

    // Send the file to the client
    $res->sendFile($path, ResponseStatus::OkCode, "image/png");
});

$pipe->get('/assets/:name.css', function (Request $req, Response $res) {
    // Compute the path to the asset file
    $path = "/files/assets/css/{$req->uri['name']}.css";

    // Send the file to the client
    $res->sendFile($path, ResponseStatus::OkCode, "text/css");
});

Extra: Example using FireFS

// A GET request to /assets/scripts/vendor/jquery/jquery.min.js will execute this pipe, for example
$pipe->get('/assets/(.+)', function (Request $req, Response $res) {
    $fs = new FireFS("/files/assets/");
    $params = $req->uri->getParams();

    // Compute the path to the asset file
    $path = $params[0]; // scripts/vendor/jquery/jquery.min.js

    if ($fs->exists($path)) {
        $res->sendFile($fs->toInternalPath($path), ResponseStatus::OkCode, $fs->mimeType($path));
    } else {
        $res->sendText("Asset file not found.", ResponseStatus::NotFoundCode);
    }
});

Handling common HTTP errors

WaterPipe allows you to quickly handle the common HTTP errors 404 and 500.

// Handle 404 errors
$pipe->error(ResponseStatus::NotFoundCode, function (Request $req, Response $res) {
    $res->sendText('404 Not Found Error.', ResponseStatus::NotFoundCode);
});

// Handle 500 errors
$pipe->error(ResponseStatus::InternalServerErrorCode, function (Request $req, Response $res) {
    $res->sendText('An error occurred while processing the request. Please contact support if the error persist.', ResponseStatus::InternalServerErrorCode);
});