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

Fix upload file to subdir #6218

Open
wants to merge 4 commits into
base: 4.x
Choose a base branch
from
Open

Fix upload file to subdir #6218

wants to merge 4 commits into from

Conversation

zorn-v
Copy link
Contributor

@zorn-v zorn-v commented Mar 20, 2024

In docs (https://symfony.com/bundles/EasyAdminBundle/current/fields/ImageField.html) and annotation of setUploadedFileNamePattern method, noticed that you can combine uploaded file name like [year]/[month]/[day]/[slug]-[contenthash].[extension]
But in such case you get that name with subdirs in database field, but not in filesystem.
UploadedFile::move works in such way (get basename of second parameter) because of
https://github.com/symfony/symfony/blob/d90416ff001b3af6197df477d19eff4f101dac96/src/Symfony/Component/HttpFoundation/File/UploadedFile.php#L185

https://github.com/symfony/symfony/blob/d90416ff001b3af6197df477d19eff4f101dac96/src/Symfony/Component/HttpFoundation/File/File.php#L125
and
https://github.com/symfony/symfony/blob/d90416ff001b3af6197df477d19eff4f101dac96/src/Symfony/Component/HttpFoundation/File/File.php#L133-L139

As workaround you can add form type option upload_new for now, like

ImageField::new('image')
    ->setBasePath('uploads')
    ->setUploadDir('public/uploads')
    ->setFormTypeOption('upload_new', static function ($file, string $uploadDir, string $fileName) {
        $file->move($uploadDir.dirname($fileName), $fileName);
    })
    ->setUploadedFileNamePattern('entity/[year]-[month]/[ulid].[extension]')

@zorn-v zorn-v marked this pull request as draft March 20, 2024 22:55
@zorn-v
Copy link
Contributor Author

zorn-v commented Mar 20, 2024

Also need fix while edit entity without changing image - it changes field in db to basename of file

@zorn-v zorn-v marked this pull request as ready for review March 21, 2024 00:11
@zorn-v
Copy link
Contributor Author

zorn-v commented Mar 21, 2024

Workaround for second fix is cumbersome but also possible

//...
use App\EasyAdmin\Form\Type\FileUploadType;
//...
ImageField::new('image')->setFormType(FileUploadType::class)
//..
<?php

namespace App\EasyAdmin\Form\Type;

use App\EasyAdmin\Form\DataTransformer\StringToFileTransformer;
use EasyCorp\Bundle\EasyAdminBundle\Form\Type\FileUploadType as BaseType;
use Psr\Container\ContainerInterface;
use Symfony\Component\Form\FormBuilderInterface;

class FileUploadType extends BaseType
{
    public function __construct(ContainerInterface $parameterBag)
    {
        parent::__construct($parameterBag->get('kernel.project_dir'));
    }

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        parent::buildForm($builder, $options);
        $uploadDir = $options['upload_dir'];
        $uploadFilename = $options['upload_filename'];
        $uploadValidate = $options['upload_validate'];

        $builder->resetModelTransformers();
        $builder->addModelTransformer(new StringToFileTransformer($uploadDir, $uploadFilename, $uploadValidate, $options['multiple']));
    }
}
<?php

namespace App\EasyAdmin\Form\DataTransformer;

use EasyCorp\Bundle\EasyAdminBundle\Form\DataTransformer\StringToFileTransformer as BaseTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;

class StringToFileTransformer extends BaseTransformer
{
    private string $uploadDir;
    private $uploadFilename;
    private $uploadValidate;
    private bool $multiple;

    public function __construct(string $uploadDir, callable $uploadFilename, callable $uploadValidate, bool $multiple)
    {
        parent::__construct($uploadDir, $uploadFilename, $uploadValidate, $multiple);

        $this->uploadDir = $uploadDir;
        $this->uploadFilename = $uploadFilename;
        $this->uploadValidate = $uploadValidate;
        $this->multiple = $multiple;
    }

    public function reverseTransform($value): mixed
    {
        if (null === $value || [] === $value) {
            return null;
        }

        if (!$this->multiple) {
            return $this->doReverseTransform($value);
        }

        if (!\is_array($value)) {
            throw new TransformationFailedException('Expected an array or null.');
        }

        return array_map([$this, 'doReverseTransform'], $value);
    }

    private function doReverseTransform($value): ?string
    {
        if (null === $value) {
            return null;
        }

        if ($value instanceof UploadedFile) {
            if (!$value->isValid()) {
                throw new TransformationFailedException($value->getErrorMessage());
            }

            $filename = ($this->uploadFilename)($value);

            return ($this->uploadValidate)($filename);
        }

        if ($value instanceof File) {
            return str_replace($this->uploadDir, '', $value->getPathname());
        }

        throw new TransformationFailedException('Expected an instance of File or null.');
    }
}

@zorn-v
Copy link
Contributor Author

zorn-v commented Mar 21, 2024

Probably will be better extend Symfony\Component\HttpFoundation\File\File with some additional methods (like getDownloadPath) and transform to that class in data transformer for forms.
This will allow to simply add "preview" in edit form for example.

Something like

<?php

namespace App\EasyAdmin\Form;

use Symfony\Component\HttpFoundation\File\File;

class DownloadableFile extends File
{
    private string $downloadPath;

    public function __construct(string $basePath, string $downloadPath)
    {
        parent::__construct($basePath.$downloadPath);
        $this->downloadPath = $downloadPath;
    }

    public function getDownloadPath()
    {
        return $this->downloadPath;
    }
}

and then in StringToFileTransformer

//... in the doTransform
        if (is_file($this->uploadDir.$value)) {
            return new DownloadableFile($this->uploadDir, $value);
        }
//...
//... in the doReverseTransform

        if ($value instanceof DownloadableFile) {
            return $value->getDownloadPath();
        }
//...

In such case getDownloadPath will return EXACTLY value in db field without error prone "transformations"

PS. If you are interested, I can make another PR with it and close this one.

@zorn-v
Copy link
Contributor Author

zorn-v commented Mar 31, 2024

Example widget for edit with preview.
Yeah, we can get db value for download path like ea_vars.field.value but I do like this.

{% block app_imageupload_widget %}
    {% if not multiple %}
        {% set firstImg = currentFiles|first %}
        {% if firstImg %}
            {% set html_id = 'ea-lightbox-' ~ ea_vars.field.uniqueId %}
            {% set image = download_path ~ firstImg.downloadPath %}
            <a href="javascript:;" class="ea-lightbox-thumbnail" data-ea-lightbox-content-selector="#{{ html_id }}">
                <img src="{{ asset(image) }}" class="img-fluid">
            </a>

            <div id="{{ html_id }}" class="ea-lightbox">
                <img src="{{ asset(image) }}">
            </div>
        {% else %}
            <img src="{{ asset('img/no-image.png') }}" class="img-fluid">
        {% endif %}
    {% endif %}
    {{ block('ea_fileupload_widget') }}
{% endblock %}
.app-image-field .form-widget {
    width: 200px;
    border: 1px solid var(--form-input-border-color);
    border-radius: var(--border-radius);
    overflow: hidden;
}
.app-image-field .img-fluid {
    width: 100%;
    height: 150px;
    object-fit: cover;
}
.app-image-field .custom-file-label,
.app-image-field .ea-fileupload .input-group-text {
    border-color: transparent;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant