Chapter 5. Handling Text Content – Posting Text

In this chapter, we will start by adding features to the user wall. The user will now be able to post text on their wall as a status update, and will also be able to browse through the statuses, added previously. As usual, we will cover both the API and frontend sides. As we implement the first form, we will discover how to do the basic validation and filtering of the data prior to sending it to the API, and also how to validate that on the API side.

By the end of this chapter, you will learn how to create simple forms with a couple of fields, how to add validation to be sure that the user has provided the correct data, and how to filter it to be sure that the information we store is safe and doesn't contain any malicious code that can compromise the platform.

API development

Let's see the tasks we have to accomplish on the API level. We will update the get() method that returns the wall data to include all the statuses of the user, and we will also add a new method called create() to be able to insert new status into the database for a specific user. In order to do that we need to add a new table in the database and the corresponding table gateway. After that we'll add a new route for the create() method. Finally on the API side, we will see how to validate and filter the data that comes from the client.

Requirements

In order to post a new status, we will modify the endpoint we created in Chapter 4, The First Request/Response – Building the User Wall, to accept the POST requests. Let's see the formal requirements of the new method in the following table:

HTTP method

Controller method

Parameters

Functionality

GET

get()

getList()

id

This is the only method providing data to the world. The id parameter is mandatory and should contain the username. This method will return all the info related to the user wall.

As the id parameter is mandatory, getList() will return an HTTP error 405.

POST

create()

data

This is the method used to post content to the wall. The parameter is an array of data containing user_id and the parameters needed to create the content.

PUT

update()

id

This method is not allowed.

DELETE

delete()

id

This method is not allowed.

The create() method will inspect the contents of the $data parameter to see if we have the information needed to create the new status on the database. In the future, as we expand this method, we will examine the data to decide the type of content we should create.

Working with the database

As mentioned before, in order to be able to see the status updates of users, we will need to store them somewhere. Let's now create a new table on the database that will tie the status data to the user itself.

This table is called users_statuses and will hold the following data:

  • Id
  • User_id
  • Status
  • Created at
  • Updated at

The column, user_id, will be filled with id of the user located in the table, users.

Now that we have seen the data this table will store, let's see the actual statement to create it. We need to connect to the database server through the VM as we did in Chapter 4, The First Request/Response – Building the User Wall, to be able to execute the following SQL statement:

CREATE TABLE `user_statuses` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` int(11) unsigned DEFAULT NULL,
  `status` text,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Note

While creating tables on a database, you have to make a choice about using foreign keys and constraints or not. Depending on the size of the tables, how they are sharded, and the database architecture, using them can be problematic but at the end it is up to you. For simplicity, we will not use them in this book.

Understanding the module structure

As we are going to add new files to the structure we already have, let's see how the structure looks with the new files added to the project in the following screenshot:

Understanding the module structure

Creating UserStatusesTable.php

Now that we have the table created on the database, let's create the table gateway to be able to access it.

Create a new file called UserStatusesTable.php inside the Model folder of the Users module, and let's see what we have to add inside.

As with previous classes, the first thing we should do is declare the namespace for the class and the dependencies that we are going to use inside the class. In this case we have the following code:

namespace UsersModel;

use ZendDbAdapterAdapter;
use ZendDbTableGatewayAbstractTableGateway;
use ZendDbAdapterAdapterAwareInterface;
use ZendDbSqlExpression;
use ZendInputFilterInputFilter;
use ZendInputFilterFactory as InputFactory;

As you can see we have a lot of dependencies. You should be familiar with the first three dependencies as they were used in Chapter 4, The First Request/Response – Building the User Wall, when we created the UsersTable class. The next dependency is ZendDbSqlExpression. This class allows us to use SQL expressions, such as count(*) or NOW(). The last two dependencies will provide us with the ability to create filters that will check the input data, and will assure that they meet the requirements we have for each parameter.

The following block of code defines the class itself, the table we are using, and the setDbAdapter() method required by the AdapterAwareInterface interface:

class UserStatusesTable extends AbstractTableGateway implements AdapterAwareInterface
{
    const COMMENT_TYPE_ID = 1;

    protected $table = 'user_statuses';
    const TABLE_NAME = 'user_statuses';

    public function setDbAdapter(Adapter $adapter)
    {
        $this->adapter = $adapter;
        $this->initialize();
    }

As you can see this code is almost identical to the one we wrote for the UsersTable class. You might notice that we have defined two constants called COMMENT_TYPE_ID and TABLE_NAME. Ignore them for now, they will be handy when we arrive at Chapter 8, Dealing with Spam – Akismet to the Rescue.

The following section shows the getByUserId() method, takes care of retrieving the statuses of a user based on the user_id column, and orders them by the creation date stored in the created_at column:

public function getByUserId($userId)
{
    $select = $this->sql->select()
        ->where(array('user_id' => $userId))
        ->order('created_at DESC'),
    return $this->selectWith($select);
}

In this method, as we want to order the results, we use a Select object retrieved by $this->sql->select().We then customize it with the where() method and with the order() method. To execute a statement with a given Select object, we use the method, selectWith(), which accepts the Select object as a parameter.

The next method we are going to review is create() as shown in the following code:

public function create($userId, $status)
{
    return $this->insert(array(
        'user_id' => $userId,
        'status' => $status,
        'created_at' => new Expression('NOW()'),
        'updated_at' => null
    ));
}

As you can see, we are just building an array with the data we want to insert using the parameters passed in the method. To populate the created_at column, you have two options; the first one will be passing the date on the format that we use for that column on the database. The second option will be using the NOW() function from SQL to populate the value with the current date and time from the server. We will go with the second option, and in order to use it we should wrap the NOW() function inside an instance of ZendDbSqlExpression.

The last section of the class will be the method called getInputFilter(). Inside this method, we will configure the input filters needed for each field. For each input, we will define the filters and validators that apply to that specific value and will be used while validating the input sent by the clients.

Zend Framework 2 (ZF2) provides a huge amount of filters and validators. They cover the common needs for web applications, and usually you'll have enough with the provided ones. If not, as always, you have the option to create your own filters and validators by extending the appropriate classes and implementing the corresponding methods.

Filters and validators can be used in two different ways, attached to a form or standalone. But in both cases each field or value has its own configuration of filters and validators and each one has its own options.

As we are working on the API, we are going to see how to use them in the standalone approach. This is illustrated in the following code:

public function getInputFilter()
{
    $inputFilter = new InputFilter();
    $factory = new InputFactory();

The first thing we do is create an instance of the InputFilter component.

The InputFilter component can be used to filter and validate generic groups of data using associative arrays to provide the data. This component can hold multiple configurations of filters and validators, each one for a specific input or variable.

A new ZendInputFilterFactory instance is also created to be able to initialize the InputFilter objects from a configuration stored in an array.

Note

Notice that we created an alias, InputFactory, for this object when we declared the dependency.

The following code represents the new ZendInputFilterFactory dependency:

    $inputFilter->add($factory->createInput(array(
        'name'     => 'user_id',
        'required' => true,
        'filters'  => array(
            array('name' => 'StripTags'),
            array('name' => 'StringTrim'),
            array('name' => 'Int'),
        ),
        'validators' => array(
            array('name' => 'NotEmpty'),
            array('name' => 'Digits'),
            array(
                'name' => 'ZendValidatorDbRecordExists',
                'options' => array(
                    'table' => 'users',
                    'field' => 'id',
                    'adapter' => $this->adapter
                )
            )
        ),
    )));

The first block of code will configure the filters and validators for a field called user_id. As a filter we add StripTags, StringTrim, or Int. The first one will remove all the HTML tags present in the value, the second one will remove whitespaces before and after the value itself, and the last one will try to convert a scalar value to an integer.

Note

As a good practice you should always remove any HTML tags and sanitize the data that comes from the user, in order to minimize the chances of being vulnerable to a Cross-site scripting (XSS) attack. Just remember to never trust anything that comes from the user.

In order to validate the data for this field, we need to check the following points:

  • The field is not empty
  • The value should be a number
  • If we are adding the status of a user, the user must exist in the user's table

Take a look at the validators section of the configuration for the first input. The first two are pretty easy, just including the validator and the default configuration for them will do the job. For the third one, we will use a validator provided by ZF2 called ZendValidatorDbRecordExists. This validator checks if a row exists in the database based on a configuration. The configuration should define the table and column to be checked if the record exists. Finally, to be able to query the database, an adapter must be provided as shown in the following code:

    $inputFilter->add($factory->createInput(array(
        'name'     => 'status',
        'required' => true,
        'filters'  => array(
            array('name' => 'StripTags'),
            array('name' => 'StringTrim'),
        ),
        'validators' => array(
            array('name' => 'NotEmpty'),
            array(
                'name'    => 'StringLength',
                'options' => array(
                    'encoding' => 'UTF-8',
                    'min'      => 1,
                    'max'      => 65535,
                ),
            ),
        ),
    )));
        
    return $inputFilter;
}

Now let's take a look at the second input where we will try to filter and validate the data. The data will be the status of the user, this means text. The filter section will be pretty standard, and we will only remove HTML tags and whitespaces before and after the data. The validator's section will contain the NotEmpty validator, as before, to be sure that we have the data. At the end, it makes no sense to store an empty status. After that, we will validate the length of the text. As we defined the column on the table as TEXT type, this will imply the minimum and maximum size of the text. With the StringLength validator, we define that the status should have a minimum of one character and a maximum of 65,535 characters. Also, we define the encoding of the text.

Extending IndexController.php

As we saw in the beginning of the chapter, we will need to extend the IndexController.php file to add a new method called create(). This method was returning a 404 response, because at that point the logic was not implemented. But now we will add the required code to validate the data coming from the client, access the database, store the status of the user, if the data is valid, and finally return the number of rows affected in the operation.

We need to add a new property to the controller to hold a copy of the new table gateway. This property will be used in the getUserStatusesTable() method to store the object and avoid creating more than one instance as shown in the following code:

protected $userStatusesTable;

The first change we need to make in this controller is adding the status of a user when the client requests the data of the profile. The previous version of the get() method was as follows:

public function get($username)
{
    $usersTable = $this->getUsersTable();
    $userData = $usersTable->getByUsername($username);
    $wallData = $userData->getArrayCopy();
        
    if ($userData !== false) {
        return new JsonModel($wallData);
    } else {
        throw new Exception('User not found', 404);
    }
}

Now, this method will be seen as follows:

public function get($username)
{
    $usersTable = $this->getUsersTable();
    $userStatusesTable = $this->getUserStatusesTable();
    	
    $userData = $usersTable->getByUsername($username);
    $userStatuses = $userStatusesTable->getByUserId(
        $userData->id
    )->toArray();
    
    $wallData = $userData->getArrayCopy();
    $wallData['feed'] = $userStatuses;
    
    usort($wallData['feed'], function($a, $b){
        $timestampA = strtotime($a['created_at']);
        $timestampB = strtotime($b['created_at']);
        
        if ($timestampA == $timestampB) {
            return 0;
        }
        
        return ($timestampA > $timestampB) ? -1 : 1;
    });
    
    if ($userData !== false) {
        return new JsonModel($wallData);
    } else {
        throw new Exception('User not found', 404);
    }
}

Let's take a closer look at the differences. We are retrieving the new table gateway to be able to access the status of a user. After retrieving the user data, we retrieve the status of the user from the database by calling the method, getByUserId(), and passing the ID of the user we just got from the database. Then, we create an array to hold the data of the user itself and the statuses.

Earlier, we were returning just a copy of the data of the user as an array. Now we have to compose our own array and merge the data and the statuses of the user on the same array. Finally, instead of returning just the data of the user, we return the array we created with both the user data and the statuses.

Now that we updated the get() method, we will be able to see the statuses of the client, or at least the client will be able to receive this data while requesting the data of a user. Now let's write the code for the create() method that will allow users to post new statuses on their wall, as shown in the following code:

public function create($data)
{
    $userStatusesTable = $this->getUserStatusesTable();
    
    $filters = $userStatusesTable->getInputFilter();
    $filters->setData($data);
    
    if ($filters->isValid()) {
        $data = $filters->getValues();
        
        $result = new JsonModel(array(
            'result' => $userStatusesTable->create(
                $data['user_id'], $data['status']
            )
        ));
    } else {
        $result = new JsonModel(array(
            'result' => false,
            'errors' => $filters->getMessages()
        ));
    }
    
    return $result;
}

The first thing we do is retrieve the table gateway we are going to use. After that we get the filters we need to validate the data sent by the client. The InputFilter component has a method called setData(), and this is used to provide the filters and validations with the data we want to validate. We just need to call the method passing the $data array, which we get as a parameter for the create() method. This variable will be populated automatically by AbstractRestfulController with the $_POST data. To execute the validation itself, we call the method, isValid(), from the InputFilter component that will apply to all the filters and validators on the data we passed and will determine if the data is valid or not.

Right after that, we need to decide what to do in case the data is valid or not. In the first case, we will insert the data to the database using the method, create(), from UserStatusesTable, and return the affected rows in JsonModel. In case the data is invalid, we will compose a new array specifying the errors encountered by the InputFilter component. The error messages are going to be passed to the client in another JsonModel.

The last bit we should add to this controller is the getUserStatusesTable() method that will be stored in $userStatusesTable, an instance of this table gateway, as shown in the following code:

protected function getUserStatusesTable()
{
    if (!$this->userStatusesTable) {
        $sm = $this->getServiceLocator();
        $this->userStatusesTable = $sm->get(
            'UsersModelUserStatusesTable'
        );
    }
    return $this->userStatusesTable;
}

As there is no change, the method is exactly the same as getUsersTable(). The only difference is the name of the component we extract from ServiceManager.

Modifying module.config.php

We need to update the module.config.php file of the Users module, in order to make the UserStatusesTable class available in the dependency injector. We just need to add the following line:

'UsersModelUserStatusesTable' => 'UsersModelUserStatusesTable'
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset