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.
For this example, we will create the following things:
ImageForm
form model capable of image file uploads;ImageManager
service class designed for getting the list of uploaded images, retrieving information about an image, and resizing an image;ImageController
class which will contain action methods serving the web pages;ImageControllerFactory
factory that will instantiate the controller and inject dependencies into it;.phtml
file per each controller's action method.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 file
field will allow the user to pick an image file for upload;
and the submit
button field allowing to send the form data to server.
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'sprepare()
method.
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:
UploadFile
validator;IsImage
validator;ImageSize
validator;RenameUpload
filter.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:
UploadFile
validator (line 32) checks whether the uploaded file was really uploaded using
the HTTP POST method.
MimeType
validator (line 34) checks whether the uploaded file is a JPEG or PNG image.
It does that by extracting MIME information from file data.
IsImage
validator (line 39) checks whether the uploaded file is an image file (PNG, JPG,
etc.). It does that by extracting MIME information from file data.
ImageSize
validator (line 41) allows to check that image dimensions lie in an allowed range.
In the code above, we check that the image is between 128 pixels and 4096 pixels in width, and
that the image height lies between 128 pixels and 4096 pixels.
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
andIsImage
validator to work, you have to enable PHPfileinfo
extension. This extension is already enabled in Linux Ubuntu, but not in Windows. After that, do not forget to restart Apache HTTP Server.
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):
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:
the getImagePathByName()
method will take the file name and prepend the path to upload directory
to that file name;
the getImageFileInfo()
method will retrieve MIME information about the file and its size in bytes;
and the getImageFileContent()
will read file data and return them as a string.
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,
],
],
// ...
];
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):
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.
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. TheFormElement
internally calls theFormFile
view helper, which performs the actual rendering.
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).
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.
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);
}
}
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,
//...
],
],
//...
];
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".
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.
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).
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).
You may find the Image Gallery complete example in the Form Demo sample web application bundled with this book.