/Razor

A tiny PHP framework for creating simple, testable REST applications

Primary LanguagePHPMIT LicenseMIT

Razor

Razor is a lightweight micro framework based around the concept of REST. It is similar to a number of projects like Slim and Silex but is designed to be syntactically nicer to use, from personal preference.

As noted above the framework is focused around RESTful web applications. Lets say we are working on the obligatory TODO application- we are going to have an end point in our API that represents the collection of TODO items or, more correctly, resources.

To model this in Razor, we would create a file in our web-facing folder called todos.php which would contain the following code:

<?php

// File: todos.php

require_once "/path/to/vendor/autoload.php";

use Razor\EndPoint;

(new EndPoint())

    ->run();

We can think of this file as our resource collection; i.e. if we want to add, edit, get, or remove TODO items then that change comes through this file. Razor is geared towards this mindset and so all the logic pertaining to a resource is encapsulated in the EndPoint object. To be able to perform actions against our resource we have to assign a delegate into a slot representing the HTTP request method, or more simply:

<?php

// File: todos.php

require_once "/path/to/vendor/autoload.php";

use Razor\EndPoint;

(new EndPoint())

    ->get(function ()
    {
        // Hey, that was easy!
    })

    ->run();

Anyone used to the format of other micro-frameworks should recognise this syntax. Essentially, in the instance that the web browser makes an HTTP GET request, the code in the delegate (or Closure) passed to the get() method is invoked. There are method calls for each of the major HTTP Verbs, and more may be supported in the future.

Great! So our client can access our todo.php page from a browser and they see... Nothing. Lets fix that by sending the browser a message:

<?php

// File: todos.php

require_once "/path/to/vendor/autoload.php";

use Razor\EndPoint;
use Razor\Services\Http;

(new EndPoint())

    ->get(function (Http $http)
    {
        return $http->response->standard("Hello, world!");
    })

    ->run();

Now when the client connects they will get the message Hello, world!. You may have noticed that you didn't have to specify that the $http object exists anywhere in this file and that it has been passed through as an argument to the delegate. What is happening here is called Service Injection.

A service is an object or value that provides some functionality to your application. When Razor decides to invoke the delegate for the HTTP GET request, it examines the arguments that are specified and looks to see if it can provide them. If it can, it then injects those arguments when the delegate is invoked. If you have an AngularJS background this will seem native to you, if not then this may feel a little foreign. The goal is to make your delegate stateless, i.e. not this:

<?php

// File: todos.php

require_once "/path/to/vendor/autoload.php";

use Razor\EndPoint;
use Razor\Services\Http;

$http = new Http(/* Have to supply all the arguments here ... */);

(new EndPoint())

    ->get(function () use ($http)
    {
        return $http->response->standard("Hello, world!");
    })

    ->run();

This is because, firstly, you always have to create the object or write quite ugly code to lazy load it, and secondly because function () use (...) is really terse, ugly and becomes difficult to maintain and read over time.

In this particular instance, $http is a service that Razor ships with, but you can use services for your own code too. Say we have an object already that handles the management of a todo's database, we can specify it in our code as follows:

<?php

// File: todos.php

require_once "/path/to/vendor/autoload.php";

use Razor\Razor;
use Razor\EndPoint;
use Razor\Services\Http;

Razor::environment()
     ->services()
     ->register("todoRepo", function ()
     {
        return new TodoRepo(/* ... */);
     });

(new EndPoint())

    ->get(function (Http $http, TodoRepo $todoRepo)
    {
        $id = $http->request->get("id");

        $data = $todoRepo->get($id);

        return $http->response->json($data);
    })

    ->run();

Here we register the service with the frameworks Environment using a delegate. Then in our code, we specify this service as a dependency. It is important to note that it is the name todoRepo that is used to find the service and not the type hint. This is because we may end up with multiple objects with the same type needing to be injected, like different log objects that both use the same API.

Also important to note is that this delegate can also receive injected services, like:

Razor::environment()
     ->services()
     ->registerMany(array(
         "conn", function ()
         {
            return new PDO(/* ... */);
         },

         "todoRepo", function (PDO $conn)
         {
            return new TodoRepo($conn);
         }
     ));

N.B. It is suggested that you move these service registrations to a bootstrap.php file so that as your application grows you can keep on top of it.

Lets say you want to open this app out onto the web, but you want to make sure you have some security in place. You could expose a service to handle this, but that will undoubtedly lead to lots of boilerplate. As such the framework ships with the well known idea of Middleware.

A Middleware is a small object which is designed to be called before your delegate, and perform request filtering or response amendments. This means you can write small pieces of functionality and compose your handling from this. An example of a middleware is:

// File: src/SecurityMiddleware.php

use Razor\Middleware;
use Razor\Services\Http;

class SecurityMiddleware extends Middleware
{
    public function __invoke(Http $http)
    {
        if (!$http->request->isSecure()) {
            return $http->response->standard("Whoa! Your connection is not secure!", 400);
        }

        return $this->invokeDelegate();
    }
}

We can then utilise the SecurityMiddleware by:

<?php

// File: todos.php

require_once "/path/to/vendor/autoload.php";

use Razor\EndPoint;
use Razor\Services\Http;

(new EndPoint())

    ->get(new SecurityMiddleware(function (Http $http, TodoRepo $todoRepo)
    {
        $id = $http->request->get("id");

        $data = $todoRepo->get($id);

        return $http->response->json($data);
    }))

    ->run();

As such the SecurityMiddleware will be called first and if the connection is secure it will then invoke our application logic for the GET request. We can also chain middleware together:

<?php

// File: todos.php

require_once "/path/to/vendor/autoload.php";

use Razor\EndPoint;
use Razor\Services\Http;

(new EndPoint())

    ->get(new SecurityMiddleware(new EnsureJsonMiddleware(function (Http $http, TodoRepo $todoRepo)
    {
        $id = $http->request->get("id");

        $data = $todoRepo->get($id);

        return $http->response->json($data);
    })))

    ->run();

About Razor

The framework is designed to be developer friendly. You want to write well structured code, but you also need to provide justification for every line of code in your project? Razor is as lightweight as possible, using a simple API for Service Injection and the well trusted Symfony HTTP Foundation library in it's HTTP Service.

You want to write code that is easily maintainable? Services allow you to keep your shared logic accessible without the need for excessive boilerplate in your application logic. Keep your application logic in the URL end points so you can easily identify what is going on.

Overall, Razor is designed to give you an elegant way of working with HTTP requests without bloat. If it fits your work flow, then more power to you!

Installation

Installation is via Composer, add Razor to your dependencies:

{
    "require": {
        "rawebone/razor-library": "dev-master"
    }
}

Once the project goes to stable, there will be two branches available for installation:

  • 1.x.y
  • 1.x.y-compat

The compat branch will allow users of PHP5.3 and above to use the framework while the main branch will be PHP5.4 and above.

Please base all pull requests against master.

License

MIT License, go wild.