A free and open-source book on ZF3 for beginners


To demonstrate the usage of file uploads in Zend Framework 3, we will create an Image Gallery that will consist of two web pages: the image upload page allowing to upload an image (figure 10.2); and the gallery page containing the list of uploaded images (figure 10.3).

You can see the working Image Gallery example in the Form Demo sample application bundled with this book.

Figure 10.2. Image Upload Page Figure 10.2. Image Upload Page

Figure 10.3. Image Gallery Page Figure 10.3. Image Gallery Page

For this example, we will create the following things:

10.8.1. Adding ImageForm Model

For this example, we will need a form model which will be used for image file uploads. We will call that form model class the ImageForm. This class will allow us to upload an image file to the server. The form will have the following fields:

The code of the ImageForm form model is presented below. It should be put to ImageForm.php file stored in Form directory under the module's source directory:

<?php
namespace Application\Form;

use Zend\Form\Form;

// This form is used for uploading an image file.
class ImageForm extends Form
{
    // Constructor.     
    public function __construct()
    {
        // Define form name.
        parent::__construct('image-form');
     
        // Set POST method for this form.
        $this->setAttribute('method', 'post');
                
        // Set binary content encoding.
        $this->setAttribute('enctype', 'multipart/form-data');
				
        $this->addElements();        
    }
    
    // This method adds elements to form.
    protected function addElements() 
    {
        // Add "file" field.
        $this->add([
            'type'  => 'file',
            'name' => 'file',
            'attributes' => [                
                'id' => 'file'
            ],
            'options' => [
                'label' => 'Image file',
            ],
        ]);        
          
        // Add the submit button.
        $this->add([
            'type'  => 'submit',
            'name' => 'submit',
            'attributes' => [                
                'value' => 'Upload',
                'id' => 'submitbutton',
            ],
        ]);               
    }
}

We have already discussed the form model creation and the code above should not cause any problems in its understanding. We just want to attract the attention of the reader that in line 19, we set the "multipart/form-data" value for the "enctype" attribute of the form to make the form use binary encoding for its data.

Actually, explicitly setting the "enctype" attribute in form's constructor is optional, because Zend\Form\Element\File element performs that automatically when you call form's prepare() method.

10.8.2. Adding Validation Rules to ImageForm Model

To demonstrate the usage of validators and filters designed to work with file uploads, we will add those to the ImageForm form model class. We want to achieve the following goals:

To add form validation rules, modify the code of the ImageForm class as follows:

<?php
namespace Application\Form;

use Zend\InputFilter\InputFilter;

// This form is used for uploading an image file.
class ImageForm extends Form
{
    // Constructor
    public function __construct()
    {
        // ...
	
        // Add validation rules
        $this->addInputFilter();          
    }
  
    // ...
	
    // This method creates input filter (used for form filtering/validation).
    private function addInputFilter() 
    {
        $inputFilter = new InputFilter();   
        $this->setInputFilter($inputFilter);
     
        // Add validation rules for the "file" field.	 
        $inputFilter->add([
                'type'     => 'Zend\InputFilter\FileInput',
                'name'     => 'file',
                'required' => true,   
                'validators' => [
                    ['name'    => 'FileUploadFile'],
                    [
                        'name'    => 'FileMimeType',                        
                        'options' => [                            
                            'mimeType'  => ['image/jpeg', 'image/png']
                        ]
                    ],
                    ['name'    => 'FileIsImage'],
                    [
                        'name'    => 'FileImageSize',
                        'options' => [
                            'minWidth'  => 128,
                            'minHeight' => 128,
                            'maxWidth'  => 4096,
                            'maxHeight' => 4096
                        ]
                    ],
                ],
                'filters'  => [                    
                    [
                        'name' => 'FileRenameUpload',
                        'options' => [  
                            'target' => './data/upload',
                            'useUploadName' => true,
                            'useUploadExtension' => true,
                            'overwrite' => true,
                            'randomize' => false
                        ]
                    ]
                ],   
            ]);                
    }
}

In the code above, we add the following file validators:

In line 52, we add the RenameUpload filter and configure it to save the uploaded file to the APP_DIR/data/upload directory. The filter will use the same file name for the destination file as the name of the original file (useUploadName option). If the file with such name already exists, the filter will overwrite it (overwrite option).

For the MimeType and IsImage validator to work, you have to enable PHP fileinfo extension. This extension is already enabled in Linux Ubuntu, but not in Windows. After that, do not forget to restart Apache HTTP Server.

10.8.3. Writing ImageManager Service

Because we strive to write code conforming to Domain Driven Design pattern, we will create a service model class encapsulating the functionality for image management. We will call this class ImageManager and put it to Application\Service namespace. We will also register this service in the service manager component of the web application.

The ImageManager service class will have the following public methods (listed in table 10.3):

Table 10.3. Public methods of the ImageManager class.
Method Description
getSaveToDir() Returns path to the directory where we save the image files.
getSavedFiles() Returns the array of saved file names.
getImagePathByName($fileName) Returns the path to the saved image file.
getImageFileInfo($filePath) Retrieves the file information (size, MIME type) by image path.
getImageFileContent($filePath) Returns the image file content. On error, returns boolean false.
resizeImage($filePath, $desiredWidth) Resizes the image, keeping its aspect ratio.

In fact, we could put the code we plan to add into the service into the controller actions, but that would make the controller fat and poorly testable. By introducing the service class, we improve the separation of concerns and code reusability.

Add the ImageManager.php file to the Service directory under the module's source directory. Add the following code to the file:

<?php
namespace Application\Service;

// The image manager service.
class ImageManager 
{
    // The directory where we save image files.
    private $saveToDir = './data/upload/';
        
    // Returns path to the directory where we save the image files.
    public function getSaveToDir() 
    {
        return $this->saveToDir;
    }  
}

As you can see from the code above, we define the ImageManager class in line 5. It has the private $saveToDir property 44 which contains the path to the directory containing our uploaded files (line 8) (we store uploaded files in APP_DIR/data/upload directory).

The getSaveToDir() public method (line 11) allows to retrieve the path to the upload directory.

44) Although the ImageManager class is a service and focused on providing services, it can have properties intended for its internal use.

Next, we want to add the getSavedFiles() public method to the service class. The method will scan the upload directory and return an array containing the names of the uploaded files. To add the getSavedFiles() method, modify the code in the following way:

<?php
//...

// The image manager service.
class ImageManager 
{
    //...
  
    // Returns the array of uploaded file names.
    public function getSavedFiles() 
    {
        // The directory where we plan to save uploaded files.
        
        // Check whether the directory already exists, and if not,
        // create the directory.
        if(!is_dir($this->saveToDir)) {
            if(!mkdir($this->saveToDir)) {
                throw new \Exception('Could not create directory for uploads: ' . 
                             error_get_last());
            }
        }
        
        // Scan the directory and create the list of uploaded files.
        $files = [];        
        $handle  = opendir($this->saveToDir);
        while (false !== ($entry = readdir($handle))) {
            
            if($entry=='.' || $entry=='..')
                continue; // Skip current dir and parent dir.
            
            $files[] = $entry;
        }
        
        // Return the list of uploaded files.
        return $files;
    }  
}

In the getSavedFiles() method above, we first check if the upload directory exists (line 16), and if not, we try to create it (line 17). Then, we get the list of files in the directory (lines 24-32) and return it to the caller.

Next, we add the three methods for getting information about an uploaded file:

To add those three methods, change the code as follows:

<?php
//...

// The image manager service.
class ImageManager 
{
    //...  
  
    // Returns the path to the saved image file.
    public function getImagePathByName($fileName) 
    {
        // Take some precautions to make file name secure.
        $fileName = str_replace("/", "", $fileName);  // Remove slashes.
        $fileName = str_replace("\\", "", $fileName); // Remove back-slashes.
                
        // Return concatenated directory name and file name.
        return $this->saveToDir . $fileName;                
    }
  
    // Returns the image file content. On error, returns boolean false. 
    public function getImageFileContent($filePath) 
    {
        return file_get_contents($filePath);
    }
    
    // Retrieves the file information (size, MIME type) by image path.
    public function getImageFileInfo($filePath) 
    {
        // Try to open file        
        if (!is_readable($filePath)) {            
            return false;
        }
            
        // Get file size in bytes.
        $fileSize = filesize($filePath);

        // Get MIME type of the file.
        $finfo = finfo_open(FILEINFO_MIME);
        $mimeType = finfo_file($finfo, $filePath);
        if($mimeType===false)
            $mimeType = 'application/octet-stream';
    
        return [
            'size' => $fileSize,
            'type' => $mimeType 
        ];
    }  
}

Finally, we want to add the image resizing functionality to the ImageManager class. The image resizing functionality will be used for creating small thumbnail images. Add the resizeImage() method to the ImageManager class as follows:

<?php
//...
class ImageManager 
{
    //...    
  
    // Resizes the image, keeping its aspect ratio.
    public  function resizeImage($filePath, $desiredWidth = 240) 
    {
        // Get original image dimensions.
        list($originalWidth, $originalHeight) = getimagesize($filePath);

        // Calculate aspect ratio
        $aspectRatio = $originalWidth/$originalHeight;
        // Calculate the resulting height
        $desiredHeight = $desiredWidth/$aspectRatio;

        // Get image info
        $fileInfo = $this->getImageFileInfo($filePath); 
        
        // Resize the image
        $resultingImage = imagecreatetruecolor($desiredWidth, $desiredHeight);
        if (substr($fileInfo['type'], 0, 9) =='image/png')
            $originalImage = imagecreatefrompng($filePath);
        else
            $originalImage = imagecreatefromjpeg($filePath);
        imagecopyresampled($resultingImage, $originalImage, 0, 0, 0, 0, 
            $desiredWidth, $desiredHeight, $originalWidth, $originalHeight);

        // Save the resized image to temporary location
        $tmpFileName = tempnam("/tmp", "FOO");
        imagejpeg($resultingImage, $tmpFileName, 80);
        
        // Return the path to resulting image.
        return $tmpFileName;
    }
}

The resizeImage() method above takes two arguments: $filePath (the path to the image file), and $desiredWidth (the width of the thumbnail image). Inside the method, we first calculate an appropriate thumbnail image height (lines 11-16) preserving its aspect ratio. Then, we resize the original image as needed and save it to a temporary file (lines 19-32).

As the ImageManager class is ready, you have to register the ImageManager service in the service manager component of the web application by adding the following lines to the module.config.php configuration file:

<?php
return [
    // ...    
    'service_manager' => [
        // ...
        'factories' => [
            // Register the ImageManager service
            Service\ImageManager::class => InvokableFactory::class,            
        ],
    ],    
    // ...
];

10.8.4. Adding ImageController

For the Image Gallery example, we will create the ImageController controller class. The controller will have the following action methods (listed in table 10.4):

Table 10.4. Action methods of the ImageController class.
Action Method Description
__construct() Will allow to inject ImageManager dependency into the controller.
uploadAction() Shows the image upload page allowing to upload a single image.
indexAction() Displays the image gallery page with the list of uploaded images.
fileAction() Provides an ability to download a full-size image or a small thumbnail for an image.

To start, create the ImageController.php file in the Application/Controller directory under the module's source directory. Put the following stub code into the file:

<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Application\Form\ImageForm;

// This controller is designed for managing image file uploads.
class ImageController extends AbstractActionController 
{
    // The image manager.
    private $imageManager;
  
    // The constructor method is used for injecting the dependencies 
    // into the controller.
    public function __construct($imageManager)
    {
        $this->imageManager = $imageManager;
    }
  
    // This is the default "index" action of the controller. It displays the 
    // Image Gallery page which contains the list of uploaded images.
    public function indexAction() 
    {                
    }
    
    // This action shows the image upload form. This page allows to upload 
    // a single file.
    public function uploadAction() 
    {
    }
    
    // This is the 'file' action that is invoked when a user wants to 
    // open the image file in a web browser or generate a thumbnail.     
    public function fileAction() 
    {        
    }    
}

In the code above, we defined the ImageController class living in the Application\Controller namespace and added the constructor method and three action method stubs into the class: indexAction(), uploadAction() and fileAction(). Next, we will populate those action methods with the code.

10.8.4.1. Adding Upload Action & Corresponding View Template

First, we will complete the uploadAction() method of our controller. This action method will handle the Upload a New Image web page containing the upload form. The form will provide an ability to upload an image file to the gallery.

Change the ImageController.php file as follows:

<?php
//...
class ImageController extends AbstractActionController 
{
    //...
    public function uploadAction() 
    {
        // Create the form model.
        $form = new ImageForm();
        
        // Check if user has submitted the form.
        if($this->getRequest()->isPost()) {
            
            // Make certain to merge the files info!
            $request = $this->getRequest();
            $data = array_merge_recursive(
                $request->getPost()->toArray(),
                $request->getFiles()->toArray()
            );
            
            // Pass data to form.
            $form->setData($data);
                
            // Validate form.
            if($form->isValid()) {
                    
                // Move uploaded file to its destination directory.
                $data = $form->getData();
                    
                // Redirect the user to "Image Gallery" page.
                return $this->redirect()->toRoute('images');
            }                        
        } 
        
        // Render the page.
        return new ViewModel([
                     'form' => $form
                 ]);
    }
}

In the uploadAction() method above, we do the following.

In line 9, we create an instance of the ImageForm form model with the help of the new operator.

In line 12, we check whether the request is an HTTP POST request. If so, we get the data from $_POST and $_FILES super-global PHP arrays and merge them into the single array (lines 15-19). This is required to correctly handle uploaded files, if any. Then we pass this array to the form model with the setData() method (line 22).

In line 25, we call the form model's isValid() method. This method runs the input filter attached to the form model. Since we have only one file input in the input filter, this will only run our three file validators: UploadFile, IsImage and ImageSize.

If the data is valid, we call the getData() method (line 28). For our file field, this will run the RenameUpload filter, which moves our uploaded file to its persistent directory.

After that, in line 31, we redirect the user to the "index" action of the controller (we will populate that action method a little bit later.

Now, its time to add the view template for the "upload" action. Add the upload.phtml view template file under the application/image directory under the module's view directory:

<?php
$form = $this->form;
$form->get('submit')->setAttributes(['class'=>'btn btn-primary']);
$form->prepare();
?>

<h1>Upload a New Image</h1>

<p>
    Please fill out the following form and press the <i>Upload</i> button.
</p>

<div class="row">
    <div class="col-md-6">
        <?= $this->form()->openTag($form); ?>
        
        <div class="form-group">
            <?= $this->formLabel($form->get('file')); ?>
            <?= $this->formElement($form->get('file')); ?>
            <?= $this->formElementErrors($form->get('file')); ?> 
            <div class="hint">(PNG and JPG formats are allowed)</div>
        </div>
                
        <?= $this->formElement($form->get('submit')); ?>
        
        <?= $this->form()->closeTag(); ?>
    </div>    
</div>    

In the code of the view template, we first set "class" attribute (line 3). This is to apply nice-looking Twitter Bootstrap styles to the form's Submit button.

Then, we render the form with the common view helpers that we discussed in Collecting User Input with Forms. For rendering the "file" field, we use the generic FormElement view helper.

Typically, you use the FormElement generic view helper for rendering the file field. The FormElement internally calls the FormFile view helper, which performs the actual rendering.

10.8.4.2. Adding Index Action & Corresponding View Template

The second action method we will complete is the indexAction(). This action will handle the Image Gallery page containing the list of uploaded files and their small thumbnails. For each image, there will be a button "Show In Natural Size" for opening the image in another tab of the web browser.

Change the ImageController.php file as follows:

<?php
//...
class ImageController extends AbstractActionController 
{
    //...
    public function indexAction() 
    {
        // Get the list of already saved files.
        $files = $this->imageManager->getSavedFiles();
        
        // Render the view template.
        return new ViewModel([
            'files'=>$files
        ]);
    }
}

In the code above, we use the getSavedFiles() method of the ImageManager class for retrieving the list of uploaded images and pass them to the view for rendering.

Please note how "slim" and clear this controller action is! We achieved this by moving the image management functionality to the ImageManager service model.

Add the index.phtml view template to application/image directory under the module's view directory. The contents of the file is shown below:

<h1>Image Gallery</h1>

<p>
  This page displays the list of uploaded images.
</p>

<p>
  <a href="<?= $this->url('images', ['action'=>'upload']); ?>" 
     class="btn btn-primary" role="button">Upload More</a>
</p>

<hr/>

<?php if(count($files)==0): ?>

<p>
  <i>There are no files to display.</i>
</p>

<?php else: ?>

<div class="row">
  <div class="col-sm-6 col-md-12">

    <?php foreach($files as $file): ?>  

    <div class="img-thumbnail">
            
      <img src="<?= $this->url('images', ['action'=>'file'], 
            ['query'=>['name'=>$file, 'thumbnail'=>true]]); ?>">
            
      <div class="caption">
        <h3><?php echo $file; ?></h3>                    
        <p>
        <a target="_blank" href="<?= $this->url('images', ['action'=>'file'], 
           ['query'=>['name'=>$file]]); ?>" 
           class="btn btn-default" role="button">Show in Natural Size</a>
        </p>
      </div>
    </div>

    <?php endforeach; ?>
  </div>
</div>

<?php endif; ?>

<hr/>

In the code above, we create the HTML markup for the Upload More button.

Under the button, we use check whether the $files array is empty. If the array is empty, we output the "There are no files to display" message; otherwise we walk through the files and output the thumbnails of each uploaded images.

For rendering a thumbnail, we use the <img> tag. We set its src attribute with the URL pointing to the "file" action of our ImageController controller. We pass two parameters to the action via the query part of the URL: the image name and thumbnail flag.

For styling the thumbnails, we use the Twitter Bootstrap provided ".img-thumbnail" CSS class.

For additional information about these Twitter Bootstrap styles, please refer to the Bootstrap official documentation.

Below each thumbnail, we put the "Show in Natural Size" link, which points to the "file" action of our ImageController controller. When site visitor clicks the link, he will be shown with the image in natural size, and the image will be opened in another browser's tab (note the target="_blank" attribute of the link).

10.8.4.3. Adding File Action

The last action we will populate is the ImageController::fileAction() method. That method will allow to preview an uploaded image or generate a small thumbnail of the image. The action method will take two GET parameters:

Change the ImageController.php file as follows:

 <?php
//...
class ImageController extends AbstractActionController 
{
    //...
    public function fileAction() 
    {
        // Get the file name from GET variable.
        $fileName = $this->params()->fromQuery('name', '');

        // Check whether the user needs a thumbnail or a full-size image.
        $isThumbnail = (bool)$this->params()->fromQuery('thumbnail', false);
    
        // Get path to image file.
        $fileName = $this->imageManager->getImagePathByName($fileName);
        
        if($isThumbnail) {
        
            // Resize the image.
            $fileName = $this->imageManager->resizeImage($fileName);
        }
                
        // Get image file info (size and MIME type).
        $fileInfo = $this->imageManager->getImageFileInfo($fileName);        
        if ($fileInfo===false) {
            // Set 404 Not Found status code
            $this->getResponse()->setStatusCode(404);            
            return;
        }
                
        // Write HTTP headers.
        $response = $this->getResponse();
        $headers = $response->getHeaders();
        $headers->addHeaderLine("Content-type: " . $fileInfo['type']);        
        $headers->addHeaderLine("Content-length: " . $fileInfo['size']);
            
        // Write file content.
        $fileContent = $this->imageManager->getImageFileContent($fileName);
        if($fileContent!==false) {                
            $response->setContent($fileContent);
        } else {        
            // Set 500 Server Error status code.
            $this->getResponse()->setStatusCode(500);
            return;
        }
        
        if($isThumbnail) {
            // Remove temporary thumbnail image file.
            unlink($fileName);
        }
        
        // Return Response to avoid default view rendering.
        return $this->getResponse();
    }    
}

In the code above, we first get the "name" and "thumbnail" parameters from $_GET super-global array (lines 9, 12). If the parameters are missing, their default values are used instead.

In line 15, we use the getImagePathByName() method provided by the ImageManager service to get the absolute path to the image by its name.

If a thumbnail is requested, we resize the image with the resizeImage() method of the ImageManager (line 20). That method returns path to a temporary file containing the thumbnail image.

Then, we get the information about the image file (its MIME type and file size) with the getImageFileInfo() method of the ImageManager (line 24).

Finally, we create a Response object, fill its headers with image information, set its content with data of the image file (lines 32-45), and return the Response object from the controller action (line 53).

Note that returning the Response object disables the default rendering of the view template for this action method. By this reason, we do not create the file.phtml view template file.

10.8.4.4. Creating Factory for the Controller

Because our ImageController uses the ImageManager service, we need to somehow pass it the instance of the ImageManager (to inject the dependency into the controller's constructor). We do this with the help of factory.

Create the ImageControllerFactory.php file under the Controller/Factory subdirectory under the module's source directory. Put the following code into the file:

<?php
namespace Application\Controller\Factory;

use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
use Application\Service\ImageManager;
use Application\Controller\ImageController;

/**
 * This is the factory for ImageController. Its purpose is to instantiate the
 * controller.
 */
class ImageControllerFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, 
                        $requestedName, array $options = null)
    {
        $imageManager = $container->get(ImageManager::class);
        
        // Instantiate the controller and inject dependencies
        return new ImageController($imageManager);
    }
}

10.8.4.5. Registering the ImageController

To let ZF3 know about our controller, register the ImageController in the module.config.php configuration file:

<?php
return [
    //...
    'controllers' => [
        'factories' => [
            Controller\ImageController::class => 
                    Controller\Factory\ImageControllerFactory::class,
            //...
        ],
    ],    
    //...
];

10.8.4.6. Creating Route

We need to add a route for our ImageController controller. To do that, modify the module.config.php file as follows:

<?php
return [
    //...
    'router' => [
        'routes' => [
            'images' => [
                'type'    => Segment::class,
                'options' => [
                    'route'    => '/images[/:action]',
                    'constraints' => [
                        'action' => '[a-zA-Z][a-zA-Z0-9_-]*'
                    ],
                    'defaults' => [
                        'controller'    => Controller\ImageController::class,
                        'action'        => 'index',
                    ],
                ],
            ],
        ],
    ],    
    //...
];

After that, you will be able to get access to our image gallery by the URL like "http://localhost/images", "http://localhost/images/upload" or "http://localhost/images/file".

10.8.5. Results

Finally, adjust directory permissions to make the APP_DIR/data directory writeable by the Apache Web Server. In Linux Ubuntu, this is typically accomplished by the following shell commands (replace the APP_DIR placeholder with the actual directory name of your web application):

chown -R www-data:www-data APP_DIR/data

chmod -R 775 APP_DIR/data

Above, the chown and chmod commands set the Apache user to be the owner of the directory and allow the web server to write to the directory, respectively.

If you now enter the URL http://localhost/images into your web browser's navigation bar, you will see the image gallery page like shown in figure 10.4.

Figure 10.4. Image Gallery Page Figure 10.4. Image Gallery Page

Clicking the Upload More button will open the Upload a New Image page where you can peek an image file for upload. If you pick an unacceptable file (not an image, or too big image), you will see validation errors (see the figure 10.5 below).

Figure 10.5. File Validation Errors Figure 10.5. File Validation Errors

If the upload is completed successfully, you will be redirected back to the Image Gallery page and see the uploaded image in the list of thumbnails. Clicking the View Full Size button will open the image in a new browser tab (see the figure 10.6 below for example).

Figure 10.6. Opening an Image in Natural Size Figure 10.6. Opening an Image in Natural Size

You may find the Image Gallery complete example in the Form Demo sample web application bundled with this book.


Top