Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
461 lines (325 sloc) 13.4 KB

Practical Work #3

Some parts of your micro-framework are missing. You have to complete the missing parts. Then, you have to improve your framework, and build a real application.

Today's goal is to create a simple PHP application to Create, Retrieve, Update, and Delete tweets. This is called a CRUD application. You will not implement the Update part though.

Insight

You are going to build the next awesome social network. The idea is to create an application a la Twitter, but with a truly RESTful API.

Twitter is about sending short messages (140 chars) over the Internet, and let people know what you are doing, thinking, or even eating... Sometimes, people write useful messages.

Your application will deal with statuses. Users can create, or delete statuses, which are the so called tweets. It is not possible to update a status. A status MUST contain at most 140 characters. Anyone can read statuses from others.

A status contains a message, and is identified by an identifier, probably a numeric id. Each status owns a date. Also, the name of the user who creates a status is embedded into this status. It is also possible to specify the client that has been used to send a status (iOS, Android, or web browser for instance), but it is optional.

1 - Think!

This application will respect REST conventions, that means, at least, the use of HTTP verbs, and consistent URIs. All routes will be declared in app/app.php as follows:

// Create
$app->post('...', $callable);
// Read
$app->get('...', $callable);
// Update
$app->put('...', $callable);
// Delete
$app->delete('...', $callable);

$callable can be either a closure, an array or a function name. More on this in the PHP manual.

It is recommended to use closures:

function ($params) use ($something) {
    // controller code
}

Your first job is to take a piece of paper, a pen, and to write your world's domination plan. Well, at least write the API methods you need in a REST-style:

GET /something
POST /...
...

The important thing here is to determine what you want to achieve before writing your application. The use of a sheet of paper is mandatory! Your teacher MUST validate your plan.

2 - The "Read" Part

Improve µFramework's Kernel

The applications's kernel is defined in src/App.php, and has been altered. Complete the registerRoute() method.

Then, in the App class, add the put(), post(), and delete() methods.

Check everything is ok by running the test suite.

The Model Layer

Create a InMemoryFinder class in src/Model. This class MUST implement the Model\Finder interface.

"In memory" means that the storage is a simple PHP array. Add fake data in this array. This allows you to quickly build your application, without taking care of the database. It is a Code-First approach.

Your First Routes

First, try to list all statuses.

Then, write a new controller to list a specific status, identified by an id parameter. You should write two templates, one for the list view, another one for the detail view.

get(), post(), put(), and delete() methods all take a regular expression as first argument. It defines the pattern for each URI you want to implement.

Assuming the following definition:

$app->get('/foo/(\d+)', function ($id) use ($app) {
});

It will match all URIs starting with /foo/ and finishing with a number (/foo/0, /foo/1, /foo/123, etc.).

You can declare more parameters if you want:

$app->get('/foo/(\d+)/bar/(\d+)', function ($fooId, $barId) use ($app) {
});

(\d+) is a regular expression that matches numbers only, but you can match whatever you want. Note that using numbers as identifiers is a good idea.

This number (0, 1 or 123 in the examples) will be injected as argument of the closure, that's why we declare a $id parameter. Use this parameter to retrieve the corresponding status.

If you can't find a status for a given id, then you should throw a HttpException with status code set to 404 which stands for Not Found. Look at the HttpException class, the first argument is the status code, and the second one is a message.

Check everything works.

A Basic Model Persistence

Statuses will now be stored in data/statuses.json. Create a JsonFinder class that implements your Finder interface. You will not rely on a PHP array anymore. Instead, use a file to store data, so that you can persist new statuses between two requests.

To manipulate JSON, SPL defines both json_encode() and json_decode() methods. Use the PHP manual to know how to use these methods in order to create a persistent model layer.

3 - The Request

You should know that a web application is about converting a request into a response. A client sends a request to a server, and the server returns a response.

In µFramework, there is no Request class, and it is quite bad. That is why you will create it in src/Http/Request.php:

<?php

namespace Http;

class Request
{
}

Using The Request In µFramework

  1. Modify the method run() in the App class to take an optional Request object as parameter.

  2. Move the constants (GET, POST, etc.) from the App class to the Request class, and updates the App class to use them.

  3. Create the first two new methods in the Request class:

  • getMethod(): returns the method (aka HTTP verb);
  • getUri(): returns the URI.

Look at the code of the run() method in the App class to know how to implement these methods.

The URI is what you described in previous practical. Add the following code to the getUri() method:

if ($pos = strpos($uri, '?')) {
    $uri = substr($uri, 0, $pos);
}

It will remove the query string from the REQUEST_URI value:

/statuses?foo=bar

What we want here is /statuses. The query string starts after the ? sign: foo=bar.

  1. Use these methods in the run() method of the App class:
public function run(Request $request = null)
{
    $method = $request->getMethod();
    $uri    = $request->getUri();

    // ...
}
  1. Implement a static method in the Request class to create a new Request instance. This method MUST be named createFromGlobals().

Returning an instance of the current class can be achieved using the following code:

return new self();

This method will contain more logic later, but by now, it should just return a new instance.

  1. Update the run() method to create a Request object if the argument is null:
if (null === $request) {
    $request = Request::createFromGlobals();
}

Using The Request In Your Application's Controllers

At the moment, your Request class does not do a lot of work. You only moved some code into it. Actually, it is better for something we call Separation of Concerns: it is Request's responsibility to determine the method used by the client.

You know that a web application is about converting a request into a response. In a Model View Controller architecture, this is handled by the Controller Layer. It seems like a good idea to inject the Request in your controllers:

$app->get('/statuses', function (Request $request) use ($app) {
});

You may wonder why we do not pass the $request instance in the closure's context. That is a good question, thank you for asking! You can consider the closure's context as a shared or global context, and the request is not shared. It is always different.

In order to inject the Request in your controllers, you will have to modify the App class. First, you have to pass the $request instance from the run() method to the process() method.

Then, update the code of the process() method to add the $request as first argument:

$arguments = $route->getArguments();
array_unshift($arguments, $request);

$response = call_user_func_array($route->getCallable(), $arguments);

Now, it will automatically inject a Request instance as first argument in your closures.

Reminder: The Controller Layer is located in the app/app.php file. A specific controller is represented by a closure in the application.

Abstracting Global Variables

In PHP, you can access data via $_GET and $_POST. You could use them, but now that you have a Request class, you should rely on it.

Note: Avoid the use of global variables in your code, as much as you can. Use classes that can abstract these variables. That's why you have a Request class.

Add a constructor method to your Request class:

__construct(array $query = array(), array $request = array())

$query is an array of GET parameters ($_GET). We use the term query as these parameters are part of the Query String.

$request is an array of POST parameters ($_POST). We use the term request as these parameters are part of the Request Body.

Declare a new attribute named $parameters in your Request class, and initialize it in the constructor by merging both $query and $request.

Implement a new method:

public function getParameter($name, $default = null)
{
}

Modify the createFromGlobals() method to inject the global variables.

Fixing The Browser

You know that a RESTful application leverages the HTTP verbs. Unfortunately, web browsers only support GET and POST. In order to by pass this limitation, we will use a hack.

The hack consists in using a hidden parameter in the request that defines the real HTTP verb the client wants to use. In a form, you will set the POST method, and use a hidden field named _method with the real HTTP verb:

<form action="..." method="POST">
    <input type="hidden" name="_method" value="PUT">
</form>

Modify the getMethod() method in your Request class to use this hack. This snippet could be useful:

if (self::POST === $method) {
    return $this->getParameter('_method', $method);
}

4 - The "Write" Part

Creating New Resources

In a RESTful application, we use the POST HTTP verb to create new resources, and we "post" data to the collection. It is also possible to "put" a new resource at a given URI, but you won't have to do that.

In your application, you will have to "post" data to /statuses. It will call the corresponding action registered in your application:

$app->post('/statuses', function () use ($app) {
    // this is the corresponding action
});

Note: Generally speaking, the term action represents a method in a controller. As you don't use a controller class, but a closure, the term action represents this closure.

In a browser, you can call this action by using a form. The following code could be added to the app/templates/statuses.php template:

<form action="/statuses" method="POST">
    <label for="username">Username:</label>
    <input type="text" name="username">

    <label for="message">Message:</label>
    <textarea name="message"></textarea>

    <input type="submit" value="Tweet!">
</form>

The user data will be accessible through the Request object.

$request->getParameter('foo');

Once you created a new status, redirect the user to the list. It should be done for two reasons:

  • if the user reloads the page, it won't create a new status again;
  • it's better for the user experience.

Redirecting the user requires a new method in the App class:

public function redirect($to, $statusCode = 302)
{
    http_response_code($statusCode);
    header(sprintf('Location: %s', $to));

    die;
}

Now you can redirect the user to the list view by using:

$app->redirect('/statuses');

Note: a REST API should return a 201 status code which stands for Created. It will be useful for the next practical. By now, you can live without that. It also adds a Location header with the URI of the resource that has just been created.

Deleting A Resource

In a RESTful application, we use the DELETE HTTP verb to delete resources, and we "delete" a resource.

In your application, you will have to "delete" a resource at /statuses/id. It will call the corresponding action registered in your application:

$app->delete('/statuses/(\d+)', function (Request $request, $id) use ($app) {
});

The first thing to do in this function is to retrieve your model object by its identifier. You can rely on the findOneById() method, part of the Finder interface.

If the object does not exist, you will get null. In this case, you will have to return a HttpException with status code 404 which stands for Not Found:

throw new HttpException(404, "Object doesn't exist");

If you get an object, then you can delete it.

As we saw in section Fixing The Browser, we can't use the DELETE keyword in your form, so we need to use a special parameter, using a hidden field, and the POST method in the form:

The following could be added to the app/templates/status.php template:

<form action="/statuses/<?= $id ?>" method="POST">
    <input type="hidden" name="_method" value="DELETE">
    <input type="submit" value="Delete">
</form>

Redirect the user to the list view.

Note: A REST API should return a 204 status code which stands for No Content.
You will need this information in the next practical.


You can jump to: Practical Work #4.