Skip to content

FormHydrator: гидрация UploadedFileInterface из POST+FILES (включая nested forms) #90

@alse0017

Description

@alse0017

Proposed new feature or change

FormHydrator::populateFromPost() и populateFromPostAndValidate() сейчас берут только $request->getParsedBody(), поэтому UploadedFileInterface-поля (включая nested/collection формы) не гидрируются без ручного merge POST + FILES.

Нужна доработка “из коробки” для файловой гидрации (в т.ч. вложенных структур), с корректной фильтрацией UPLOAD_ERR_NO_FILE (иначе в модель попадают “пустые” файлы).

Сейчас обходной путь такой:

$post = array_merge_recursive(
    $request->getParsedBody(),
    UploadedFilesFilter::filter($request->getUploadedFiles())
);
$this->formHydrator->populateAndValidate($form, $post);
<?php

declare(strict_types=1);

namespace App;

use Psr\Http\Message\UploadedFileInterface;

final class UploadedFilesFilter
{
    /**
     * @param array<array-key, mixed> $uploadedFiles
     *
     * @return array<array-key, mixed>
     */
    public static function filter(array $uploadedFiles): array
    {
        $result = [];

        foreach ($uploadedFiles as $key => $value) {
            if ($value instanceof UploadedFileInterface) {
                if ($value->getError() === UPLOAD_ERR_NO_FILE) {
                    continue;
                }

                $result[$key] = $value;
                continue;
            }

            if (is_array($value)) {
                $filtered = self::filter($value);
                if ($filtered !== []) {
                    $result[$key] = $filtered;
                }
                continue;
            }

            $result[$key] = $value;
        }

        return $result;
    }
}

Пример формы с вложенными изображениями

use Psr\Http\Message\UploadedFileInterface;
use Yiisoft\FormModel\FormModel;
use Yiisoft\Hydrator\Attribute\Parameter\Collection;
use Yiisoft\Validator\Label;
use Yiisoft\Validator\Rule\Each;
use Yiisoft\Validator\Rule\Image\Image;
use Yiisoft\Validator\Rule\Nested;
use Yiisoft\Validator\Rule\Length;

final class Form extends FormModel
{
    #[Label('Title')]
    #[Length(min: 2)]
    public string $title = '';
    
    #[Label('Picture')]
    #[Image(skipOnEmpty: true)]
    public ?UploadedFileInterface $picture = null;

    #[Label('Nested picture')]
    #[Nested(PictureForm::class)]
    public ?PictureForm $nestedPicture = null;

    /** @var array<UploadedFileInterface> */
    #[Label('Gallery')]
    #[Each([new Image(skipOnEmpty: true)])]
    public array $pictures = [];

    /** @var array<PictureForm> */
    #[Collection(PictureForm::class)]
    #[Label('Nested gallery')]
    #[Each([new Nested(PictureForm::class)])]
    public array $nestedPictures = [];
}

final class PictureForm extends FormModel
{
    #[Label('Description')]
    #[Length(min: 0)]
    public string $description = '';
    
    #[Label('Picture')]
    #[Image(skipOnEmpty: true)]
    public ?UploadedFileInterface $picture = null;

    /** @var array<NestedPictureForm> */
    #[Collection(NestedPictureForm::class)]
    #[Label('Nested gallery level 2')]
    #[Each([new Nested(NestedPictureForm::class)])]
    public array $nestedPictures = [];
}

final class NestedPictureForm extends FormModel
{
    #[Label('Description')]
    #[Length(min: 0)]
    public string $description = '';
    
    #[Label('Picture')]
    #[Image(skipOnEmpty: true)]
    public ?UploadedFileInterface $picture = null;
}

Input names

  • Form[title]
  • Form[picture]
  • Form[pictures][]
  • Form[nestedPicture][description]
  • Form[nestedPicture][picture]
  • Form[nestedPictures][0][description]
  • Form[nestedPictures][0][picture]
  • Form[nestedPictures][0][nestedPictures][0][description]
  • Form[nestedPictures][0][nestedPictures][0][picture]

Ожидание

Встроенный механизм в FormHydrator (новые методы или флаг в populateFromPost*), чтобы ручной merge/фильтр не дублировались в каждом action.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions