Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
310 lines (220 sloc) 8.85 KB

Practical Work #5

The aim of this practical work is to design a stronger Model Layer, by using database design patterns seen in class, but also by relying on a database engine provided as a Docker container.

In order to persist the data of the container, you have to create a data-only container, which is a special container that should never stop and store all the data for another container. A core concept of Docker is to provide containers that do not have any persistence, hence the concept of data-only containers:

$ docker run -d \
    --volume /var/lib/mysql \
    --name data_mysql \
    --entrypoint /bin/echo \
    busybox \
    "mysql data-only container"

Now, we can tell Docker to: (i) fetch the mysql image, (ii) run a Docker container, (iii) use the data container created previously, and (iv) initialize MySQL with a user/password and a database:

$ docker run -d -p 3306 \
    --name mysql \
    --volumes-from data_mysql \
    -e MYSQL_USER=uframework \
    -e MYSQL_PASS=p4ssw0rd \
    -e ON_CREATE_DB=uframework \
    tutum/mysql

Here, we chose to publish (-p option) the MySQL port (3306) but we did not specify any "local" port. Type docker ps to get the port.

On your machine (or hina), try to connect to the database:

$ mysql uframework -h127.0.0.1 -P<assigned port> -uuframework -p

If you want to stop the container, you can execute the following command:

$docker stop mysql # or its ID

To start the container again:

$docker start mysql # or its ID

Important: if you want to run the MySQL container again, you have to run the following command:

$ docker run -d -p 3306 \
    --volumes-from data_mysql \
    tutum/mysql

The Connection

Write a SQL script named app/config/schema.sql to create a statuses table. This script MUST be written in SQL that is also MySQL compliant. It should be repeatable, so be sure to add a check if the table already exists for instance.

Executing your SQL from the command line can be done that way:

$ mysql <database> -h<host> -P<port> -u<user> -p<password> < app/config/schema.sql

Be sure to replace <host>, <port>, <database>, <user>, and <password> with the appropriate values. There is no space between -h/-P/-u/-p and their corresponding values, this is not a mistake!

Udate your Model Layer to manipulate a database rather than a file. In order to do that, you have to:

  • use PDO;
  • use a Connection class as described in class;
  • use the Connection instance to execute queries.

You can create another class which implements the Finder interface, and replace the instance of JsonFinder in your code, by an instance of this new class. Choose an appropriate name for such a class.

The Connection should be instantiated in app/app.php only once. That way, the whole configuration part is located in a single place.

The find*() methods will not use the executeQuery() method, you will have to write your own logic to return the data (hint: FETCH_ASSOC can become your new friend).

You MAY need to update the templates.

Now, check everything still works.

The Model Class

Create a Status class which represents a single status if it is not done yet. Be sure to write clean code (ie. avoid setters).

For dates, use DateTime instances. Optional values are nullable in PHP:

class Foo
{
    /**
     * @var string
     */
    private $required;

    /**
     * @var string|null
     */
    private $optional;

    public function __construct($required, $optional = null)
    {
        $this->required = $required;
        $this->optional = $optional;
    }
}

Use the class you have created in your project.

The Data Mapper

Create a class that implements the Data Mapper design pattern. This class is responsible for persisting and removing your statuses.

Useful: the Y-m-d H:i:s format is the one used by MySQL for its DATETIME type.

Now you should have a cleaner Model Layer containing:

  • a DAL thanks to the Connection class;
  • a Data Mapper to persist/remove Plain Old PHP Objects (POPO);
  • a Finder to perform queries and retrieve sets of POPOs.

Filtering The Results

At the moment, your API returns collections of resources. But, what if you want to return the last 5 statuses? Or the statuses between two dates?

First of all, you need to understand how a client will perform a request for such a use case:

GET /statuses?limit=5&orderBy=createdAt

This literally means:

Give me the last 5 statuses

If you decompose the URI, the client adds two parameters to the query string: limit and orderBy.

  • limit stands for "number of results to return";
  • orderBy is a criterion.

This criterion will be passed to the Model Layer, in order to fetch the results the right way. In this example, the results in the database will be ordered by date.

The SQL query will look like:

SELECT * FROM statuses
ORDER BY created_at
LIMIT 0, 5

Your job is to modify your Finder to allow the use of criteria.

Your find*() methods need a $criteria parameter. This parameter will modify the SQL queries by adding SQL clauses like LIMIT, ORDER BY, WHERE, and so on.

You can write tests to prove everything works fine.

Now, update your controllers to allow filtering the results. It only makes sense on collections. You should be able to:

  1. limit the number of results;
  2. order results by a given column and a given direction (ASC or DESC).

Testing

Software testing is a requirement for all developers. Your job is not to write code, it is all about providing solutions for given problems. The right solution is always the one that works.

The question is: how do you ensure that your solution works? You cannot rely on the well-known it works on my machine™. This is just silly. What you need to do is to provide a set of automated tests.

Tests are more than just assertions on some properties of your application, it is also the best documentation you can provide to your customers/users because tests are always up to date.

For all these reasons, you have to learn how to write tests.

Unit Tests

Unit testing is ll about testing some parts of your application in a white box approach. The main idea is to test algorithms, methods, functions one by one. That does not mean you have to test your private methods, because these methods should be used by your public methods anyway. You have to focus on behaviours, so you don't need to test getters/setters or logic less methods.

In his last talk, Volker Dush, a PHPUnit contributor, provides a good sample:

<?php

public function setValue($value)
{
    $this->value = $value;
}

public function execute()
{
    if (!$this->value) {
        throw new Exception('No Value, no good');
    }

    return $value * 10; // business logic
}

In the code above, what should you test? The answer is:

  • if you don't call setValue() calling execute() will throw an exception;
  • if you do call setValue() calling execute() will return the computed result.

So we are testing two behaviors of the class and not the methods in isolation! This is really important!

The PHP unit testing framework in PHP is named PHPUnit. Look at the tests/ directory in µFramework.

In this part, your job is to test your Model Layer. For instance, the DataMapper class requires a Connection instance to work. You will not provide a real Connection instance here, because you don't want to use a database in order to run your tests.

Unit tests should not contain hard dependencies to external services. In order to avoid that, we often rely on mocks. A mock is an implementation that you control in your tests.

Creating a mock is relatively easy. PHPUnit has its own mocking framework: http://www.phpunit.de/manual/3.0/en/mock-objects.html.

Mock the Connection to return the executed SQL queries, and compare them to queries you expect to execute.

This can be useful to ensure your persist() method distinguishes the state of the object. Note that PDO cannot be mocked out of the box. You have to extend it, and avoid to call its constructor:

<?php

class MockConnection extends Connection
{
    public function __construct()
    {
    }
}

Now, you can mock your Connection:

<?php

$this->getMock('MockConnection');

 Functional Tests

Next week!


You can jump to: Practical Work #6.