In an ideal world we would never need to send a file for download in PHP, and rather write the file out and let Apache handle it all. However, it’s not always that simple, and sometimes we need to use PHP, for example if we are creating files on the fly that will only be accessed once, or we want to secure downloads to authenticated sessions only. Jani Hartikainen recently posted an article on using the X-Sendfile header to send files rather than reading and outputting the file with PHP. I was also planning to write an article about it myself but Jani beat me to the punch, so instead I thought I would follow up his post with a handy little Zend Framework action helper I wrote to make sending files easy.
I have put my action helper on GitHub, so please take a look and feel free to use it in your own projects.
Noginn_Controller_Action_Helper_SendFile
The basic usage is incredibly simple and just requires the path to the file and mime-type.
$this->_helper->sendFile('/path/to/file.zip', 'application/zip');
This will send the correct headers and output the file using readfile. Alternatively if you would rather make us of the X-Sendfile response header then we can set the xsendfile option.
$this->_helper->sendFile('/path/to/file.zip', 'application/zip', array('xsendfile' => true));
For more information on the X-Sendfile response header, take a look at Jani’s blog post, the mod_xsendfile Apache module or the Lighttp documentation.
The options available to the sendFile method are:
- filename - the filename the file will be sent as.
- modified - when the file was last modified in unix timestamp format.
- disposition - whether to send the file as an attachment or inline.
- xsendfile - whether to send the file using the X-Sendfile header.
- cache - these options will make up the Cache-Control response header.
- public - marks authenticated responses as cacheable; normally, if HTTP authentication is required, responses are automatically un-cacheable (for shared caches).
- no-cache - forces browsers to submit the request to the origin server for validation before releasing a cached copy, every time.
- no-store - never cache under any circumstances.
- must-revalidate - tells the browser that it must obey any freshness information you give it.
- proxy-validate - similar to must-revalidate except it only applies to proxy cache.
- max-age - specifies the amount of seconds the file will remain in cache, this is relative and will also set the Expires response header.
- s-maxage - similar to max-age except it only applies to proxy cache.
As well as sending the file I also wanted to allow browser caching and therefore needed to validate If-Modified-Since request headers, using the example below we can make the file cacheable for 1 hour.
$this->_helper->sendFile('/path/to/image.jpg', 'image/jpeg', array(
'cache' => array(
'must-revalidate' => true,
'max-age' => 3600,
)
));
When the browser sends an If-Modified-Since request header, the helper will check if the file modified time is greater and if so continue to send the file, otherwise a 304 not modified header will be sent.
/**
* Validate the cache using the If-Modified-Since request header
*
* @param int $modified When the file was last modified as a unix timestamp
* @return bool
*/
public function notModifiedSince($modified)
{
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $modified < = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
// Send a 304 Not Modified header
$response = $this->getResponse();
$response->setHttpResponseCode(304);
$response->sendHeaders();
return true;
}
return false;
}
In comparison if we never want the file to be cached we can utilise the no-store Cache-Control header.
$this->_helper->sendFile('/path/to/image.jpg', 'image/jpeg', array('cache' => array('no-store' => true)));
If you are creating files on the fly then the sendData method may be useful, it works the same as sendFile except it takes binary data rather than a file path.
$data = 'My file contents';
$this->_helper->sendFile->sendData($data, 'text/plain', 'filename.txt');
Just like the sendFile method, sendData also supports supports browser caching, just remember you need to provide a valid modified timestamp.
$data = 'My cached file';
$this->_helper->sendFile->sendData($data, 'text/plain', 'filename.txt', array(
'modified' => 1235952735,
'cache' => array(
'must-revalidate' => true,
'max-age' => 3600
)
));
One small caveat of my implementation of sendData is that it requires the data to be passed directly into the method, however the output of the GD library functions cannot be assigned to a variable. This can be worked around by using ob_start and ob_get_contents, which is probably not the most elegant solution, but it works.
$image = imagecreatetruecolor(150, 30);
$backgroundColour = imagecolorallocate($image, 50, 255, 125);
imagefilledrectangle($image, 0, 0, 150, 30, $backgroundColour);
ob_start();
imagejpeg($image);
$data = ob_get_contents();
ob_end_clean();
$this->_helper->sendFile->sendData($data, 'image/jpeg', 'green.jpg');
I hope this can be of some use, and I’m open to all suggestions and improvements in my implementation. I’ve just started to use git and the helper is now hosted on GitHub, feel free to fork it and make modifications.
Continue reading this article or post a comment (4)




















