Routing Basics

Defining a route can be done with just a couple lines of code. To add a route to your Concrete CMS application, first open application/bootstrap/app.php. Then, get a copy of the application router instance from the Concrete\Core\Application\Application object, which is already defined as $app in the local scope of the file.

$router = $app->make(‘router’);

Next, define your route. In our example before, we wanted to define a route at /api/current_user that would return a simple JSON representation of the currently logged-in user. Let’s define that route.

$router->get(‘/api/current_user’, function() {
    return ‘Return a simple string.’;
}

Then, when you visit http://www.mysite.com/api/current_user you’ll receive a simple “Return a simple string.” on a white screen.

The most basic routes accept a URI and a Closure, as in the example above.

HTTP Verb Support

Notice the get() method that we used above? This method specifies that the route in question will respond to the HTTP GET request. There are HTTP-specific verbs for all routes

  • $router->get($endpoint, $callback)
  • $router->post($endpoint, $callback)
  • $router->put($endpoint, $callback)
  • $router->patch($endpoint, $callback)
  • $router->delete($endpoint, $callback)
  • $router->options($endpoint, $callback) Want to make a route respond to all verbs? Use the all() method.
  • $router->all($endpoint, $callback)

Now, let’s move onto something a little less contrived.

Route Parameters

If you need to capture parameters from the route, you can do with required parameters. Here’s an example of a route that requires an ID.

$router->get(‘/api/customer/{customerId}, function($customerId) {
    return ‘The customer ID is: ‘ . $customerId;
});

Routes can take multiple parameters. They are passed to the callback in order:

$router->get(‘/api/customers/{country}/{province}, function($country, $province) {
    return ‘We are searching for customers in ‘ . $country . ‘ and in ‘ . $province;
});

Parameter Formats

You can force routes to take parameters that conform to a particular format by using setRequirements on the Router object when defining a route. Pass in a regular expression to set the format. For example, this forces an alphanumeric string for the particular parameter:

$router->get('/rss/{identifier}', function() {
    // Callback here.
    })
    ->setRequirements(['identifier' => '[A-Za-z0-9_/.]+']);

Named Routes

If you’d like you can name a particular route. This information will be passed through the route, and you can always retrieve a route from the system route collection by name, which can be useful in certain contexts.

$router->get('/rss/{identifier}', function() {
    // Callback here.
    })
    ->setName(‘rss’)
    ->setRequirements(['identifier' => '[A-Za-z0-9_/.]+']);

Note: The Router Object is Chainable

As you can see above, all methods when working with route generation are chainable, which results in a simpler, more expressive syntax.

Accessing the Request from within a Route Callback

Accessing the Request object from within a route callback is no different than elsewhere – just use the createFromGlobals method on the Request object:

$router->put(‘/api/customer/update/{customerId}, function($customerId) {
    $request = \Concrete\Core\Http\Request::createFromGlobals();
    $email = $request->request->get(‘email’);
    // Now we have the customerId, and we have the email address from the POST that we want to update it to.
});

Responses

Returning strings makes routing extremely simple, but this is a bit contrived: strings don’t give you the ability to set HTTP Response codes, and they also require you to work with your own JSON formats. Thankfully, the Concrete router works very well with the Concrete\Core\Http\Response object (which itself is just a subclass of Symfony\Component\HttpFoundation\Response. Return any response object from your route, and it will be handled automatically.

Simple Response

Here’s our first example, using the Response object.

use Symfony\Component\HttpFoundation\Response;
    $router->get(‘/api/current_user’, function() {
        $response = new Response(‘Return a simple string.’);
        return $response;
    }

But it gets better when you want something more advanced. Let’s implement our current_user endpoint functionality.

JSON Response

use Symfony\Component\HttpFoundation\JsonResponse;
$router->get(‘/api/current_user’, function() {
    $u = new \User();
    if ($u->isRegistered()) {
        $data = [];
        $data[‘user_id’] = $u->getUserID();
        $data[‘username’] = $u->getUserName();
        return new JsonResponse($data);
    } else {
        return new JsonResponse([], 400);
    }
});

What’s going on here? Well, if the user is registered, we create a PHP array with their user ID and username populated, and return it inside the JsonResponse. If they’re not logged in, we return an empty object, along with the HTTP 400 error code.

Redirect Responses

Returning a redirect response from a route works as well.

use Symfony\Component\HttpFoundation\RedirectResponse;
$router->get(‘/api/current_user’, function() {
    return new RedirectResponse(‘/api/new_location’, 301);
});

Error Handling in Routes

The Concrete\Core\Error\ErrorList\ErrorList object is a handy object for tracking errors and formatting them. This object works well in a route context as well. Let’s refactor our code above to use the error list object, and use its builtin methods for error response.

use Symfony\Component\HttpFoundation\JsonResponse;
use Concrete\Core\Error\ErrorList\ErrorList;
$router->get(‘/api/current_user’, function() {
    $u = new \User();
    $errors = new ErrorList;
    if ($u->isRegistered()) {
        // This code is the same.
    } else {
        $errors->add(‘You must be logged in to get the current user.’);
        return $errors->createResponse(400);
    }
});

The ErrorList object keeps track of all errors added to it. Running createResponse from it returns a standardized JSON error object, with all error strings, using the HTTP error code passed to it.

Middleware

In Concrete any route can be attached to middleware. Middleware is a reusable routine that filters HTTP requests. A middleware might be responsible for adding certain HTTP headers on every request, or logging all requests.

Example Middleware

If you want to make an API route only accessible to authenticated API calls, just add the Concrete\Core\Http\Middleware\OAuthAuthenticationMiddleware to it:

$router->get(‘/api/current_user’, function() {
    //snip
})->addMiddleware(OAuthAuthenticationMiddleware::class);

Going Beyond Closures

This is all it takes to define routes in Concrete – just define these routes in application/bootstrap/app.php as you need. However, you may not want to define all your route logic within a \Closure. Instead, you should wire your routes to controllers. Read on for more.