PSX Framework

About

PSX is a framework written in PHP to create RESTful APIs. It focuses on providing all tools you need to build a clean and extensible API. PSX does not restrict you to any libraries instead you can use the tools you like. By default it uses well known opensource libraries like Monolog, Doctrine DBAL and the Symfony Event Dispatcher. The following chapter shows some features of PSX which should give you an first impression about PSX. Talk is cheap show me the code!

Schema definition

PSX gives you the possibility to define an clear schema for your request and response data. It is possible to parse and validate data based on the defined schema. The schema can also be used to generate API documentation or other schema formats like XSD or JsonSchema. In the example we define an schema which would match the given JSON request.

Also you can use the WSDL generator to generate a fully Java and .NET compatible WSDL definition for your API endpoint. In this way consumers can access your REST API endpoint per SOAP.

PHP

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

        return $sb->getProperty();
    }
}

JSON

{
    "title": "acme news",
    "author": {
        "name": "foo"
    },
    "content": "lorem ipsum"
}

Request parsing

PHP

class Controller extends ControllerAbstract
{
    /**
     * @Inject
     * @var PSX\Data\Schema\SchemaManager
     */
    protected $schemaManager;

    public function doInsert()
    {
        $schema = $this->schemaManager->getSchema('Acme\NewsSchema');
        $news   = $this->import($schema);

        // @TODO do something with the news
        // $news->getTitle();
        // $news->getAuthor()->getName();
        // $news->getContent();
    }
}

Based on the defined schema it is possible to parse incoming request data into an object graph. The returned object has the same structure as the defined schema.

In the example we import the request data based on the schema definition which also validates the data. PSX tries to read the incoming request based on the Content-Type header.

API testing

Because PSX is build around an HTTP (PSR-7) 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 from 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 FooController extends PSX\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 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
    }
}

Response generation

PHP

class Index extends ControllerAbstract
{
    public function doIndex()
    {
        $atom = new Atom();
        $atom->setTitle('lorem ipsum');

        // @TODO add more atom entries
        $entry = new Atom\Entry();
        $entry->setTitle('foobar');

        $atom->add($entry);

        $this->setBody($atom);
    }
}

PSX offers built in data models to generate Atom, RSS and ActivityStream responses. In the example we generate an simple Atom feed.

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 the most polished and 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 FooController 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 array
        $body = $this->getBody():
     
        // set an response code
        $this->setResponseCode(200);
     
        // write an response
        $this->setBody('foobar');
    }
}

Command

PHP

class FooCommand 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.