Skip to content

Routing

Overview

The Skinny framework helps you map resource URIs to callback functions for specific HTTP request methods (e.g. GET, POST, PUT, DELETE, OPTIONS, PATCH or HEAD). A Skinny application will invoke the first route that matches the current HTTP request’s URI and method.

If the Skinny application does not find a route with a URI that matches the HTTP request URI and method, it will automatically return a 404 Not Found response.

Divergence from Slim v2

Heads Up! While using Slim v2 to refactor a large application, it became apparant that routing had significant shortcomings. Addressing this issue, particularly the lack of scalability, was a key factor in the decision to create Skinny. You can read more about it here.

Route Definition Methods

The following methods define an application's routes – which URI's will match what request method(s) to invoke an appropriate callback.

GET Routes

Use the Skinny application’s get() method to map a callback function to a resource URI that is requested with the HTTP GET method.

<?php
$app = \Skinny\Skinny::newInstance();
$app->get('/books/:id', function ($id) {
    //Show book identified by $id
});

In this example, an HTTP GET request for “/books/1” will invoke the associated callback function, passing “1” as the callback’s argument.

The first argument of the Skinny application’s get() method is the resource URI. The last argument is anything that returns true for is_callable(). The last argument can be an anonymous function, but if you prefer cleaner notation an appropriately formatted string or array can be used.

POST Routes

Use the Skinny application’s post() method to map a callback function to a resource URI that is requested with the HTTP POST method.

<?php
$app = \Skinny\Skinny::newInstance();
$app->post('/books', function () {
    //Create book
});

In this example, an HTTP POST request for “/books” will invoke the associated callback function

The first argument of the Skinny application’s post() method is the resource URI. The last argument is anything that returns true for is_callable(). The last argument can be an anonymous function, but if you prefer cleaner notation an appropriately formatted string or array can be used.

PUT Routes

Use the Skinny application’s put() method to map a callback function to a resource URI that is requested with the HTTP PUT method.

<?php
$app = \Skinny\Skinny::newInstance();
$app->put('/books/:id', function ($id) {
    //Update book identified by $id
});

In this example, an HTTP PUT request for “/books/1” will invoke the associated callback function, passing “1” as the callback function’s argument.

The first argument of the Skinny application’s put() method is the resource URI. The last argument is anything that returns true for is_callable(). The last argument can be an anonymous function, but if you prefer cleaner notation an appropriately formatted string or array can be used.

Method Override

Unfortunately, modern browsers do not provide native support for HTTP PUT requests. To work around this limitation, ensure your HTML form’s method attribute is “post”, then add a method override parameter to your HTML form like this:

<form action="/books/1" method="post">
    ...  other form fields here...
    <input type="hidden" name="_METHOD" value="PUT"/>
    <input type="submit" value="Update Book"/>
</form>

If you are using Backbone.js or a command-line HTTP client, you may also override the HTTP method by using the X-HTTP-Method-Override header.

DELETE Routes

Use the Skinny application’s delete() method to map a callback function to a resource URI that is requested with the HTTP DELETE method.

<?php
$app = \Skinny\Skinny::newInstance();
$app->delete('/books/:id', function ($id) {
    //Delete book identified by $id
});

In this example, an HTTP DELETE request for “/books/1” will invoke the associated callback function, passing “1” as the callback function’s argument.

The first argument of the Skinny application’s delete() method is the resource URI. The last argument is anything that returns true for is_callable().

Method Override

Unfortunately, modern browsers do not provide native support for HTTP DELETE requests. To work around this limitation, ensure your HTML form’s method attribute is “post”, then add a method override parameter to your HTML form like this:

<form action="/books/1" method="post">
    ...  other form fields here...
    <input type="hidden" name="_METHOD" value="DELETE"/>
    <input type="submit" value="Delete Book"/>
</form>

If you are using Backbone.js or a command-line HTTP client, you may also override the HTTP method by using the X-HTTP-Method-Override header.

OPTIONS Routes

Use the Skinny application’s options() method to map a callback function to a resource URI that is requested with the HTTP OPTIONS method.

<?php
$app = \Skinny\Skinny::newInstance();
$app->options('/books/:id', function ($id) {
    //Return response headers
});

In this example, an HTTP OPTIONS request for “/books/1” will invoke the associated callback function, passing “1” as the callback function’s argument.

The first argument of the Skinny application’s options() method is the resource URI. The last argument is anything that returns true for is_callable().

Method Override

Unfortunately, modern browsers do not provide native support for HTTP OPTIONS requests. To work around this limitation, ensure your HTML form’s method attribute is “post”, then add a method override parameter to your HTML form like this:

<form action="/books/1" method="post">
    ...  other form fields here...
    <input type="hidden" name="_METHOD" value="OPTIONS"/>
    <input type="submit" value="Fetch Options For Book"/>
</form>

If you are using Backbone.js or a command-line HTTP client, you may also override the HTTP method by using the X-HTTP-Method-Override header.

PATCH Routes

Use the Skinny application’s patch() method to map a callback function to a resource URI that is requested with the HTTP PATCH method.

<?php
$app = \Skinny\Skinny::newInstance();
$app->patch('/books/:id', function ($id) {
    // Patch book with given ID
});

In this example, an HTTP PATCH request for “/books/1” will invoke the associated callback function, passing “1” as the callback function’s argument.

The first argument of the Skinny application’s patch() method is the resource URI. The last argument is anything that returns true for is_callable().

Custom Routes

One route, multiple HTTP methods

Heads Up! map() and rtMap have different signatures!

Sometimes you may need a route to respond to multiple HTTP methods or to a custom HTTP method. You can accomplish both with this method. This example demonstrates how to map a resource URI to a callback that responds to multiple HTTP methods.

The map() method adds HTTP methods as the first argument. It is usually an array, but it can also be a string.

Legacy rtMap() can add multple HTTP methods with the Route object's via() method.

<?php
$app = \Skinny\Skinny::newInstance();

// Skinny method
$app->map(['GET', 'POST'], '/foo/bar', function() {
    echo "I respond to multiple HTTP methods!";
});

// -- OR -- 

// Legacy method
$app->rtMap('/foo/bar', function() {
    echo "I respond to multiple HTTP methods!";
})->via('GET', 'POST');

$app->run();

The route defined in this example with either method will respond to both GET and POST requests for the resource identified by “/foo/bar”.

For rtMap(), specify each appropriate HTTP method as a separate string argument to the Route object’s via() method. Like other legacy Route methods (e.g. name() and conditions()), the via() method is chainable:

<?php
$app = \Skinny\Skinny::newInstance();
$app->rtMap('/foo/bar', function() {
    echo "Fancy, huh?";
})->via('GET', 'POST')->name('foo');
$app->run();

One route, custom HTTP methods

map() and rtMap() are not limited to just GET, POST, PUT, DELETE, and OPTIONS methods. You may also specify your own custom HTTP methods (e.g. if you were responding to WebDAV HTTP requests). You can define a route that responds to a custom “FOO” HTTP method like this:

<?php
$app = \Skinny\Skinny::newInstance();

// Skinny method
$app->map('FOO', '/hello', function() {
    echo "Hello";
});

// Legacy method
$app->rtMap('/hello', function() {
    echo "Hello";
})->via('FOO');

$app->run();

Parameters

You can embed parameters into route resource URIs. In this example, there are two parameters in the route URI, “:one” and “:two”.

<?php
$app = \Skinny\Skinny::newInstance();
$app->get('/books/:one/:two', function ($one, $two) {
    echo "The first parameter is " .  $one;
    echo "The second parameter is " .  $two;
});

To create a URL parameter, prepend “:” to the parameter name in the route URI pattern. When the route matches the current HTTP request, the values for each route parameter are extracted from the HTTP request URI and are passed into the associated callback function in order of appearance.

The previous example may suggest parameter names and function arguments are somehow connected, but they are not. Consider an alternate way to code the same example:

<?php
class Handler
{
    public function reveal($first, $second)
    {
        echo "The first parameter is " .  $first;
        echo "The second parameter is " .  $second;
    }
}

$app = \Skinny\Skinny::newInstance();

$app->get('/books/:one/:two', 'Handler::reveal');

Wildcard route parameters

You may also use wildcard route parameters. These will capture one or many URI segments that correspond to the route pattern’s wildcard parameter into an array. A wildcard parameter is identified by a “+” suffix; it otherwise acts the same as normal route parameters shown above. Here’s an example:

<?php
$app = \Skinny\Skinny::newInstance();
$app->get('/hello/:name+', function ($name) {
    // Do something
});

When you invoke this example application with a resource URI “/hello/Josh/T/Lockhart”, the route callback’s $name argument will be equal to array('Josh', 'T', Lockhart').

Optional route parameters

Heads Up! Optional route segments are experimental. They should only be used in the manner demonstrated below.

You may also have optional route parameters. These are ideal for using one route for a blog archive. To declare optional route parameters, specify your route pattern like this:

<?php
$app = \Skinny\Skinny::newInstance();
$app->get('/archive(/:year(/:month(/:day)))', function ($year = 2010, $month = 12, $day = 05) {
    echo sprintf('%s-%s-%s', $year, $month, $day);
});

Each subsequent route segment is optional. This route will accept HTTP requests for:

/archive /archive/2010 /archive/2010/12 /archive/2010/12/05

If an optional route segment is omitted from the HTTP request, the default values in the callback signature are used instead.

Currently, you can only use optional route segments in situations like the example above where each route segment is subsequently optional. You may find this feature unstable when used in scenarios different from the example above.

Names

Heads Up! This only works with legacy route definition methods (prefixed with ”rt”).

Skinny lets you assign a name to a route. Naming a route enables you to dynamically generate URLs using the urlFor() helper method. When you use the Skinny application’s urlFor() method to create application URLs, you can freely change route patterns without breaking your application. Here is an example of a named route:

<?php
$app = \Skinny\Skinny::newInstance();
$app->rtGet('/hello/:name', function ($name) {
    echo "Hello, $name!";
})->name('hello');

You may now generate URLs for this route using the urlFor() method, described later in this documentation. The route name() method is also chainable:

<?php
$app = \Skinny\Skinny::newInstance();
$app->rtGet('/hello/:name', function ($name) {
    echo "Hello, $name!";
})->name('hello')->conditions(array('name' => '\w+'));

Conditions

Heads Up! This only works with legacy route definition methods (prefixed with ”rt”).

Skinny lets you assign conditions to route parameters. If the specified conditions are not met, the route is not run. For example, if you need a route with a second segment that must be a valid 4-digit year, you could enforce this condition like this:

<?php
$app = \Skinny\Skinny::newInstance();
$app->rtGet('/archive/:year', function ($year) {
    echo "You are viewing archives from $year";
})->conditions(array('year' => '(19|20)\d\d'));

Invoke the Route object’s conditions() method. The first and only argument is an associative array with keys that match any of the route’s parameters and values that are regular expressions.

Application-wide route conditions

If many of your Skinny application Routes accept the same parameters and use the same conditions, you can define default application-wide Route conditions like this:

<?php
\Skinny\Route::setDefaultConditions(array(
    'firstName' => '[a-zA-Z]{3,}'
));

Define application-wide route conditions before you define application routes. When you define a route, the route will automatically be assigned any application-wide Route conditions defined with \Skinny\Route::setDefaultConditions(). If for whatever reason you need to get the application-wide default route conditions, you can fetch them with \Skinny\Route::getDefaultConditions(). This static method returns an array exactly as the default route conditions were defined.

You may override a default route condition by redefining the route’s condition when you define the route, like this:

<?php
$app = \Skinny\Skinny::newInstance();
$app->rtGet('/hello/:firstName', $callable)
    ->conditions(array('firstName' => '[a-z]{10,}'));

You may append new conditions to a given route like this:

<?php
$app = \Skinny\Skinny::newInstance();
$app->rtGet('/hello/:firstName/:lastName', $callable)
    ->conditions(array('lastName' => '[a-z]{10,}'));

Middleware

Skinny enables you to associate middleware with a specific application route. When the given route matches the current HTTP request and is invoked, Skinny will first invoke the associated middleware in the order they are defined.

What is route middleware?

Route middleware is anything that returns true for is_callable. Route middleware will be invoked in the sequence defined before its related route callback is invoked.

How do I add route middleware?

When you define a new application route with the Skinny application’s get(), post(), put(), or delete() methods you must define a route pattern and a callable to be invoked when the route matches an HTTP request.

<?php
$app = \Skinny\Skinny::newInstance();
$app->get('/foo', function () {
    //Do something
});

In the example above, the first argument is the route pattern. The last argument is the callable to be invoked when the route matches the current HTTP request. The route pattern must always be the first argument. The route callable must always be the last argument.

You can assign middleware to this route by passing each middleware as a separate interior or… (ahem) middle… argument like this:

<?php
function mw1() {
    echo "This is middleware!";
}
function mw2() {
    echo "This is middleware!";
}
$app = \Skinny\Skinny::newInstance();
$app->get('/foo', 'mw1', 'mw2', function () {
    //Do something
});

When the /foo route is invoked, the mw1 and mw2 functions will be invoked in sequence before the route’s callable is invoked.

Suppose you wanted to authenticate the current user against a given role for a specific route. You could use some closure magic like this:

<?php
$authenticateForRole = function ( $role = 'member' ) {
    return function () use ( $role ) {
        $user = User::fetchFromDatabaseSomehow();
        if ( $user->belongsToRole($role) === false ) {
            $app = \Skinny\Skinny::getInstance();
            $app->flash('error', 'Login required');
            $app->redirect('/login');
        }
    };
};
$app = \Skinny\Skinny::newInstance();
$app->get('/foo', $authenticateForRole('admin'), function () {
    //Display admin control panel
});

What arguments are passed into each route middleware callable?

Each middleware callable is invoked with one argument, the currently matched \Skinny\Route object.

<?php
$aBitOfInfo = function (\Skinny\Route $route) {
    echo "Current route is " .  $route->getName();
};

$app->get('/foo', $aBitOfInfo, function () {
    echo "foo";
});

Groups

Skinny lets you group related routes. This is helpful when you find yourself repeating the same URL segments for multiple routes. This is best explained with an example. Let’s pretend we are building an API for books.

<?php
$app = \Skinny\Skinny::newInstance();

// API group
$app->group('/api', function () use ($app) {

    // Library group
    $app->group('/library', function () use ($app) {

        // Get book with ID
        $app->get('/books/:id', function ($id) {

        });

        // Update book with ID
        $app->put('/books/:id', function ($id) {

        });

        // Delete book with ID
        $app->delete('/books/:id', function ($id) {

        });

    });

});

The routes defined above would be accessible at, respectively:

GET /api/library/books/:id PUT /api/library/books/:id DELETE /api/library/books/:id

Route groups are very useful to group related routes and avoid repeating common URL segments for each route definition. They can also significantly shorten the path to the first matching route in a large application with many routes.

Note: group pattern can include plain URL segments only, not parameters.

Helpers

Skinny provides several helper methods (exposed via the Skinny application instance) that will help you control the flow of your application.

Please be aware that the following application instance method helpers halt(), pass(), redirect() and stop() are implemented using Exceptions. Each will throw a \Skinny\Exception\Stop or \Skinny\Exception\Pass exception. Throwing the Exception in these cases is a simple way to stop user code from processing, have the framework take over, and immediately send the necessary response to the client. This behavior can be surprising if unexpected. Take a look at the following code.

<?php
$app->get('/', function() use ($app, $obj) {
    try {
        $obj->thisMightThrowException();
        $app->redirect('/success');
    } catch(\Exception $e) {
        $app->flash('error', $e->getMessage());
        $app->redirect('/error');
    }
});

If $obj-&gt;thisMightThrowException() does throw an Exception the code will run as expected. However, if no exception is thrown the call to $app-&gt;redirect() will throw a \Skinny\Exception\Stop Exception that will be caught by the user catch block rather than by the framework redirecting the browser to the “/error” page. Where possible in your own application you should use typed Exceptions so your catch blocks are more targeted rather than swallowing all Exceptions. In some situations the thisMightThrowException() might be an external component call that you don’t control, in which case typing all exceptions thrown may not be feasible. For these instances we can adjust our code slightly by moving the success $app-&gt;redirect() after the try/catch block to fix the issues. Since processing will stop on the error redirect this code will now execute as expected.

<?php
$app->get('/', function() use ($app, $obj) {
    try {
        $obj->thisMightThrowException();
    } catch(Exception $e) {
        $app->flash('error', $e->getMessage());
        $app->redirect('/error');
    }
    $app->redirect('/success');
});

Halt

The Skinny application’s halt() method will immediately return an HTTP response with a given status code and body. This method accepts two arguments: the HTTP status code and an optional message. Skinny will immediately halt the current application and send an HTTP response to the client with the specified status and optional message (as the response body). This will override the existing \Skinny\Http\Response object.

<?php
$app = \Skinny\Skinny::newInstance();

//Send a default 500 error response
$app->halt(500);

//Or if you encounter a Balrog...
$app->halt(403, 'You shall not pass!');

//Or if you'd like to have a tea but you have no teapot at hand (and you respect [RFC7168](https://tools.ietf.org/html/rfc7168))
$app->halt(418, 'Feel free to take me as teapot!');

If you would like to render a template with a list of error messages, you should use the Skinny application’s render() method instead.

<?php
$app = \Skinny\Skinny::newInstance();
$app->get('/foo', function () use ($app) {
    $errorData = array('error' => 'Permission Denied');
    $app->render('errorTemplate.php', $errorData, 403);
});
$app->run();

The halt() method may send any type of HTTP response to the client: informational, success, redirect, not found, client error, or server error.

Pass

A route can tell the Skinny application to continue to the next matching route with the Skinny application’s pass() method. When this method is invoked, the Skinny application will immediately stop processing the current matching route and invoke the next matching route. If no subsequent matching route is found, a 404 Not Found response is sent to the client. Here is an example. Assume an HTTP request for “GET /hello/Frank”.

<?php
$app = \Skinny\Skinny::newInstance();
$app->get('/hello/Frank', function () use ($app) {
    echo "You won't see this...";
    $app->pass();
});
$app->get('/hello/:name', function ($name) use ($app) {
    echo "But you will see this!";
});
$app->run();

Redirect

It is easy to redirect the client to another URL with the Skinny application’s redirect() method. This method accepts two arguments: the first argument is the URL to which the client will redirect; the second optional argument is the HTTP status code. By default the redirect() method will send a 302 Temporary Redirect response.

<?php
$app = \Skinny\Skinny::newInstance();
$app->get('/foo', function () use ($app) {
    $app->redirect('/bar');
});
$app->run();

Or if you wish to use a permanent redirect, you must specify the destination URL as the first parameter and the HTTP status code as the second parameter.

<?php
$app = \Skinny\Skinny::newInstance();
$app->get('/old', function () use ($app) {
    $app->redirect('/new', 301);
});
$app->run();

This method will automatically set the Location: header. The HTTP redirect response will be sent to the HTTP client immediately.

Stop

The Skinny application’s stop() method will stop the Skinny application and send the current HTTP response to the client as is. No ifs, ands, or buts.

<?php
$app = \Skinny\Skinny::newInstance();
$app->get('/foo', function () use ($app) {
    echo "You will see this...";
    $app->stop();
    echo "But not this";
});
$app->run();

URL For

The Skinny application's urlFor() method lets you dynamically create URLs for a named route so that, were a route pattern to change, your URLs would update automatically without breaking your application. This example demonstrates how to generate URLs for a named route.

<?php
$app = \Skinny\Skinny::newInstance();

//Create a named routea - legacy method
$app->rtGet('/hello/:name', function ($name) use ($app) {
    echo "Hello $name";
})->name('hello');

//Generate a URL for the named route
$url = $app->urlFor('hello', array('name' => 'Josh'));

In this example, $url is “/hello/Josh”. To use the urlFor() method, you must first assign a name to a route. Next, invoke the urlFor() method. The first argument is the name of the route, and the second argument is an associative array used to replace the route’s URL parameters with actual values; the array’s keys must match parameters in the route’s URI and the values will be used as substitutions.

URL Rewriting

I strongly encourage you to use a web server that supports URL rewriting; this will let you enjoy clean, human-friendly URLs with your Skinny application. To enable URL rewriting, you should use the appropriate tools provided by your web server to forward all HTTP requests to the PHP file in which you instantiate and run your Skinny application. The following are sample, bare minimum, configurations for Apache with mod_php and nginx. These are not meant to be production ready configurations but should be enough to get you up and running. To read more on server configuration see Web Server Setup Tips.

Apache and mod_rewrite

Here is an example directory structure:

/path/www.mysite.com/
    public_html/ <-- Document root!
        .htaccess
        index.php <-- I instantiate Skinny here!
    lib/
        Skinny/ <-- I store Skinny lib files here!

The .htaccess file in the directory structure above contains:

RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [QSA,L]

You also need a directory directive to enable .htaccess files and allow the RewriteEngine directive to be used. This is sometimes done globally in the httpd.conf file, but its generally a good idea to limit the directive to just your virtual host by enclosing it in your VirtualHost configuration block. This is generally setup in your configuration in the form of:

<VirtualHost *:80>
    ServerAdmin me@mysite.com
    DocumentRoot "/path/www.mysite.com/public_html"
    ServerName mysite.com
    ServerAlias www.mysite.com

    #ErrorLog "logs/mysite.com-error.log"
    #CustomLog "logs/mysite.com-access.log" combined

    <Directory "/path/www.mysite.com/public_html">
        AllowOverride All
        Order allow,deny
        Allow from all
    </Directory>
</VirtualHost>

As a result, Apache will send all requests for non-existent files to my index.php script in which I instantiate and run my Skinny application. With URL rewriting enabled and assuming the following Skinny application is defined in index.php, you can access the application route below at “/foo” rather than “/index.php/foo”.

<?php
$app = \Skinny\Skinny::newInstance();
$app->get('/foo', function () {
    echo "Foo!";
});
$app->run();

nginx

We will use the same example directory structure as before, but with nginx our configuration will go into nginx.conf.

/path/www.mysite.com/
    public_html/ <-- Document root!
        index.php <-- I instantiate Skinny here!
    lib/
        Skinny/ <-- I store Skinny lib files here!

Here is a snippet of a nginx.conf in which we use the try_files directive to serve the file if it exists, good for static files (images, css, js etc), and otherwise forward it on to the index.php file.

server {
    listen       80;
    server_name  www.mysite.com mysite.com;
    root         /path/www.mysite.com/public_html;

    try_files $uri $uri/ /index.php?$query_string;  

    # this will only pass index.php to the fastcgi process which is generally safer but
    # assumes the whole site is run via Skinny.
    location /index.php {
        fastcgi_connect_timeout 3s;     # default of 60s is just too long
        fastcgi_read_timeout 10s;       # default of 60s is just too long
        include fastcgi_params;
        fastcgi_pass 127.0.0.1:9000;    # assumes you are running php-fpm locally on port 9000
    }
}

Most installations will have a default fastcgi_params file setup that you can just include as shown above. Some configurations don’t include the SCRIPT_FILENAME parameter. You must ensure you include this parameter otherwise you might end up with a No input file specified error from the fastcgi process. This can be done directly in the location block or simply added to the fastcgi_params file. Either way it looks like this:

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

Without URL Rewriting

Skinny will work without URL rewriting. In this scenario, you must include the name of the PHP file in which you instantiate and run the Skinny application in the resource URI. For example, assume the following Skinny application is defined in index.php at the top level of your virtual host’s document root:

<?php
$app = \Skinny\Skinny::newInstance();
$app->get('/foo', function () {
    echo "Foo!";
});
$app->run();

You can access the defined route at “/index.php/foo”. If the same application is instead defined in index.php inside of the physical subdirectory blog/, you can access the defined route at /blog/index.php/foo.