johnvandeweghe/lunixrest

There is no license information available for the latest version (v3.2.0) of this package.

A lightweight REST server

v3.2.0 2017-06-04 06:37 UTC

README

Build Status Code Coverage Scrutinizer Code Quality

Overview

LunixREST is a highly extensible, lightweight library for writing REST APIs in PHP (7.1+).

It's primary goal is to allow the creation of a REST API in which every part of it's behaviour can be overwritten without any code hacks. Because of this, it's generally very compatible with existing codebases.

Features

  • API key access control
  • Routing to endpoints
  • API versioning
  • API Access throttling
  • PSR-2 styled
  • PSR-3 (logging) compatible
  • PSR-4 (autoloading) compatible
  • PSR-6 (caching) compatible
  • PSR-7 (http) compatible
  • Versatile output formatting

See https://github.com/johnvandeweghe/LunixREST-Basics for some basic implementations, and examples.

Project standards

Unit test coverage

This project seeks to approach as close to 100% code coverage at all times as reasonably possible. Both in numbers, and in actual code path coverage. If that is ever not the case, leave an issue and it will be addressed ASAP.

Contributing

Contributions for this project are really appreciated. Just leave a pull request with the requested change/addition. Preferably let me know you're working on it by leaving an issue. All PRs not following the standards outlined here will not be merged until any differences are resolved.

Code style

As mentioned in the Features list above, this project adheres as closely to PSR-2 as possible. No auto stylers are utilized, so there may be portions of the code that violate this. Feel free to drop an issue ticket for any infractions and I will address them.

Namespacing

Namespacing follows PSR-4. Further, namespaces should be nested by dependency. Meaning, if a Server is the only class that uses a Widget, then a Widget should be within the Server namespace. Cross dependencies should result in the depended class being no closer to root than needed.

Installation

Requirements

All dependencies are specified in the composer.json, so as long as you use composer with this library, all dependencies should be taken care of.

That being said, here are some dependencies:

  • PHP 7.1+
  • php-mbstring
  • PSR/Cache
  • PSR/HTTP-Message
  • PSR/Log

Version Notice

This project updates master regularly. Changes to master that are not released are not guaranteed to be stable, and should be treated as such. Use the release tags for production projects.

All minor version number updates will have guaranteed backwards compatibility. Major version changes won't be held to that standard, but generally the goal is to minimise interface changes.

Installation

This project is listed in Packigist, the default repository for Composer. So installation is as simple as:

composer require johnvandeweghe/lunixrest

Usage

HTTPServer

The basis of an implementation of LunixREST is the class HTTPServer. This class takes in a PSR-7 ServerRequestInterface and returns a ResponseInterface that can be dumped to the SAPI.

Here is an example that uses Guzzle's PSR-7 implementation (which if you're using LunixREST-Basics is already included).

$httpServer = new \LunixREST\HTTPServer($server, $requestFactory, $logger);

$serverRequest = \GuzzleHttp\Psr7\ServerRequest::fromGlobals();

\LunixREST\HTTPServer::dumpResponse($httpServer->handleRequest($serverRequest, new GuzzleHttp\Psr7\Response()));

Looks pretty simple, except for the obvious missing variable definitions of $server, $requestFactory, and $logger. An HTTPServer requires these to be constructed, so lets build them one at a time as an example.

Server/GenericServer

A Server's jobs is to take in a APIRequest generated by the HTTPServer, an return an APIResponse (or throw an exception, if applicable to the request)

But Server is an interface, so we'll need a specific implementation to use. In this example we'll be using a GenericServer. GenericServer is an implementation that is meant to derive as much behaviour as possible from other classes.

$server = new GenericServer($accessControl, $throttle, $responseFactory, $router);

Again, simple, but we're missing some definitions, so lets break those down one-by-one.

AccessControl/PublicAccessControl

A GenericServer requires an AccessControl instance to handle controlling access.

For example, a PublicAccessControl takes in a request and says that it is allowed, without checking it at all. As the name implies, it's for a public API, and ignores the key entirely.

$accessControl = new \LunixREST\Server\AccessControl\PublicAccessControl();

Throttle/NoThrottle

A GenericServer also requires a Throttle instance to handling throttling requests if needed.

For example, a NoThrottle just returns that a given request doesn't need to be throttled, ever. Less applicable to real API implementations, beyond smaller ones. Actual implementations of Throttle will be able to be found in LunixREST-Basics.

$throttle = new \LunixREST\Server\Throttle\NoThrottle();

ResponseFactory/RegisteredResponseFactory

Another thing that a GenericServer requires is an instance of a ResponseFactory, which it uses to form the APIResponseData into an APIResponse. A key feature to this is transforming the data in an APIResponseData object and converting it into an PSR-7 StreamInterface.

For example, a RegisteredResponseFactory takes in a list of APIResponseDataSerializers and associates them with a specific MIME type.

$responseFactory = new \LunixREST\Server\ResponseFactory\RegisteredResponseFactory([
    'application/json' => new \LunixRESTBasics\APIResponse\JSONResponseDataSerializer()
]);

You'll notice that the JSONResponseDataSerializer is in the LunixREST-Basics project. This is because it requires a specific PSR-7 implementation (it uses Guzzle's). No actual implementations of APIResponseDataSerializer are included in Core because of this reason.

Router/GenericRouter

The final thing that a GenericServer needs to function is a Router. A Router takes a request and decides which Endpoint and method on that Endpoint to call. It then proceeds to call the endpoint and return the result.

For this example, we'll be using a GenericRouter, which defines some basic behaviour, but passes most of the details off to an EndpointFactory which is used to actually find an Endpoint.

$router = new \LunixREST\Server\Router\GenericRouter($endpointFactory);
EndpointFactory/SingleEndpointFactory

An EndpointFactory builds an endpoint from the requested endpoint name, and the APIRequest's parsed version.

The implementation we'll be using in this example is a SingleEndpointFactory from the Basics repo. We're also using a generic HelloWorld Endpoint, the code for which is provided after the example.

$endpointFactory = new \LunixRESTBasics\Endpoint\SingleEndpointFactory(new HelloWorld());
use LunixREST\Server\APIResponse\APIResponseData;
use LunixREST\Server\Router\Endpoint\DefaultEndpoint;
use LunixREST\Server\Router\Endpoint\Exceptions\UnsupportedMethodException;
use LunixREST\Server\APIRequest\APIRequest;

class HelloWorld extends DefaultEndpoint
{

    /**
     * @param APIRequest $request
     * @return APIResponseData
     * @throws UnsupportedMethodException
     */
    public function getAll(APIRequest $request): APIResponseData
    {
        return new APIResponseData([
            "helloworld" => "HelloWorld"
        ]);
    }
}

This is also the time where an API could take advantage of an implementation of an EndpointFactory that extends the included abstract classes LoggingEndpointFactory and CachingEndpointFactory. These implementations are written to allow the use of the logging and caching PSRs.

RequestFactory

Now that we have the Server taken care of, the next thing an HTTPServer needs to function is an instance of a RequestFactory.

The job of a RequestFactory is to take in the PSR-7 ServerRequestInterface, and to parse it into an APIRequest.

Generally you'll want to use the GenericRequestFactory and define you're own URLParser and HeaderParser. If you just want to use a pre-written header parser, you can use a DefaultRequestFactory that just needs a URLParser and uses the built in DefaultHeaderParser.

However, for this example we'll keep it a bit simpler and use another class from LunixREST-Basics: BasicRequestFactory, which extends the DefaultRequestFactory to use a BasicURLParser.

The BasicURLParser expects a request to look like this:

/VERSION/API_KEY/ENDPOINT[/OPTIONAL_ELEMENT].RESPONSE_TYPE_EXTENSION

Or to test our current example:

/1.0/public/helloworld.json

Finally, our example code:

$requestFactory = new \LunixRESTBasics\APIRequest\RequestFactory\BasicRequestFactory();

LoggerInterface

The final thing that an HTTPServer needs is an instance of a PSR LoggerInterface. This has been required here as the PSR offers an easy out for those who don't want to log: NullLogger. For actual logging, we recommend Monolog.

$logger = new \Psr\Log\NullLogger();

Full Example

We now have everything we need to define a basic API using LunixREST. Our API has the following features defined:

  • Public
  • Explicitly avoids throttling
  • Can write JSON responses
  • Handles all requests with a single endpoint (HelloWorld)
  • Avoids any PSR-7 request body middleware, so it only can handle HTTP urlencoded/form-data requests

The code for this looks like:

$accessControl = new \LunixREST\Server\AccessControl\PublicAccessControl();
$throttle = new \LunixREST\Server\Throttle\NoThrottle();

$responseFactory = new \LunixREST\Server\ResponseFactory\RegisteredResponseFactory([
    'application/json' => new \LunixRESTBasics\APIResponse\JSONResponseDataSerializer()
]);

$endpointFactory = new \LunixRESTBasics\Endpoint\SingleEndpointFactory(new \HelloWorld());

$router = new \LunixREST\Server\Router\GenericRouter($endpointFactory);

$server = new \LunixREST\Server\GenericServer($accessControl, $throttle, $responseFactory, $router);

$requestFactory = new \LunixRESTBasics\APIRequest\RequestFactory\BasicRequestFactory\BasicRequestFactory();

$logger = new \Psr\Log\NullLogger();

$httpServer = new \LunixREST\HTTPServer($server, $requestFactory, $logger);

$serverRequest = \GuzzleHttp\Psr7\ServerRequest::fromGlobals();

\LunixREST\HTTPServer::dumpResponse($httpServer->handleRequest($serverRequest, new \GuzzleHttp\Psr7\Response()));

As well as the code for our one Endpoint:

use LunixREST\Server\APIResponse\APIResponseData;
use LunixREST\Server\Router\Endpoint\DefaultEndpoint;
use LunixREST\Server\Router\Endpoint\Exceptions\UnsupportedMethodException;
use LunixREST\Server\APIRequest\APIRequest;

class HelloWorld extends DefaultEndpoint
{

    /**
     * @param APIRequest $request
     * @return APIResponseData
     * @throws UnsupportedMethodException
     */
    public function getAll(APIRequest $request): APIResponseData
    {
        return new APIResponseData([
            "helloworld" => "HelloWorld"
        ]);
    }
}