Skip to content

Commit

Permalink
support for handling file uploads
Browse files Browse the repository at this point in the history
  • Loading branch information
craue committed Aug 19, 2014
1 parent 1b5a46f commit c33001a
Show file tree
Hide file tree
Showing 19 changed files with 546 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- [#125]: added generic form options to simplify passing options to all steps
- [#126]: allow custom classes on buttons
- [#133]+[#134]: added Farsi translation
- [#146]: added support for handling file uploads

[#98]: https://github.com/craue/CraueFormFlowBundle/issues/98
[#101]: https://github.com/craue/CraueFormFlowBundle/issues/101
Expand All @@ -28,6 +29,7 @@
[#133]: https://github.com/craue/CraueFormFlowBundle/issues/133
[#134]: https://github.com/craue/CraueFormFlowBundle/issues/134
[#143]: https://github.com/craue/CraueFormFlowBundle/issues/143
[#146]: https://github.com/craue/CraueFormFlowBundle/issues/146

## 2.1.5 (2014-06-13)

Expand Down
15 changes: 15 additions & 0 deletions CraueFormFlowBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Craue\FormFlowBundle;

use Craue\FormFlowBundle\Util\TempFileUtil;
use Symfony\Component\HttpKernel\Bundle\Bundle;

/**
Expand All @@ -10,4 +11,18 @@
* @license http://opensource.org/licenses/mit-license.php MIT License
*/
class CraueFormFlowBundle extends Bundle {

/**
* {@inheritDoc}
*/
public function boot() {
/*
* Removes all temporary files created while handling file uploads.
* Use a shutdown function to clean up even in case of a fatal error.
*/
register_shutdown_function(function() {
TempFileUtil::removeTempFiles();
});
}

}
65 changes: 63 additions & 2 deletions Form/FormFlow.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Craue\FormFlowBundle\Event\PreBindEvent;
use Craue\FormFlowBundle\Event\PreviousStepInvalidEvent;
use Craue\FormFlowBundle\Exception\InvalidTypeException;
use Craue\FormFlowBundle\Storage\SerializableFile;
use Craue\FormFlowBundle\Storage\StorageInterface;
use Craue\FormFlowBundle\Util\StringUtil;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
Expand Down Expand Up @@ -60,6 +61,16 @@ abstract class FormFlow implements FormFlowInterface {
*/
protected $allowDynamicStepNavigation = false;

/**
* @var boolean If file uploads should be handled by serializing them into the storage.
*/
protected $handleFileUploads = true;

/**
* @var string|null Directory for storing temporary files while handling uploads. If {@code null}, the system's default will be used.
*/
protected $handleFileUploadsTempDir = null;

/**
* @var string
*/
Expand Down Expand Up @@ -335,6 +346,28 @@ public function isAllowDynamicStepNavigation() {
return $this->allowDynamicStepNavigation;
}

public function setHandleFileUploads($handleFileUploads) {
$this->handleFileUploads = (boolean) $handleFileUploads;
}

/**
* {@inheritDoc}
*/
public function isHandleFileUploads() {
return $this->handleFileUploads;
}

public function setHandleFileUploadsTempDir($handleFileUploadsTempDir) {
$this->handleFileUploadsTempDir = $handleFileUploadsTempDir !== null ? (string) $handleFileUploadsTempDir : null;
}

/**
* {@inheritDoc}
*/
public function getHandleFileUploadsTempDir() {
return $this->handleFileUploadsTempDir;
}

public function setDynamicStepNavigationInstanceParameter($dynamicStepNavigationInstanceParameter) {
$this->dynamicStepNavigationInstanceParameter = $dynamicStepNavigationInstanceParameter;
}
Expand Down Expand Up @@ -609,7 +642,16 @@ protected function bindFlow() {
public function saveCurrentStepData(FormInterface $form) {
$stepData = $this->retrieveStepData();

$stepData[$this->currentStepNumber] = $this->getRequest()->request->get($form->getName(), array());
$request = $this->getRequest();
$formName = $form->getName();

$currentStepData = $request->request->get($formName, array());

if ($this->handleFileUploads) {
$currentStepData = array_merge($currentStepData, $request->files->get($formName, array()));
}

$stepData[$this->currentStepNumber] = $currentStepData;

$this->saveStepData($stepData);
}
Expand Down Expand Up @@ -851,10 +893,29 @@ protected function loadStepsConfig() {
}

protected function retrieveStepData() {
return $this->storage->get($this->getStepDataKey(), array());
$data = $this->storage->get($this->getStepDataKey(), array());

if ($this->handleFileUploads) {
$tempDir = $this->handleFileUploadsTempDir;
array_walk_recursive($data, function(&$value, $key) use ($tempDir) {
if ($value instanceof SerializableFile) {
$value = $value->getAsFile($tempDir);
}
});
}

return $data;
}

protected function saveStepData(array $data) {
if ($this->handleFileUploads) {
array_walk_recursive($data, function(&$value, $key) {
if (SerializableFile::isSupported($value)) {
$value = new SerializableFile($value);
}
});
}

$this->storage->set($this->getStepDataKey(), $data);
}

Expand Down
10 changes: 10 additions & 0 deletions Form/FormFlowInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ function isRevalidatePreviousSteps();
*/
function isAllowDynamicStepNavigation();

/**
* @return boolean If file uploads should be handled by serializing them into the storage.
*/
function isHandleFileUploads();

/**
* @return string|null Directory for storing temporary files while handling uploads. If {@code null}, the system's default will be used.
*/
function getHandleFileUploadsTempDir();

/**
* @return string
*/
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Features:
- step labels
- skipping of steps
- different validation group for each step
- handling of file uploads
- dynamic step navigation

A live demo showcasing these features is available at http://craue.de/sf2playground/en/CraueFormFlow/.
Expand Down Expand Up @@ -540,6 +541,36 @@ you should modify the opening form tag in the form template like this:
app.request.query.all | craue_removeDynamicStepNavigationParameters(flow)) }}" {{ form_enctype(form) }}>
```

## Handling of file uploads

File uploads are transparently handled by Base64-encoding the content and storing it in the session, so it may affect performance.
This feature is enabled by default for convenience, but can be disabled in the flow class as follows:

```php
// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php
class CreateVehicleFlow extends FormFlow {

protected $handleFileUploads = false;

// ...

}
```

By default, the system's directory for temporary files will be used for files restored from the session while loading step data.
You can set a custom one:

```php
// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php
class CreateVehicleFlow extends FormFlow {

protected $handleFileUploadsTempDir = '/path/for/flow/uploads';

// ...

}
```

## Using events

There are some events which you can subscribe to. Using all of them right inside your flow class could look like this:
Expand Down
68 changes: 68 additions & 0 deletions Storage/SerializableFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace Craue\FormFlowBundle\Storage;

use Craue\FormFlowBundle\Exception\InvalidTypeException;
use Craue\FormFlowBundle\Util\TempFileUtil;
use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
* Representation of a serializable file. Only supports {@code UploadedFile} currently.
*
* @author Christian Raue <christian.raue@gmail.com>
* @copyright 2011-2014 Christian Raue
* @license http://opensource.org/licenses/mit-license.php MIT License
*/
class SerializableFile {

protected $content;
protected $type; // for possible future support of further types

protected $clientOriginalName;
protected $clientMimeType;
protected $clientSize;

/**
* @param mixed $file An object meant to be serialized.
* @throws InvalidTypeException If the type of {@code $file} is unsupported.
*/
public function __construct($file) {
if (!self::isSupported($file)) {
throw new InvalidTypeException($file, 'Symfony\Component\HttpFoundation\File\UploadedFile');
}

$this->content = base64_encode(file_get_contents($file->getPathname()));
$this->type = 'Symfony\Component\HttpFoundation\File\UploadedFile';

$this->clientOriginalName = $file->getClientOriginalName();
$this->clientMimeType = $file->getClientMimeType();
$this->clientSize = $file->getClientSize();
}

/**
* @param string|null $tempDir Directory for storing temporary files. If {@code null}, the system's default will be used.
* @return mixed The unserialized object.
*/
public function getAsFile($tempDir = null) {
if ($tempDir === null) {
$tempDir = sys_get_temp_dir();
}

// create a temporary file with its original content
$tempFile = tempnam($tempDir, 'craue_form_flow_serialized_file');
file_put_contents($tempFile, base64_decode($this->content));

TempFileUtil::addTempFile($tempFile);

return new UploadedFile($tempFile, $this->clientOriginalName, $this->clientMimeType, $this->clientSize, null, true);
}

/**
* @param mixed $file
* @return boolean
*/
public static function isSupported($file) {
return $file instanceof UploadedFile;
}

}
Binary file added Tests/Fixtures/blue-pixel.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions Tests/Fixtures/some-text.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
some text
30 changes: 30 additions & 0 deletions Tests/Form/FormFlowTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,36 @@ public function testSetIsAllowDynamicStepNavigation($expectedValue, $allowDynami
$this->assertEquals($expectedValue, $flow->isAllowDynamicStepNavigation());
}

/**
* @dataProvider dataBooleanSetter
*/
public function testSetIsHandleFileUploads($expectedValue, $handleFileUploads) {
$flow = $this->getFlowMock();

$flow->setHandleFileUploads($handleFileUploads);

$this->assertEquals($expectedValue, $flow->isHandleFileUploads());
}

/**
* @dataProvider dataSetGetHandleFileUploadsTempDir
*/
public function testSetGetHandleFileUploadsTempDir($expectedValue, $handleFileUploadsTempDir) {
$flow = $this->getFlowMock();

$flow->setHandleFileUploadsTempDir($handleFileUploadsTempDir);

$this->assertEquals($expectedValue, $flow->getHandleFileUploadsTempDir());
}

public function dataSetGetHandleFileUploadsTempDir() {
return array(
array(null, null),
array('1', 1),
array('/tmp', '/tmp'),
);
}

public function testSetGetDynamicStepNavigationInstanceParameter() {
$flow = $this->getFlowMock();

Expand Down
9 changes: 9 additions & 0 deletions Tests/IntegrationTestBundle/Controller/FormFlowController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Craue\FormFlowBundle\Form\FormFlow;
use Craue\FormFlowBundle\Tests\IntegrationTestBundle\Entity\Issue64Data;
use Craue\FormFlowBundle\Tests\IntegrationTestBundle\Entity\PhotoUpload;
use Craue\FormFlowBundle\Tests\IntegrationTestBundle\Entity\RevalidatePreviousStepsData;
use Craue\FormFlowBundle\Tests\IntegrationTestBundle\Entity\Topic;
use Craue\FormFlowBundle\Tests\IntegrationTestBundle\Entity\Vehicle;
Expand Down Expand Up @@ -96,6 +97,14 @@ public function onlyOneStepAction() {
return $this->processFlow((object) array(), $this->get('integrationTestBundle.form.flow.onlyOneStep'));
}

/**
* @Route("/photoUpload/", name="_FormFlow_photoUpload")
* @Template("IntegrationTestBundle:FormFlow:photoUpload.html.twig")
*/
public function photoUploadAction() {
return $this->processFlow(new PhotoUpload(), $this->get('integrationTestBundle.form.flow.photoUpload'));
}

protected function processFlow($formData, FormFlow $flow) {
$flow->bind($formData);

Expand Down
35 changes: 35 additions & 0 deletions Tests/IntegrationTestBundle/Entity/PhotoUpload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Craue\FormFlowBundle\Tests\IntegrationTestBundle\Entity;

use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;

/**
* @author Christian Raue <christian.raue@gmail.com>
* @copyright 2011-2014 Christian Raue
* @license http://opensource.org/licenses/mit-license.php MIT License
*/
class PhotoUpload {

/**
* @var UploadedFile
* @Assert\NotNull(groups={"flow_photoUpload_step1"})
* @Assert\Image(groups={"flow_photoUpload_step1"})
*/
public $photo;

/**
* @var string
*/
public $comment;

public function getPhotoDataBase64Encoded() {
return base64_encode(file_get_contents($this->photo->getPathname()));
}

public function getPhotoMimeType() {
return $this->photo->getMimeType();
}

}

0 comments on commit c33001a

Please sign in to comment.