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.
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
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.
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.
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
.
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 ;-)
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);
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.
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.
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!
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.