Chapter 6. Working with Images – Publishing Pictures

In this chapter, we are going to add functionalities to the user wall, which will allow the users to add images. We will cover the API and frontend code as usual, and we will discover how to handle file uploads, how to validate files, and how to handle them.

By the end of this chapter, you will know:

  • How to create forms that accept files
  • How to add validation to those files
  • How to handle the upload itself and the manipulation of an image

API development

Let's review the changes we should do on the API level to support file uploads. We will need a new table on the database to store the relations between the images uploaded and the users. As we are creating a new table on the database, we will also need a table gateway object to access the data. Finally, we are going to modify the create() method to handle the code for the status text and add the code for the image upload.

Requirements

The requirements for the API are the same in terms of HTTP methods. The only thing we are going to change is the way we process the data in the create() method. Essentially, when a request arrives to the create() method, we should try to detect if we are fulfilling a request to create a new status entry or are trying to post a new image.

The create() method will inspect the contents of the $data parameter, searching for an entry on the array. If we find the key status in the array, we will call it the createStatus() method, which contains the code we made in Chapter 5, Handling Text Content – Posting Text. If we find the key image, we will call the createImage() method that will handle the request.

Working with the database

Let's have a look at the new table we have to create. Essentially, it is the same as the one we created for the status; the only difference is that we are going to store the filename of the image instead of the status.

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

  • Id
  • User_id
  • Filename
  • Created at
  • Updated at

As usual, the table will also contain two columns to keep a track of the creation and update timestamps of each row.

By now, you should be pretty confident about how to connect to the database hosted on the VM, so we can just jump to see how the syntax to create the table looks , as shown in the following code:

CREATE TABLE `user_images` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` int(11) unsigned DEFAULT NULL,
  `filename` varchar(44) DEFAULT '',
  `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

Changes on the module structure

Now let's have a look at how the folder and file structure will like on the API side of the project. Notice that we added a new folder inside the public folder called images. This is where the uploaded images are going to be stored.

Changes on the module structure

Creating the UserImagesTable.php file

We are not going to spend too much time on this file because it is a copy of UserStatusesTable.php and we are only changing a few lines. We will essentially copy the file and review the changes we should make.

class UserImagesTable extends AbstractTableGateway implements AdapterAwareInterface

Of course, the first thing we should do is change the name of the class; in this case the class will be called UserImagesTable.

The second thing we should change is the name of the table we are going to query; the line will be as follows:

protected $table = 'user_images';

We will change one line in the create() method, but let's see the full method to have a clear understanding about where the change should be made:

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

The last method, getInputFilter(),will remain more or less the same; the only thing we need to do is to remove the block of code that configures the validators and filters for the status field. This will leave the method with just the configuration for the user_id field.

Extending the IndexController.php file

Now that we have the table and the class to access data, let's see the changes we should make in the controller and the improvements we should do in the meantime.

When a user uploads an image, it should appear on the wall as a new entry, which means that we should return this data when the client requests the wall.

Earlier, it was easy as we just had the status updates; but now, we should somehow merge the status updates and the images, and order the entries by the creation date from the most recent to the ones in the past. Basically, that's the approach we are going to follow: first retrieve the data, then merge it, and finally order it by a common column; in this case, created_at.

public function get($username)
{
    $usersTable = $this->getUsersTable();
    $userStatusesTable = $this->getUserStatusesTable();
    $userImagesTable = $this->getUserImagesTable();
    
    $userData = $usersTable->getByUsername($username);
    $userStatuses = $userStatusesTable->getByUserId(
        $userData->id
    )->toArray();
    $userImages = $userImagesTable->getByUserId($userData->id)
        ->toArray();
    
    $wallData = $userData->getArrayCopy();
    $wallData['feed'] = array_merge($userStatuses, $userImages);
    
    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);
    }
}

Ok, that's the whole method; let's review a couple of lines of this method .In the first block, we get instances of the tables. Then in the second block, we get the data based on user_id.

Next, we arrive at the merge phase; basically, we ask for an array version of the results we just got from the db and merge them under the key feed of the array.

After that, we find the order section. We basically use the PHP method, usort(), to sort an array based on an anonymous function we created. Inside the function, we just check if the created_at value of one item is equal, more, or lesser than the created_at value of the other.

Now, let's jump to the create() method and see how we are going to change it to accommodate the new code. The first thing we should do is try to detect the type of request and then based on that, forward the flow to a more specific method that will take care of the request.

public function create($data)
{
    if (array_key_exists('status', $data) && 
        !empty($data['status'])) {
        $result = $this->createStatus($data);
    }
        
    if (array_key_exists('image', $data) && 
        !empty($data['image'])) {
        $result = $this->createImage($data);
    }
        
    return $result;
}

As you can see in this snippet of code, we just check if a certain key exists inside the data. The first block tries to determine if the request is for the creation of a status and to accomplish that, we check if the status key exists on the $data array.

The second block is related to the image a user might send. The code keeps checking for a key called image and will forward everything to the createImage() method if the conditions are satisfied.

Let's us now review the new method, createImage(); the method is a little bit long and is doing quite a lot of stuff, so we are going to see it in sections:

$userImagesTable = $this->getUserImagesTable();

The first line of the following code snippet just gets the table gateway for the images using the getUserImagesTable() method, which we will see later.

$filters = $userImagesTable->getInputFilter();
$filters->setData($data);

In the next block, we focus on the validation of the data. This step is the same as the one we use on the createStatus() method, the only difference is in where we get the input filter from.

if ($filters->isValid()) {
    $filename = sprintf(
        'public/images/%s.png', 
        sha1(uniqid(time(), true))
    );
    $content = base64_decode($data['image']);
    $image = imagecreatefromstring($content);
            
    if (imagepng($image, $filename) === true) {
        $result = new JsonModel(array(
            'result' => $userImagesTable->create(
                $data['user_id'], 
                basename($filename)
            )
        ));
    } else {
        $result = new JsonModel(array(
            'result' => false,
            'errors' => 'Error while storing the image'
        ));
    }
    imagedestroy($image);
} else {
    $result = new JsonModel(array(
        'result' => false,
        'errors' => $filters->getMessages()
    ));
}

This is the next block of code we will encounter in this method. First of all, we have a condition based on the validation of the data. In case it's wrong, we just return an error message to the frontend.

When the data is validated, we jump to handle the image. The image itself is sent by the browser along with the request encoded in base64. The job on the API side is to store that image in a file. To accomplish that, we first create a name for the file as we want to avoid collisions on the filenames, and then generate a random one as follows:

$filename = sprintf(
    'public/images/%s.png', 
    sha1(uniqid(time(), true))
);

As you can see here, we generate a unique ID prepending the current time and activating the entropy flag of the uniqid() method. Then we just encode the result using the sha1 algorithm.

We do two things just after that. Firstly, we decode the image from base64 to the raw data and store it in a variable. Secondly, we load the raw data to an image with the help of imagecreatefromstring(), which takes a variable with the image data as a parameter.

The last thing we do here is store the image converted to PNG; we accomplish that using the imagepng() method and the filename we generated before. If the operation succeeds, we store the filename in the database linked to the user_id attribute and then return the rows affected to the client. If not, we just return an error.

The following is the full code for this method:

protected function createImage($data)
{
    $userImagesTable = $this->getUserImagesTable();
        
    $filters = $userImagesTable->getInputFilter();
    $filters->setData($data);
    $filters->isValid();
    if ($filters->isValid()) {
        $filename = sprintf(
            'public/images/%s.png', 
            sha1(uniqid(time(), true))
        );
        $content = base64_decode($data['image']);
        $image = imagecreatefromstring($content);
            
        if (imagepng($image, $filename) === true) {
            $result = new JsonModel(array(
                'result' => $userImagesTable->create(
                    $data['user_id'], 
                    basename($filename)
                )
            ));
        } else {
            $result = new JsonModel(array(
                'result' => false,
                'errors' => 'Error while storing the image'
            ));
        }
        imagedestroy($image);
    } else {
        $result = new JsonModel(array(
            'result' => false,
            'errors' => $filters->getMessages()
        ));
    }
        
    return $result;
}

Finally, we have arrived on the last change we need to do on the controller. We are going to create a method that will create an instance of the table gateway for the new table we are using. The code doesn't need any explanation as it is almost the same as the one we saw before:

protected function getUserImagesTable()
{
    if (!$this->userImagesTable) {
        $sm = $this->getServiceLocator();
        $this->userImagesTable = $sm->get(
            'UsersModelUserImagesTable'
        );
    }
    return $this->userImagesTable;
} 

As you also know, this method uses a property called userImagesTable that should be added to the class. The code is pretty simple and similar to the one used in the previous chapters:

protected $userImagesTable;

Changing the module.config.php file

As we did in the previous chapter, we only have to tell Dependency Injector about the new table gateway we have. To do that, we just need to add the following line in the module.config.php file of the Users module:

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

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