Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
247 lines (171 sloc) 6.97 KB

Practical Work #4

The goal of this practical work is to implement a simple content negotiation layer in your µFramework, and to make your application a bit more REST compliant.

1 - Meet Composer

You will now use Composer to manage your project, and its dependencies.

In your project, create a composer.json file with the following content:

{
    "require": {
    },
    "autoload": {
        "psr-4": { "": [ "src/" ] }
    }
}

Now, run:

$ composer install

Replace Your Autoloader With Composer's One

When you run composer install, it "installs" project's dependencies in a vendor/ directory but also generates an autoloader file in vendor/autoload.php. That one is optimized, and probably better than yours. Moreover, it's automatically generated, and you don't need to waste your time on that part.

In app/app.php, replace the autoloader with vendor/autoload.php.

You can safely delete the autoload.php file you wrote in the previous practical work.

Fixing The Test Suite

If you run the test suite, it should fail because the autoloader setup has changed.

Edit the tests/boostrap.php file, by replacing its content with:

$loader = require __DIR__ . '/../vendor/autoload.php';
$loader->add('', __DIR__);

Be sure to understand what it does.

2 - Content Negotiation

Content Negotiation is part of the HTTP protocol, used to serve a resource in the best format for a client. You can read RFC 2616: HTTP/1.1 and RFC 2295: content negotiation if you want more information.

Content negotiation means that a single resource can be presented in different ways, depending on the client preferences. To achieve this goal, HTTP headers (Accept, Accept-*) are used by a client to tell the server about its preferences.

Your application should be able to serve resources in:

  • HTML using a template;
  • JSON encoded data.

The HTML part has been done in the previous practical work.

The JSON part is what you have to do in this practical.

Your implementation will also accept parameters encoded in:

  • application/x-www-form-urlencoded - used when you submit a HTML form;
  • application/json.

Guessing The Best Format To Return

It's Request's responsibility to resolve the best format to serve, so you need a guessBestFormat() method in this class.

Negotiation will help you get the best format from headers by handling content negotiation. The Accept header could be found in $_SERVER['HTTP_ACCEPT'].

Create the guessBestFormat() method as specified. Rely on the Negotiation library if you think it's worth using it ;-)

Decode Parameters Based On the Content Type

When a request has a body, it should provide a Content-Type. This content type header is available in either $_SERVER['HTTP_CONTENT_TYPE'] or $_SERVER['CONTENT_TYPE']. You have to check both variables, and order matters.

Modify the createFromGlobals() method to convert a JSON content (application/json) into parameters, and use them as request parameters.

This snippet can be useful:

$data    = file_get_contents('php://input');
$request = @json_decode($data, true);

3 - The Response

To fit the HTTP protocol, every response should contain at least:

  • a Content-Type header to describe the content;
  • a status code to give feedback on what happened;
  • the content (or body) of the response.

That makes a lot of things to handle, it cannot all fit into the App class.

A Response class will be in charge of the response configuration:

<?php

namespace Http;

class Response
{
    private $content;

    private $statusCode;

    private $headers;

    public function __construct($content, $statusCode = 200, array $headers = [])
    {
        $this->content    = $content;
        $this->statusCode = $statusCode;
        $this->headers    = array_merge([ 'Content-Type' => 'text/html' ], $headers);
    }

    public function getStatusCode()
    {
        return $this->statusCode;
    }

    public function getContent()
    {
        return $this->content;
    }

    public function sendHeaders()
    {
        http_response_code($this->statusCode);

        foreach ($this->headers as $name => $value) {
            header(sprintf('%s: %s', $name, $value));
        }
    }

    public function send()
    {
        $this->sendHeaders();

        echo $this->content;
    }
}

Update the process() method in the App class to use this new class.

Important: it should be possible to return either a string as you used to do or directly a Response object.

4 - Formatting Your Data The Right Way

HTML rendering is achieved through your template engine. Rendering your data in other formats is called serialization. Serialization is the process of converting a data structure or object state into a format that can be stored.

The serialization is handled by a Serializer. This serializer can be as simple as the json_encode() function or you can use the Serializer Component.

Use the new methods created in the Request class, and return a Response with the right content/headers, in each controller's function. You can rely on the Serializer component, but using json_encode() is easier.

You can use an anonymous function to factorize some code, or extend the Response class. Just saying...

Important: always set the right status code to the response. It has been described in the previous practical work.

Testing

In a terminal, try these commands:

$ curl -XGET -H "Accept: application/json" http://localhost:8082/statuses
$ curl -XGET -H "Accept: application/json" http://localhost:8082/statuses/1
$ curl -XGET -H "Accept: application/json" http://localhost:8082/statuses/1000

Also, try to create new statuses using JSON:

$ curl -XPOST -H "Accept: application/json" -H 'Content-Type: application/json' \
    -d '{"message": "Hello", "username": "..."}' \
    http://localhost:8082/statuses

Using Goutte or Buzz HTTP clients, you can create a simple script executing scenarios to test your application.

If you feel like using PHPUnit to implement your tests, go ahead, it will be useful for your whole PHP developer life!

Fixing the Exception handler

As of PHP 7, all exception handlers have to handle Throwable instances. Open the src/Exception/ExceptionHandler.php file, and modify it:

    /**
-    * @param Exception $exception
+    * @param Throwable $exception
     */
-    public function handle(\Exception $exception)
+    public function handle(\Throwable $exception)
     {

You can jump to: Practical Work #5.