Sometimes the builtin routes do not cover all requirements of a project. But
REST also allows the creation of custom Handlers, which allow developers to
take full control.
Building a custom web service
This guide will show how the extension can be used as a starting point for
custom web services. Web services are built around the concept of Handlers
who are responsible for handling requests for a specific Resource Type.
Access configuration
The following configuration allows read and write access to all requests
matching the Resource Type customhandler (e.g.
https://example.com/rest/customhandler, or
https://example.com/rest/customhandler/subpath)
# {EXT:my_ext}/Configuration/Sets/{site}/settings.yaml
rest:
settings:
paths:
customhandler:
path: customhandler
read: allow
write: allow
handlerClass: \MyVendor\MyExt\Rest\Handler
Basic Handler structure
The configuration above tells the REST extension to use the service
\MyVendor\MyExt\Rest\Handler to handle requests matching the Resource Type
customhandler. A Handler must implement \Cundd\Rest\Handler\HandlerInterface
and it's method configureRoutes(RouterInterface $router, RestRequestInterface $request).
A Handler which only accepts requests for a single URL and HTTP method could
look like this:
<?php
declare(strict_types=1);
use Cundd\Rest\Handler\HandlerInterface;
use Cundd\Rest\Http\RestRequestInterface;
use Cundd\Rest\Router\RouterInterface;
/**
* Example Custom Handler
*/
final class CustomHandler implements HandlerInterface
{
public function configureRoutes(
RouterInterface $router,
RestRequestInterface $request,
): void {
$router->routeGet($request->getResourceType(), $this->hello(...));
}
public function hello(RestRequestInterface $request): string
{
return 'hello world';
}
}
Building a Route
A route is an instance of \Cundd\Rest\Router\Route. It encapsulates the
necessary information for the Router to dispatch incoming requests to the
correct callback (typically a method provided by the Handler).
To route incoming requests to the correct destination, the Route stores a
URL-path-pattern and the accepted HTTP method. The pattern will be compared
against the current request's URI.
Important thing about the route: The extension must not be part of the
Route's pattern (it will be removed from the requested URL. E.g.http://localhost:8888/rest/customhandler.jsonwill be routed using the normalized URIhttp://localhost:8888/rest/customhandler).
The example above defines a new Handler with a single Route: A GET-request
to the Handler's Resource Type. An alternative way to register the Route
would be:
<?php
declare(strict_types=1);
use Cundd\Rest\Handler\HandlerInterface;
use Cundd\Rest\Http\RestRequestInterface;
use Cundd\Rest\Router\RouterInterface;
final class CustomHandler2 implements HandlerInterface
{
public function configureRoutes(
RouterInterface $router,
RestRequestInterface $request,
): void {
$router->add(
new Route($request->getResourceType(), 'GET', $this->hello(...))
);
}
public function hello(RestRequestInterface $request): string
{
return 'hello world';
}
}
In both examples $request->getResourceType() is used as the Route's pattern.
This can act as an alias to the path configured in the site settings.
The Route's pattern of course is not limited to the Resource Type. Patterns
can consist of multiple path segments (e.g. $request->getResourceType() . '/foo/bar' to handle requests for .../customhandler/foo/bar).
<?php
use Cundd\Rest\Router\Route;
use Cundd\Rest\Http\RestRequestInterface;
new Route($request->getResourceType() . '/foo/bar', 'GET', function(RestRequestInterface $request) {
# curl -X GET http://localhost:8888/rest/customhandler/foo/bar
});
Callbacks for other HTTP request methods can be added in the same way. For the most common HTTP methods factory methods are available.
<?php
use Cundd\Rest\Router\Route;
use Cundd\Rest\Http\RestRequestInterface;
// Route::get(...) or
new Route($request->getResourceType() . '/foo', 'GET', function(RestRequestInterface $request) {
# curl http://example.com/rest/customhandler/foo
});
// Route::post(...) or
new Route($request->getResourceType() . '/foo', 'POST', function(RestRequestInterface $request) {
# curl -X POST http://example.com/rest/customhandler/foo
});
// Route::put(...) or
new Route($request->getResourceType() . '/foo', 'PUT', function(RestRequestInterface $request) {
# curl -X PUT http://example.com/rest/customhandler/foo
});
// Route::delete(...) or
new Route($request->getResourceType() . '/foo', 'DELETE', function(RestRequestInterface $request) {
# curl -X DELETE http://example.com/rest/customhandler/foo
});
new Route($request->getResourceType() . '/foo', 'PATCH', function(RestRequestInterface $request) {
# curl -X PATCH http://example.com/rest/customhandler/foo
});
Registering a Route
Finally a created Route must be added to the Router instance:
<?php
use Cundd\Rest\Router\Route;
use Cundd\Rest\Http\RestRequestInterface;
$myRoute = Route::get($request->getResourceType(), function(RestRequestInterface $request) {});
$router->add($myRoute);
Parameters
A routing system would be incomplete without the ability to pass variable parts
to the callback. The Router allows to use a set of parameter expressions
inside the route's pattern.
A parameter expression must be a complete URI segment
- ❌
/foo{slug}/- ✅
/foo/{slug}/
Matching strings aka. slug
Extracts the value from segments matching the regular expression [a-zA-Z0-9\._\-]+:
<?php
use Cundd\Rest\Router\Route;
use Cundd\Rest\Http\RestRequestInterface;
Route::get($request->getResourceType() . '/{slug}', function(RestRequestInterface $request, string $theParameter) {
// Callback will be invoked for
// curl -X GET http://localhost:8888/rest/customhandler/some-string
// with $theParameter set to 'some-string'
});
Matching integers
Extract positive integer values:
<?php
use Cundd\Rest\Router\Route;
use Cundd\Rest\Http\RestRequestInterface;
Route::get($request->getResourceType() . '/{integer}', function(RestRequestInterface $request, int $theParameter) {
// Callback will be invoked for
// curl -X GET http://localhost:8888/rest/customhandler/109
// with $theParameter set to 109
});
Matching floats
Extract positive float values:
<?php
use Cundd\Rest\Router\Route;
use Cundd\Rest\Http\RestRequestInterface;
Route::get($request->getResourceType() . '/{float}', function(RestRequestInterface $request, float $theParameter) {
// Callback will be invoked for
// curl -X GET http://localhost:8888/rest/customhandler/109.0
// with $theParameter set to 109.0
});
Matching booleans
Extract the value from segments matching the regular expression (1|true|on|yes|0|false|off|no) and converts it into a
boolean
<?php
use Cundd\Rest\Router\Route;
use Cundd\Rest\Http\RestRequestInterface;
Route::get($request->getResourceType() . '/{boolean}', function(RestRequestInterface $request, bool $theParameter) {
// Callback will be invoked for
// curl -X GET http://localhost:8888/rest/customhandler/yes
// with $theParameter set to true
});
Matching anything aka. raw
Extracts the value from segments matching the regular expression [^/]+
<?php
use Cundd\Rest\Router\Route;
use Cundd\Rest\Http\RestRequestInterface;
Route::get($request->getResourceType() . '/{raw}', function(RestRequestInterface $request, $theParameter) {
// Callback will be invoked for
// curl -X GET http://localhost:8888/rest/customhandler/Mr Müller
// with $theParameter set to 'Mr%20Müller'
});
The parameter value will not be decoded before being passed to the
Route's callback
Response
The extension utilizes the \Cundd\Rest\ResponseFactory class to transform raw
Handler results into a presentation matching the format from the request's getFormat() method.
If a custom response should be sent to the client, without additional
formatting, the Handler callback should return an instance of
\Psr\Http\Message\ResponseInterface (e.g. created with the builtin
\Cundd\Rest\ResponseFactory->createResponse($data, int $status)).
For a complete example see
\Cundd\Rest\Tests\Functional\Fixtures\CustomHandler