diff --git a/Form/FormFlow.php b/Form/FormFlow.php index 3927c39e..b9c2fbc3 100644 --- a/Form/FormFlow.php +++ b/Form/FormFlow.php @@ -70,6 +70,16 @@ abstract class FormFlow implements FormFlowInterface { */ protected $handleFileUploads = true; + /** + * @var bool If file uploads should be handled with Gaufrette + */ + protected $handleFileUploadsWithGaufrette = false; + + /** + * @var string Filesystem that Gaufrette will use + */ + protected $gaufretteFilesystem = null; + /** * @var string|null Directory for storing temporary files while handling uploads. If null, the system's default will be used. */ @@ -384,6 +394,20 @@ public function getHandleFileUploadsTempDir() { return $this->handleFileUploadsTempDir; } + /** + * {@inheritDoc} + */ + public function isHandleFileUploadsWithGaufrette() { + return $this->handleFileUploadsWithGaufrette; + } + + /** + * {@inheritDoc} + */ + public function getGaufretteFilesystem() { + return $this->gaufretteFilesystem; + } + public function setAllowRedirectAfterSubmit($allowRedirectAfterSubmit) { $this->allowRedirectAfterSubmit = (bool) $allowRedirectAfterSubmit; } @@ -459,6 +483,10 @@ protected function applySkipping($stepNumber, $direction = 1) { * {@inheritDoc} */ public function reset() { + if(!empty($this->currentStepNumber) && $this->getCurrentStepNumber() >= $this->getLastStepNumber()){ + $this->getDataManager()->cleanup($this); + } + $this->dataManager->drop($this); $this->currentStepNumber = $this->getFirstStepNumber(); $this->newInstance = true; diff --git a/Form/FormFlowInterface.php b/Form/FormFlowInterface.php index 57f4121c..bb0e3868 100644 --- a/Form/FormFlowInterface.php +++ b/Form/FormFlowInterface.php @@ -66,6 +66,16 @@ function isHandleFileUploads(); */ function getHandleFileUploadsTempDir(); + /** + * @var bool If file uploads should be handled with Gaufrette + */ + function isHandleFileUploadsWithGaufrette(); + + /** + * @return string|null Filesystem that Gaufrette will use + */ + function getGaufretteFilesystem(); + /** * @return bool */ diff --git a/README.md b/README.md index 065196e3..1f5206c0 100644 --- a/README.md +++ b/README.md @@ -588,6 +588,23 @@ class CreateVehicleFlow extends FormFlow { } ``` +File uploads can be handled by Gaufrette, a library that provides a filesystem abstraction layer.This can be very useful when yo dou a cluster deployment or you store sessions on Redis. +Just define a Gaufrette filesystem and add this 2 lines: + +```php +// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php +class CreateVehicleFlow extends FormFlow { + + protected $handleFileUploadsWithGaufrette = true; + protected $gaufretteFilesystem = 'tmp_uploads'; + + // ... + +} +``` +More info here: +https://github.com/KnpLabs/KnpGaufretteBundle#configuring-the-filesystems + ## Enabling redirect after submit This feature will allow performing a redirect after submitting a step to load the page containing the next step using a GET request. diff --git a/Resources/config/form_flow.xml b/Resources/config/form_flow.xml index 38308211..75fcdd08 100644 --- a/Resources/config/form_flow.xml +++ b/Resources/config/form_flow.xml @@ -12,6 +12,7 @@ Craue\FormFlowBundle\Form\FormFlow Craue\FormFlowBundle\Storage\SessionStorage + Craue\FormFlowBundle\Storage\GaufretteStorage Craue\FormFlowBundle\EventListener\PreviousStepInvalidEventListener Craue\FormFlowBundle\Form\FormFlowEvents::PREVIOUS_STEP_INVALID Craue\FormFlowBundle\EventListener\FlowExpiredEventListener @@ -25,8 +26,15 @@ + + + + + + + diff --git a/Storage/DataManager.php b/Storage/DataManager.php index 893820b0..2ad9b13a 100644 --- a/Storage/DataManager.php +++ b/Storage/DataManager.php @@ -3,6 +3,7 @@ namespace Craue\FormFlowBundle\Storage; use Craue\FormFlowBundle\Form\FormFlowInterface; +use Gaufrette\File; /** * Manages data of flows and their steps. @@ -34,11 +35,18 @@ class DataManager implements ExtendedDataManagerInterface { */ private $storage; + /** + * @var GaufretteStorage + */ + private $gaufretteStorage; + /** * @param StorageInterface $storage + * @param GaufretteStorage $gaufretteStorage */ - public function __construct(StorageInterface $storage) { + public function __construct(StorageInterface $storage, GaufretteStorage $gaufretteStorage) { $this->storage = $storage; + $this->gaufretteStorage = $gaufretteStorage; } /** @@ -48,15 +56,32 @@ public function getStorage() { return $this->storage; } + /** + * {@inheritDoc} + */ + public function getGaufretteStorage() { + return $this->gaufretteStorage; + } + /** * {@inheritDoc} */ public function save(FormFlowInterface $flow, array $data) { // handle file uploads if ($flow->isHandleFileUploads()) { - array_walk_recursive($data, function(&$value, $key) { - if (SerializableFile::isSupported($value)) { - $value = new SerializableFile($value); + array_walk_recursive($data, function(&$value, $key) use ($flow) { + if(!$flow->isHandleFileUploadsWithGaufrette()){ + if (SerializableFile::isSupported($value)) { + $value = new SerializableFile($value); + } + }else{ + if (GaufretteFile::isSupported($value)) { + $fileName = $value->getClientOriginalName(); + if(!$this->gaufretteStorage->hasFile($flow->getGaufretteFilesystem(), $fileName)){ + $fileName = $this->gaufretteStorage->doUpload($flow->getGaufretteFilesystem(), $value); + } + $value = new GaufretteFile($fileName, $value); + } } }); } @@ -93,9 +118,16 @@ public function load(FormFlowInterface $flow) { // handle file uploads if ($flow->isHandleFileUploads()) { $tempDir = $flow->getHandleFileUploadsTempDir(); - array_walk_recursive($data, function(&$value, $key) use ($tempDir) { - if ($value instanceof SerializableFile) { - $value = $value->getAsFile($tempDir); + array_walk_recursive($data, function(&$value, $key) use ($flow, $tempDir) { + if(!$flow->isHandleFileUploadsWithGaufrette()){ + if ($value instanceof SerializableFile) { + $value = $value->getAsFile($tempDir); + } + }else{ + if ($value instanceof GaufretteFile) { + $downloadedFile = $this->gaufretteStorage->doDownload($flow->getGaufretteFilesystem(), $value); + $value = $value->getAsUploadedFile($downloadedFile); + } } }); } @@ -111,6 +143,30 @@ public function exists(FormFlowInterface $flow) { return isset($savedFlows[$flow->getName()][$flow->getInstanceId()][self::DATA_KEY]); } + /** + * {@inheritDoc} + */ + public function cleanup(FormFlowInterface $flow) { + $data = []; + + // try to find data for the given flow + $savedFlows = $this->storage->get(DataManagerInterface::STORAGE_ROOT, []); + if (isset($savedFlows[$flow->getName()][$flow->getInstanceId()][self::DATA_KEY])) { + $data = $savedFlows[$flow->getName()][$flow->getInstanceId()][self::DATA_KEY]; + } + + // look for Gaufrette files to cleanup + if ($flow->isHandleFileUploads()) { + array_walk_recursive($data, function(&$value, $key) use ($flow) { + if($flow->isHandleFileUploadsWithGaufrette()){ + if ($value instanceof GaufretteFile) { + $this->gaufretteStorage->doRemove($flow->getGaufretteFilesystem(), $value); + } + } + }); + } + } + /** * {@inheritDoc} */ diff --git a/Storage/DataManagerInterface.php b/Storage/DataManagerInterface.php index 07555428..e983b136 100644 --- a/Storage/DataManagerInterface.php +++ b/Storage/DataManagerInterface.php @@ -21,6 +21,11 @@ interface DataManagerInterface { */ function getStorage(); + /** + * @return GaufretteStorage + */ + function getGaufretteStorage(); + /** * Saves data of the given flow. * @param FormFlowInterface $flow @@ -42,6 +47,13 @@ function exists(FormFlowInterface $flow); */ function load(FormFlowInterface $flow); + /** + * Cleanups Gaufrette temp data of the given flow. + * @param FormFlowInterface $flow + * @return bool + */ + function cleanup(FormFlowInterface $flow); + /** * Drops data of the given flow. * @param FormFlowInterface $flow diff --git a/Storage/GaufretteFile.php b/Storage/GaufretteFile.php new file mode 100644 index 00000000..88da3cdb --- /dev/null +++ b/Storage/GaufretteFile.php @@ -0,0 +1,79 @@ +UploadedFile currently. + * + * @author Kevin Cerro + * @copyright 2020 Kevin Cerro + * @license http://opensource.org/licenses/mit-license.php MIT License + */ +class GaufretteFile +{ + /** + * @var string Name of the file provided by Gaufrette on upload + */ + private $fileName; + + private $clientMimeType; + + /** + * @param string $filename + * @param $originalFile + */ + public function __construct(string $filename, $originalFile) + { + if (!self::isSupported($originalFile)) { + throw new InvalidTypeException($originalFile, UploadedFile::class); + } + + //Filename of uploaded file with Gaufrette + $this->fileName = $filename; + + //Keep client original mime type + $this->clientMimeType = $originalFile->getClientMimeType(); + } + + /** + * @param File $file + * @return mixed The file retrieved from Gaufrette converted to UploadedFile + */ + public function getAsUploadedFile(File $file) + { + $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, $file->getContent()); + + TempFileUtil::addTempFile($tempFile); + + // avoid a deprecation notice regarding "passing a size as 4th argument to the constructor" + // TODO remove as soon as Symfony >= 4.1 is required + if (property_exists(UploadedFile::class, 'size')) { + return new UploadedFile($tempFile, $this->fileName, $this->clientMimeType, null, null, true); + } + + return new UploadedFile($tempFile, $this->fileName, $this->clientMimeType, null, true); + } + + public function getFileName() + { + return $this->fileName; + } + + /** + * @param mixed $file + * @return bool + */ + public static function isSupported($file) + { + return $file instanceof UploadedFile; + } +} diff --git a/Storage/GaufretteStorage.php b/Storage/GaufretteStorage.php new file mode 100644 index 00000000..c86dea80 --- /dev/null +++ b/Storage/GaufretteStorage.php @@ -0,0 +1,88 @@ + + * @copyright 2020 Kevin Cerro + * @license http://opensource.org/licenses/mit-license.php MIT License + */ +class GaufretteStorage +{ + private $filesystemMap; + + /** + * Constructs a new instance of GaufretteStorage. + * + * @param FilesystemMapInterface $filesystemMap + */ + public function __construct(FilesystemMapInterface $filesystemMap) + { + $this->filesystemMap = $filesystemMap; + } + + public function doUpload(string $filesystem, UploadedFile $file) + { + $filesystem = $this->getFilesystem($filesystem); + $randomName = $this->generateRandomName(); + + if ($filesystem->getAdapter() instanceof MetadataSupporter) { + $filesystem->getAdapter()->setMetadata($randomName, ['contentType' => $file->getMimeType()]); + } + + $filesystem->write($randomName, file_get_contents($file->getPathname()), true); + return $randomName; + } + + public function doDownload(string $filesystem, GaufretteFile $gaufretteFile) + { + $filesystem = $this->getFilesystem($filesystem); + return $filesystem->get($gaufretteFile->getFileName()); + } + + public function doRemove(string $filesystem, GaufretteFile $gaufretteFile) + { + $filesystem = $this->getFilesystem($filesystem); + + try { + return $filesystem->delete($gaufretteFile->getFileName()); + } catch (FileNotFound $e) { + return false; + } + } + + public function hasFile(string $filesystem, string $fileName) + { + return $this->getFilesystem($filesystem)->has($fileName); + } + + /** + * Get filesystem adapter from the property mapping. + * @param string $filesystem + * @return FilesystemInterface + */ + private function getFilesystem(string $filesystem): FilesystemInterface + { + return $this->filesystemMap->get($filesystem); + } + + /** + * Generates random name + * TODO May we can improve this by setting the extension on the random name + * @return string|string[] + */ + private function generateRandomName() + { + return str_replace('.', '', \uniqid('', true)); + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index 758d49c6..3acccd3e 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ ], "require": { "php": "~7.0", + "knplabs/knp-gaufrette-bundle": "^0.7.1", "symfony/config": "~3.4|~4.2|~5.0", "symfony/dependency-injection": "~3.4|~4.2|~5.0", "symfony/event-dispatcher": "~3.4|~4.2|~5.0",