-
-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introducing the FileUploadNormalizer (#221)
* Introducing the FileUploadNormalizer * Docs
- Loading branch information
Showing
7 changed files
with
483 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# FileUploadNormalizer | ||
|
||
The problem the `FileUploadNormalizer` tries to tackle is that Contao's file uploads in the form generator (`Form.php`) | ||
accesses file uploads from the widget itself and there is no defined API. The built-in upload form field generates a | ||
typical PHP upload array. Some form field widgets return a Contao Dbafs UUID, others just a file path and some even | ||
return multiple values. It's a mess. | ||
|
||
It is designed to be used with the `processFormData` hook specifically. | ||
|
||
## Usage | ||
|
||
```php | ||
|
||
use Codefog\HasteBundle\FileUploadNormalizer; | ||
use Contao\CoreBundle\DependencyInjection\Attribute\AsHook; | ||
use Contao\Form; | ||
use Contao\Widget; | ||
|
||
#[AsHook('prepareFormData')] | ||
class PrepareFomDataListener | ||
{ | ||
public function __construct(private readonly FileUploadNormalizer $fileUploadNormalizer) | ||
{ | ||
} | ||
|
||
/** | ||
* @param array<Widget> $fields | ||
*/ | ||
public function __invoke(array $submitted, array $labels, array $fields, Form $form, array $files): void | ||
{ | ||
// You now have an array of normalized files. | ||
$normalizedFiles = $this->fileUploadNormalizer->normalize($files); | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Codefog\HasteBundle; | ||
|
||
use Contao\CoreBundle\Filesystem\VirtualFilesystemInterface; | ||
use Contao\StringUtil; | ||
use Contao\Validator; | ||
use Symfony\Component\Filesystem\Filesystem; | ||
use Symfony\Component\Filesystem\Path; | ||
use Symfony\Component\Mime\MimeTypeGuesserInterface; | ||
use Symfony\Component\Uid\Uuid; | ||
|
||
class FileUploadNormalizer | ||
{ | ||
private const REQUIRED_KEYS = ['name', 'type', 'tmp_name', 'error', 'size', 'uuid']; | ||
|
||
public function __construct( | ||
private readonly string $projectDir, | ||
private readonly MimeTypeGuesserInterface $mimeTypeGuesser, | ||
private readonly VirtualFilesystemInterface $filesStorage, | ||
) { | ||
} | ||
|
||
/** | ||
* This service helps to normalize file upload widget values. Some return an | ||
* array, others just uuids, some only file paths. This method is designed to | ||
* bring them all to the Contao FormUpload value style. | ||
* | ||
* @return array<string, array<array{name: string, type: string, tmp_name: string, error: int, size: int, uploaded: bool, uuid: ?string, stream: ?resource}>> | ||
*/ | ||
public function normalize(array $files): array | ||
{ | ||
$standardizedPerKey = []; | ||
|
||
foreach ($files as $k => $file) { | ||
switch (true) { | ||
case $this->hasRequiredKeys($file): | ||
$file['stream'] = $this->fopen($file['tmp_name']); | ||
$file['uploaded'] ??= true; | ||
$standardizedPerKey[$k][] = $file; | ||
break; | ||
case $this->isPhpUpload($file): | ||
$standardizedPerKey[$k][] = $this->fromPhpUpload($file); | ||
break; | ||
case \is_array($file): | ||
foreach ($this->normalize($file) as $nestedFiles) { | ||
$standardizedPerKey[$k] = array_merge($standardizedPerKey[$k] ?? [], $nestedFiles); | ||
} | ||
break; | ||
case null !== ($uuid = $this->extractUuid($file)): | ||
$standardizedPerKey[$k][] = $this->fromUuid($uuid); | ||
break; | ||
case null !== ($filePath = $this->extractFilePath($file)): | ||
$standardizedPerKey[$k][] = $this->fromFile($filePath); | ||
break; | ||
} | ||
} | ||
|
||
return $standardizedPerKey; | ||
} | ||
|
||
private function fromFile(string $file): array | ||
{ | ||
return [ | ||
'name' => basename($file), | ||
'type' => $this->mimeTypeGuesser->guessMimeType($file), | ||
'tmp_name' => $file, | ||
'error' => 0, | ||
'size' => false === ($size = filesize($file)) ? 0 : $size, | ||
'uploaded' => true, | ||
'uuid' => null, | ||
'stream' => $this->fopen($file), | ||
]; | ||
} | ||
|
||
private function fromUuid(Uuid $uuid): array | ||
{ | ||
$item = $this->filesStorage->get($uuid); | ||
|
||
if (null === $item) { | ||
return []; | ||
} | ||
|
||
return [ | ||
'name' => $item->getName(), | ||
'type' => $item->getMimeType(), | ||
'tmp_name' => $item->getPath(), | ||
'error' => 0, | ||
'size' => $item->getFileSize(), | ||
'uploaded' => true, | ||
'uuid' => $uuid->toRfc4122(), | ||
'stream' => $this->filesStorage->readStream($uuid), | ||
]; | ||
} | ||
|
||
private function hasRequiredKeys(mixed $file): bool | ||
{ | ||
if (!\is_array($file)) { | ||
return false; | ||
} | ||
|
||
return [] === array_diff(self::REQUIRED_KEYS, array_keys($file)); | ||
} | ||
|
||
private function extractUuid(mixed $candidate): Uuid|null | ||
{ | ||
if (!Validator::isUuid($candidate)) { | ||
return null; | ||
} | ||
|
||
if (Validator::isBinaryUuid($candidate)) { | ||
$candidate = StringUtil::binToUuid($candidate); | ||
} | ||
|
||
try { | ||
return Uuid::isValid($candidate) ? Uuid::fromString($candidate) : Uuid::fromBinary($candidate); | ||
} catch (\Throwable) { | ||
return null; | ||
} | ||
} | ||
|
||
private function extractFilePath(mixed $file): string|null | ||
{ | ||
if (!\is_string($file)) { | ||
return null; | ||
} | ||
|
||
$file = Path::makeAbsolute($file, $this->projectDir); | ||
|
||
if (!(new Filesystem())->exists($file)) { | ||
return null; | ||
} | ||
|
||
return $file; | ||
} | ||
|
||
/** | ||
* @return resource|null | ||
*/ | ||
private function fopen(string $file) | ||
{ | ||
try { | ||
$handle = @fopen($file, 'r'); | ||
} catch (\Throwable) { | ||
return null; | ||
} | ||
|
||
if (false === $handle) { | ||
return null; | ||
} | ||
|
||
return $handle; | ||
} | ||
|
||
private function isPhpUpload(mixed $file): bool | ||
{ | ||
if (!\is_array($file) || !isset($file['tmp_name'])) { | ||
return false; | ||
} | ||
|
||
return is_uploaded_file($file['tmp_name']); | ||
} | ||
|
||
private function fromPhpUpload(array $file): array | ||
{ | ||
return [ | ||
'name' => $file['name'], | ||
'type' => $file['type'], | ||
'tmp_name' => $file['tmp_name'], | ||
'error' => 0, | ||
'size' => $file['size'], | ||
'uploaded' => true, | ||
'uuid' => null, | ||
'stream' => $this->fopen($file['tmp_name']), | ||
]; | ||
} | ||
} |
Oops, something went wrong.