Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FormElement: Add FileElement #66

Merged
merged 4 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"ipl/validator": "dev-master",
"psr/http-message": "~1.0"
},
"require-dev": {
"guzzlehttp/psr7": "^1"
},
"autoload": {
"psr-4": {
"ipl\\Html\\": "src"
Expand Down
1 change: 1 addition & 0 deletions src/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ public function handleRequest(ServerRequestInterface $request)
$params = [];
}

$params = array_merge_recursive($params, $request->getUploadedFiles());
$this->populate($params);

// Assemble after populate in order to conditionally provide form elements
Expand Down
162 changes: 162 additions & 0 deletions src/FormElement/FileElement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?php

namespace ipl\Html\FormElement;

use ipl\Html\Attributes;
use ipl\Validator\FileValidator;
use ipl\Validator\ValidatorChain;
use Psr\Http\Message\UploadedFileInterface;
use ipl\Html\Common\MultipleAttribute;

class FileElement extends InputElement
{
use MultipleAttribute;

protected $type = 'file';

/** @var UploadedFileInterface|UploadedFileInterface[] */
protected $value;

/** @var int The default maximum file size */
protected static $defaultMaxFileSize;

public function __construct($name, $attributes = null)
{
$this->getAttributes()->get('accept')->setSeparator(', ');

parent::__construct($name, $attributes);
}

public function getValueAttribute()
{
// Value attributes of file inputs are set only client-side.
return null;
}

public function getNameAttribute()
{
$name = parent::getNameAttribute();

return $this->isMultiple() ? ($name . '[]') : $name;
}

public function hasValue()
sukhwinder33445 marked this conversation as resolved.
Show resolved Hide resolved
{
if ($this->value === null) {
return false;
}

$file = $this->value;

if ($this->isMultiple()) {
return $file[0]->getError() !== UPLOAD_ERR_NO_FILE;
}

return $file->getError() !== UPLOAD_ERR_NO_FILE;
}

public function getValue()
{
return $this->hasValue() ? $this->value : null;
}

protected function addDefaultValidators(ValidatorChain $chain): void
{
$chain->add(new FileValidator([
'maxSize' => $this->getDefaultMaxFileSize(),
'mimeType' => array_filter(
(array) $this->getAttributes()->get('accept')->getValue(),
function ($type) {
// file inputs also allow file extensions in the accept attribute. These
// must not be passed as they don't resemble valid mime type definitions.
return is_string($type) && ltrim($type)[0] !== '.';
}
)
]));
}

protected function registerAttributeCallbacks(Attributes $attributes)
{
parent::registerAttributeCallbacks($attributes);
$this->registerMultipleAttributeCallback($attributes);
}

/**
* Get the system's default maximum file upload size
*
* @return int
*/
public function getDefaultMaxFileSize(): int
{
if (static::$defaultMaxFileSize === null) {
$ini = $this->convertIniToInteger(trim(static::getPostMaxSize()));
$max = $this->convertIniToInteger(trim(static::getUploadMaxFilesize()));
$min = max($ini, $max);
if ($ini > 0) {
$min = min($min, $ini);
}

if ($max > 0) {
$min = min($min, $max);
}

static::$defaultMaxFileSize = $min;
}

return static::$defaultMaxFileSize;
}

/**
* Converts a ini setting to a integer value
*
* @param string $setting
*
* @return int
*/
private function convertIniToInteger(string $setting): int
{
if (! is_numeric($setting)) {
$type = strtoupper(substr($setting, -1));
$setting = (int) substr($setting, 0, -1);

switch ($type) {
case 'K':
$setting *= 1024;
break;

case 'M':
$setting *= 1024 * 1024;
break;

case 'G':
$setting *= 1024 * 1024 * 1024;
break;

default:
break;
}
}

return (int) $setting;
}

/**
* Get the `post_max_size` INI setting
*
* @return string
*/
protected static function getPostMaxSize(): string
{
return ini_get('post_max_size') ?: '8M';
}

/**
* Get the `upload_max_filesize` INI setting
*
* @return string
*/
protected static function getUploadMaxFilesize(): string
{
return ini_get('upload_max_filesize') ?: '2M';
}
}
148 changes: 148 additions & 0 deletions tests/FormElement/FileElementTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

namespace ipl\Tests\Html\FormElement;

use GuzzleHttp\Psr7\ServerRequest;
use GuzzleHttp\Psr7\UploadedFile;
use ipl\Html\Form;
use ipl\Html\FormElement\FileElement;
use ipl\I18n\NoopTranslator;
use ipl\I18n\StaticTranslator;
use ipl\Tests\Html\Lib\FileElementWithAdjustableConfig;
use ipl\Tests\Html\TestCase;

class FileElementTest extends TestCase
sukhwinder33445 marked this conversation as resolved.
Show resolved Hide resolved
{
protected function setUp(): void
{
StaticTranslator::$instance = new NoopTranslator();
}

public function testElementLoading(): void
{
$form = (new Form())
->addElement('file', 'test_file');

$this->assertInstanceOf(FileElement::class, $form->getElement('test_file'));
}

public function testRendering()
{
$file = new FileElement('test_file', ['accept' => ['image/png', 'image/jpeg']]);

$this->assertHtml('<input name="test_file" type="file" accept="image/png, image/jpeg">', $file);
}

public function testUploadedFiles()
{
$fileToUpload = new UploadedFile(
'test/test.pdf',
500,
0,
'test.pdf',
'application/pdf'
);

$req = (new ServerRequest('POST', ServerRequest::getUriFromGlobals()))
->withUploadedFiles(['test_file' => $fileToUpload])
->withParsedBody([]);

$form = (new Form())
->addElement('file', 'test_file')
->handleRequest($req);

$this->assertSame($form->getValue('test_file'), $fileToUpload);
}

public function testMutipleAttributeAlsoChangesNameAttribute()
{
$file = new FileElement('test_file', ['multiple' => true]);

$this->assertHtml('<input multiple name="test_file[]" type="file">', $file);
$this->assertSame($file->getName(), 'test_file');
}

public function testValueAttributeIsNotRendered()
{
$file = new FileElement('test_file');

$file->setValue('test');
$this->assertHtml('<input name="test_file" type="file">', $file);
}

public function testDefaultMaxFileSizeAsBytesIsParsedCorrectly()
{
$element = new FileElementWithAdjustableConfig('test');
$element->setValue(new UploadedFile('test', 500, 0));

$element::$defaultMaxFileSize = null;
$element::$uploadMaxFilesize = '500';
$element::$postMaxSize = '1000'; // Just needs to be bigger than 500

$this->assertTrue($element->isValid(), implode("\n", $element->getMessages()));
}

public function testDefaultMaxFileSizeAsKiloBytesIsParsedCorrectly()
{
$element = new FileElementWithAdjustableConfig('test');
$element->setValue(new UploadedFile('test', 1024, 0));

$element::$defaultMaxFileSize = null;
$element::$uploadMaxFilesize = '1K';
$element::$postMaxSize = '2K'; // Just needs to be bigger than 1K

$this->assertTrue($element->isValid(), implode("\n", $element->getMessages()));
}

public function testDefaultMaxFileSizeAsMegaBytesIsParsedCorrectly()
{
$element = new FileElementWithAdjustableConfig('test');
$element->setValue(new UploadedFile('test', 102400, 0));

$element::$defaultMaxFileSize = null;
$element::$uploadMaxFilesize = '1M';
$element::$postMaxSize = '2M'; // Just needs to be bigger than 1M

$this->assertTrue($element->isValid(), implode("\n", $element->getMessages()));
}

public function testDefaultMaxFileSizeAsGigaBytesIsParsedCorrectly()
{
$element = new FileElementWithAdjustableConfig('test');
$element->setValue(new UploadedFile('test', 1073741824, 0));

$element::$defaultMaxFileSize = null;
$element::$uploadMaxFilesize = '1G';
$element::$postMaxSize = '2G'; // Just needs to be bigger than 1G

$this->assertTrue($element->isValid(), implode("\n", $element->getMessages()));
}

/**
* @depends testDefaultMaxFileSizeAsKiloBytesIsParsedCorrectly
*/
public function testUploadMaxFilesizeOverrulesPostMaxSize()
{
$element = new FileElementWithAdjustableConfig('test');
$element->setValue(new UploadedFile('test', 1024, 0));

$element::$defaultMaxFileSize = null;
$element::$uploadMaxFilesize = '1K';
$element::$postMaxSize = '2K'; // Just needs to be bigger than 1K

$this->assertTrue($element->isValid(), implode("\n", $element->getMessages()));

// ...if possible

$element::$defaultMaxFileSize = null;
$element::$uploadMaxFilesize = '2K';
$element::$postMaxSize = '1K';

$this->assertTrue($element->isValid(), implode("\n", $element->getMessages()));

$element->setValue(new UploadedFile('test', 2048, 0));
$element::$defaultMaxFileSize = null;

$this->assertFalse($element->isValid());
}
}
24 changes: 24 additions & 0 deletions tests/Lib/FileElementWithAdjustableConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace ipl\Tests\Html\Lib;

use ipl\Html\FormElement\FileElement;

class FileElementWithAdjustableConfig extends FileElement
{
public static $defaultMaxFileSize;

public static $postMaxSize = '8M';

public static $uploadMaxFilesize = '2M';

protected static function getPostMaxSize(): string
{
return self::$postMaxSize;
}

protected static function getUploadMaxFilesize(): string
{
return self::$uploadMaxFilesize;
}
}