metanull / restful-service
RESTfulService provides the needed to implement a RESTful web service.
README
In order to create a RESTful webservice with this class, we need:
- An application class
- Some request handlers (or routes)
- Register our handlers withing the application
- Run the application
Here is a simple sample implementation.
Importing the library
- If not yet done, Install composer
- Create a directory for your project, in this directory:
- Install the MetaNull\RESTfulService package:
composer require metanull/restful-service
- Create the following files:
The Application Class
This is the core of your application. It is responsible for:
- initializing the system, by instanciating a
RESTfulService
object - registering the request handlers of the application
- invoking the
Serve()
method of theRESTfulService
- Note:
Serve()
looks for a suitable handler for the request and invokes it.
./Application/Application.php
<?php
namespace Application;
use MetaNull\RESTfulService\Network\Http\Http;
use MetaNull\RESTfulService\Network\Http\RESTfulService;
use MetaNull\RESTfulService\Application\ApplicationInterface;
/** The RESTfulService application */
class Application implements ApplicationInterface
{
public ?RESTfulService $service = null;
public function __construct()
{
$this->service = new RESTfulService(); // Create the RESTfulService
}
public function Run()
{
// Add some headers that will be returned with any response
$this->service->serviceData['ExtraHeaders'][Http::HEADER_ACCESS_CONTROL_ALLOW_ORIGIN] = Http::ALLOW_ORIGIN_ANY;
$this->service->serviceData['ExtraHeaders'][Http::HEADER_VARY] = Http::VARY_ORIGIN;
// Add request handlers - in order of precedence
$this->service->Route(Routes\Root::class, '/'); // Handler for requests to "/"
$this->service->Route(Routes\Item::class, '%^/item/([\d+])$%'); // Handler fro requests matching the provided perl regexp
$this->service->Route(Routes\Fallback::class); // Handler for anything else
// Handle the received request
$this->service->Serve();
}
}
The Request Handler Classes
Handling "GET /"
First we want to handle any GET
request that is reaching for the root of our API (e.g.: /
).
To be recognised as a handler for GET
requests, our handler shall implements the Get
interface.
All base request handler classes and interfaces implement implement the RouteHandlerInterface
. It defines a mechanism by which the Application can ask to the handler if it is capable to handle a given route.
It is possible to define your own route handler classes, but the system comes with a a few predefined ones.
In this case we will extend the StringMatchRouteHandler
class. This is a handler that simply compares the route
with a string. If they are strictly equals, then the handler can process the request.
Note: The value of that string could have been defined in the class itself (using the SpecializedRouteHandler::SetExpression()
method), but it is more flexible and easier to read if all 'routes' are defined in
the application initialisation function.
Here is how it was done in the Run()
method of our Application
class:
// Registers our 'Root' handler for requests to `/`.
$this->service->Route('\Application\Routes\Root', '/');
./Application/Routes/Root.php
<?php
namespace Application\Routes;
use MetaNull\RESTfulService\Network\Http\Get;
use MetaNull\RESTfulService\Network\Http\StringMatchRouteHandler;
use MetaNull\RESTfulService\Network\Http\Request;
use MetaNull\RESTfulService\Network\Http\Response;
use MetaNull\RESTfulService\Network\Http\ResponseFactory;
class Root extends StringMatchRouteHandler implements Get
{
/**
* A RequestHandler provides a method to handle a HTTP Request, this processing produces a HTTP Response
* @param Request $request The received HTTP request
* @param array $serviceData Data shared by the controller
* @param string $routeParameters,... May receive parameters that were extracted from the Request's Route)
* @return Response The resulting HTTP Response
* @throws HttpException
*/
public function Handle(Request $request, array &$serviceData, string ...$routeParameters) : Response
{
$response = ResponseFactory::Json([
'message' => 'It works!',
'handler' => __METHOD__,
]);
return $response;
}
}
Handling "GET /Item/{itemId}
Pretty similar to the previous example, this time our handler needs to support some parameters.
It is supposed to receive the ID of an item, and to return the matching item.
We can achieve this by extanding the RegexpMatchRouteHandler
class. Rather than a string comparison, this class will perform a regular expression matching with the route (using PERL regular expressions).
If the route matches, then the handler processes the request.
If the regular expression extracts some information, then the extracted values are automatically passed to our handler via the third argument of its Handle()
method: string ...$routeParamters
.
In the Run()
method of our Application
class, we had registered our handler with:
// Registers our 'Item' handler for requests to `/item/{itemId}`.
// itemId is expected to be composed of one or more digits.
$this->service->Route('\Application\Routes\Item', '%^/item/([\d+])$%');
The expression %^/item/(\d+)$%
will extract exactly one group from the route: (\d+)
.
Note: \d+
stands for a series of 1 or more consecutive digits.
Our handler will receive the extracted value as $routeParameters[0]
./Application/Routes/Item.php
// Registers our 'Item' handler for requests to `/item/{itemId}`.
// itemId is expected to be composed of one or more digits.
$this->service->Route('\Application\Routes\Item', '%^/item/(\d+)$%');
<?php
namespace Application\Routes;
use MetaNull\RESTfulService\Network\Http\Get;
use MetaNull\RESTfulService\Network\Http\RegexpMatchRouteHandler;
use MetaNull\RESTfulService\Network\Http\Request;
use MetaNull\RESTfulService\Network\Http\Response;
use MetaNull\RESTfulService\Network\Http\ResponseFactory;
class Item extends RegexpMatchRouteHandler implements Get
{
/**
* A RequestHandler provides a method to handle a HTTP Request, this processing produces a HTTP Response
* @param Request $request The received HTTP request
* @param array $serviceData Data shared by the controller
* @param string $routeParameters,... May receive parameters that were extracted from the Request's Route)
* @return Response The resulting HTTP Response
* @throws HttpException
*/
public function Handle(Request $request, array &$serviceData, string ...$routeParameters) : Response
{
dd($request->route);
$response = ResponseFactory::Json([
'message' => 'It works!',
'item_id' => $routeParameters[0] ?? null; // The ItemId received from the route.
'handler' => __METHOD__,
]);
return $response;
}
}
Handling "Any" other GET requests
Finally we would like to intercept any request that was not processed by any of our handlers.
This is made possible by extending the class AnyRouteHandler
.
Unlike the previous two examples, AnyRouteHandler doesn't need to check if it can process the route or not. It will simply handle Any requesty. Because of this, it is important to define this route last, as handlers are evaluated in the order in which they are defined.
If this route was defined first, it would swallow any GET
request it sees.
Here is how the handler was registered by the Run()
method of our Application
class:
// Registers our 'Fallback' handler for any other request.
$this->service->Route('\Application\Routes\Fallback');
./Application/Routes/Fallback.php
<?php
namespace Application\Routes;
use MetaNull\RESTfulService\Network\Http\Get;
use MetaNull\RESTfulService\Network\Http\AnyRouteHandler;
use MetaNull\RESTfulService\Network\Http\Request;
use MetaNull\RESTfulService\Network\Http\Response;
use MetaNull\RESTfulService\Network\Http\ResponseFactory;
class Fallback extends AnyRouteHandler implements Get
{
/**
* A RequestHandler provides a method to handle a HTTP Request, this processing produces a HTTP Response
* @param Request $request The received HTTP request
* @param array $serviceData Data shared by the controller
* @param string $routeParameters,... May receive parameters that were extracted from the Request's Route)
* @return Response The resulting HTTP Response
* @throws HttpException
*/
public function Handle(Request $request, array &$serviceData, string ...$routeParameters) : Response
{
$response = ResponseFactory::Json([
'message' => 'It works!',
'handler' => __METHOD__,
]);
return $response;
}
}
What else do we need?
A bootstrap file
The bootstrap is the starting point of the application. It is responsible for setting up the autoloader, and for invoking your application's main method.
./bootstrap.php
<?php
// Your autoloader (see below)
require_once __DIR__ . '/autoload.php';
// Composer autoloader
require_once __DIR__ . '/vendor/autoload.php';
use Application\Application;
$app = new Application();
$app->Run();
An autoloader for our classes
The autoloader is capable of loading 'your' application classes (defined in the directory '/Application').
./autoload.php
<?php
function autoload($class)
{
// Define an array of namespace prefixes and their corresponding base directory paths
$prefixes = [
'Application\\' => __DIR__ . '/Application',
];
// Iterate over the prefixes and try to match the class with the corresponding file path
foreach ($prefixes as $prefix => $baseDir) {
// Check if the class uses the current namespace prefix
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
continue;
}
// Remove the namespace prefix and convert the namespace separators to directory separators
$relativeClass = substr($class, $len);
$relativeClass = str_replace('\\', '/', $relativeClass);
// Construct the file path by appending the base directory path and the relative class name with '.php' extension
$file = $baseDir . '/' . $relativeClass . '.php';
// Require the file if it exists
if (file_exists($file)) {
require_once $file;
}
}
}
// Register the autoloader function with spl_autoload_register()
spl_autoload_register('autoload');
Have our application's code published in a web server
The configuration of the webserver is outside of the scope of this document. But as an example, here is how it could be configured with Apache 2.4.
Copying the following .htaccess
file in our application's root directory would instruct apache to Rewrite all the requests so that they are passed (transparently) to our bootstrap.php
.
By example if the server receives a request for http://my.domain.com/directory/do/something
, it would rewrite it into /directory/bootstrap.php?route=/do/something
.
Note: route
is the parameter used internally by the RESTfulService framework. The service captures that parameter and makes it available to all request handlers via the Request
object.
public function Handle(Request $request, array &$serviceData, string ...$routeParameters) : Response
{
error_log($request->route); // Prints the received route into the php log file
}
./.htaccess
# If you are using Apache 2.4, and do not have access to the config files, you
# may copy this .htaccess file to the root of your directory.
#
# It is required that administrators of the server permit loading .htaccess
# file from you application's directory. This is typically achieved by adding:
# AllowOverride FileInfo
# in the Directory configuration.
# Modify /directory/ to match with the path (relative to the root) of your
# server. Note that the trailing slash is important!
RewriteBase /directory/
# Rewrite all requests so that they are "passed" to the bootstrap function
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^bootstrap\.php$ "bootstrap.php?route=/" [L,QSA]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ "bootstrap.php?route=/$1" [L,DPI,QSA]
</IfModule>