PSX Framework

About

Welcome, PSX is a framework written in PHP to create RESTful APIs. It provides tools to handle routing, API versioning, data transformation, authentication, documentation and testing. With PSX you can easily build an REST API around an existing application or create a new one from scratch. Take a look at the example to see an API in action. The following chapter shows some features of PSX which should give you an first impression how it works. Talk is cheap show me the code!

Documentation

PSX provides tools to automatically generate a documentation from the defined API. The documentation gets generated from the schema and is therefore always up to date. To see the documentation you can view the example project or take a look at the source.

Schema definition

PSX gives you the possibility to define an clear schema for your API endpoint. The schema can be defined as RAML specification or directly in PHP. Because PSX has all informations how the API is structured it can validate incoming and outgoing data according to the schema.

Based on the defined schema PSX can also generate different API definition formats: RAML, Swagger and WSDL. These formats can be used i.e. to automatically generate client-side code.

PHP

class Endpoint extends SchemaApiAbstract
{
    public function getDocumentation()
    {
        return new Raml::fromFile(
            'endpoint.raml', 
            $this->context->get(Context::KEY_PATH)
        );
    }
}

RAML

#%RAML 0.8
title: Acme api
baseUri: http://api.acme.com
version: v1
/population:
  description: Returns a collection of population entries
  get:
    queryParameters:
      startIndex:
        type: integer
      count:
        type: integer
    responses:
      200:
        body:
          application/json:
            schema: !include schema/collection.json
  post:
    body:
      application/json:
        schema: !include schema/entry.json

PHP

class Endpoint extends SchemaApiAbstract
{
    public function getDocumentation()
    {
        $resource = new Resource(
            Resource::STATUS_ACTIVE, 
            $this->context->get(Context::KEY_PATH)
        );

        $resource->setTitle('Acme api');
        $resource->setDescription('Returns a collection of population entries');

        $resource->addMethod(Resource\Factory::getMethod('GET')
            ->addQueryParameter(Property::getInteger('startIndex'))
            ->addQueryParameter(Property::getInteger('count'))
            ->addResponse(
                200, 
                $this->schemaManager->getSchema('Acme\Schema\Collection')
            )
        );

        $resource->addMethod(Resource\Factory::getMethod('POST')
            ->setRequest($this->schemaManager->getSchema('Acme\Schema\Entry'))
        );

        return new Documentation\Simple($resource);
    }
}

Request parsing

Controller

class Endpoint extends SchemaApiAbstract
{
    protected function doCreate(RecordInterface $record, Version $version);
    {
        // @TODO do something with the news
        $record->getTitle();
        $record->getAuthor()->getName();
        $record->getContent();

        return array(
            'success' => true,
            'message' => 'Record successful created',
        );
    }
}

JsonSchema

{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "id": "http://example.phpsx.org#",
    "type": "object",
    "properties": {
        "title": {
            "type": "string",
            "description": "Title description",
            "pattern": "[A-z]{3,16}"
        },
        "author": {
            "$ref": "file:///author.json#"
        },
        "content": {
            "type": "string",
            "minLength": 3,
            "maxLength": 512
        }
    },
    "required": ["content"]
}

PHP

class News extends SchemaAbstract
{
    public function getDefinition()
    {
        $sb = $this->getSchemaBuilder('news');
        $sb->string('title')
            ->setDescription('Title description')
            ->setPattern('[A-z]{3,16}');
        $sb->complexType($this->getSchema('Acme\Schema\Author'));
        $sb->string('content')
            ->setDescription('Content description')
            ->setMinLength(3)
            ->setMaxLength(512)
            ->setRequired(true);

        return $sb->getProperty();
    }
}

PSX parses the incoming request into an object graph according to the defined data model. The data model can be defined either as JsonSchema or directly in PHP.

In the example we see an API controller which consumes the request data of an POST request. In the tabs you can see the used schema as JsonSchema or defined in PHP.

Versioning

PSX supports different API versioning methods. You can use simple url versioning by providing a route for each version or you can use an custom Accept header. The version which you provide in the Accept header gets passed to the API controller as version object.

Accept header versioning is the preferred method in PSX because it is more RESTful. This is because you have for each resource only one endpoint but can request different representations of the resource by providing the Accept header.

Url versioning

GET /v1/news HTTP/1.1

Accept-Header versioning

GET /news HTTP/1.1
Accept: application/vnd.acme.v1+json

Response

{
    "title": "foo"
}

Url versioning

GET /v2/news HTTP/1.1

Accept-Header versioning

GET /news HTTP/1.1
Accept: application/vnd.acme.v2+json

Response

{
    "displayName": "foo"
}

Response generation

Controller

class Endpoint extends SchemaApiAbstract
{
    protected function doGet(Version $version);
    {
        return [
            'totalResults' => 2,
            'entry' => [[
                'title' => 'Acme news one',
                'content' => 'lorem ipsum'
            ],[
                'title' => 'Acme news two',
                'content' => 'lorem ipsum'
            ]],
        ];
    }
}

Json

{
    "totalResults": 2,
    "entry": [
        {
            "title": "Acme news one",
            "content": "lorem ipsum"
        },
        {
            "title": "Acme news two",
            "content": "lorem ipsum"
        }
    ]
}

Xml

<?xml version="1.0" encoding="UTF-8"?>
<record>
 <totalResults>2</totalResults>
 <entry>
  <title>Acme news one</title>
  <content>lorem ipsum</content>
 </entry>
 <entry>
  <title>Acme news two</title>
  <content>lorem ipsum</content>
 </entry>
</record>

Atom

<?xml version="1.0" encoding="UTF-8"?>
<entry xmlns="http://www.w3.org/2005/Atom">
 <content type="application/xml">
  <record>
   <totalResults>2</totalResults>
   <entry>
    <title>Acme news one</title>
    <content>lorem ipsum</content>
   </entry>
   <entry>
    <title>Acme news two</title>
    <content>lorem ipsum</content>
   </entry>
  </record>
 </content>
</entry>

SOAP

<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
 <soap:Body>
  <getResponse xmlns="http://phpsx.org/2014/data">
   <totalResults>2</totalResults>
   <entry>
    <title>Acme news one</title>
    <content>lorem ipsum</content>
   </entry>
   <entry>
    <title>Acme news two</title>
    <content>lorem ipsum</content>
   </entry>
  </getResponse>
 </soap:Body>
</soap:Envelope>

Jsonx

<?xml version="1.0" encoding="UTF-8"?>
<json:object xmlns:json="http://www.ibm.com/xmlns/prod/2009/jsonx">
 <json:number name="totalResults">2</json:number>
 <json:array name="entry">
  <json:object>
   <json:string name="title">Acme news one</json:string>
   <json:string name="content">lorem ipsum</json:string>
  </json:object>
  <json:object>
   <json:string name="title">Acme news two</json:string>
   <json:string name="content">lorem ipsum</json:string>
  </json:object>
 </json:array>
</json:object>

If you produce an response PSX analyzes the data structure and uses an data writer to generate an response. By default PSX comes with multiple data writers which can generate different data formats like i.e. Json or XML. The writer which gets used depends on the Accept header field or the GET parameter format. In the example you can see an controller and the corresponding responses from different data writers.

PSX offers also built in data models to generate Atom, RSS and ActivityStream responses.

API testing

Because PSX is build around an HTTP request and response object we can easily test our API code. We dont need to start an webserver or mock the request we can simply call our controller from the test. Internally this is the same code as when we make an call to an webserver except that we manually create the HTTP request and response.

In this way we can easily make an integration test for an controller by looking at the actually response. In our example we call the doIndex method of the controller and check whether the response is an JSON object {"hello": "world"}.

PHP

class HelloWorldApiTest extends ControllerTestCase
{
    public function testHelloWorld()
    {
        // create http request
        $request  = new GetRequest('http://localhost.com/foo');
        $request->addHeader('Accept', 'application/json');

        // create http response
        $response = new Response();
        $body     = new TempStream(fopen('php://memory', 'r+'));
        $response->setBody($body);

        // send request the response gets written to the body
        $controller = $this->loadController($request, $response);
        $data       = json_decode((string) $body);

        $this->assertArrayHasKey('hello', $data);
        $this->assertEquals($data['hello'], 'world');
    }

    protected function getPaths()
    {
        return array(
            '/foo' => 'Acme\TestViewController::doIndex',
        );
    }
}

Dependency managment

PHP

class Controller extends ControllerAbstract
{
    /**
     * @Inject
     * @var Doctrine\DBAL\Connection
     */
    protected $connection;
}

PHP

class DefaultContainer extends Container
{
    /**
     * @return Doctrine\DBAL\Connection
     */
    public function getConnection()
    {
        $config = new Configuration();
        $params = array(
            'dbname'   => $this->get('config')->get('psx_sql_db'),
            'user'     => $this->get('config')->get('psx_sql_user'),
            'password' => $this->get('config')->get('psx_sql_pw'),
            'host'     => $this->get('config')->get('psx_sql_host'),
            'driver'   => 'pdo_mysql',
        );

        return DriverManager::getConnection($params, $config);
    }
}

PSX comes with an fast DI container which implements the Symfony DI container interface. Instead of configuration files it uses simple traits where each method is an service definition.

Inside an controller/command it is not possible to access the DI container instead each dependency must be specified as property with an @Inject annotation. PSX injects the dependency into this property. This gives an clear overview of the dependencies for each controller/command which eventually should help you to decouple your application code from the framework.

Routing

PSX uses a simple routing file which was inspired by the Java Play-Framework. We can specify the allowed request methods, the path and the controller which should be called. In the example controller we access the dynamic part of the path.

Routing

GET      /news             Acme\News\Application\Index::doIndex
GET      /news/:news_id    Acme\News\Application\Index::doDetail
GET      /bar/$foo<[0-9]+> Acme\News\Application\Article
GET      /download/*file   Acme\News\Application\Download
GET|POST /bar              Acme\News\Application\BarApi

PHP

class Index extends ControllerAbstract
{
    public function doDetail()
    {
        $newsId = $this->getUriFragment('news_id');

        // @TODO work with the news id
    }
}

Middleware oriented

PHP

class AcmeController extends ControllerAbstract
{
    public function getApplicationStack()
    {
        return [function($request, $response, $filterChain){

            $response->getBody()->write('Hello world!');

            $filterChain->handle($request, $response);

        }];
    }
}

PSX controllers are designed as middleware. In the end each controller returns only an array of middlewares which get executed. By default an controller returns the ControllerExecutor middleware which executes the specific methods of the controller. In the example we return an simple closure as middleware which writes the string Hello world! as response.

Controller

PSX provides an simple controller class which gives you the possibility to read from the request and write to the response. Because the controller is the connection between your application and the framework PSX tries to provide an stable API for your application. Inside the controller you can specify your dependecies via the @Inject annotation. The example shows some important methods inside an controller.

PHP

class AcmeController extends ControllerAbstract
{
    public function doIndex()
    {
        // get request method i.e. POST
        $requestMethod = $this->getMethod();
     
        // get Content-Type header
        $contentType = $this->getHeader('Content-Type');
     
        // get GET parameter foo
        $requestMethod = $this->getParameter('foo');
     
        // if its an XML Content-Type returns an DOMDocument. On
        // an JSON or x-www-form-urlencoded Content-Type an stdClass
        $body = $this->getBody():
     
        // set an response code
        $this->setResponseCode(200);
     
        // write an response
        $this->setBody('foobar');
    }
}

Command

PHP

class AcmeCommand extends CommandAbstract
{
    /**
     * @Inject
     * @var Doctrine\DBAL\Connection
     */
    protected $connection;

    public function onExecute(Parameters $parameters, OutputInterface $output)
    {
        $this->connection->insert('acme_news', array(
            'title' => $parameters->get('title')
        ));

        $output->writeln('Inserted a news');
    }

    public function getParameters()
    {
        return $this->getParameterBuilder()
            ->setDescription('Inserts an news entry')
            ->addOption('title', Parameter::TYPE_REQUIRED, 'The title of the news')
            ->getParameters();
    }
}

PSX offers a command system which helps to encapsulate code into micro services. An command is like an controller but without the request/response context. Each command can specify parameters which are needed to complete the task. Like in an controller you can specify your dependecies via the @Inject annotation.

The idea is that an command can be executed from any context i.e. within an Controller/Command, CLI, message queue or any other environment. Although it is possible to call an command from CLI you can not ask for user input inside an command. All needed values must be provided as parameters. This ensures that we always can call an command from other environments. The example shows an command which inserts an news entry.