Symfony 表单:上传的文件 - "This value should be of type string"
Symfony form: Uploaded file - "This value should be of type string"
[更新]:2019/06/24 - 23;28
使用表单上传文件,遇到如下错误:
This value should be of type string
表单生成器设置为 FileType
,因为它应该:
FormType
class DocumentType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
/** @var Document $salle */
$document=$options['data']; //Unused for now
$dataRoute=$options['data_route']; //Unused for now
$builder->add('nom')
->add('description')
->add('fichier', FileType::class, array(
//'data_class' is not the problem, tested without it.
//see comments if you don't know what it does.
'data_class'=>null,
'required'=>true,
))
->add('isActif', null, array('required'=>false));
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class'=>Document::class,
'data_route'=>null,
]);
}
}
我的 getter 和 setter 没有类型提示来确保 UploadedFile::__toString()
不会被调用:
实体
class Document {
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=100)
*/
private $nom;
/**
* @ORM\Column(type="string", length=40)
*/
private $fichier;
/**
* @ORM\Column(type="boolean")
*/
private $isActif;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Salle", inversedBy="documents")
* @ORM\JoinColumn(onDelete="CASCADE")
*/
private $salle;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Stand", inversedBy="documents")
* @ORM\JoinColumn(onDelete="CASCADE")
*/
private $stand;
public function __construct() {
$this->isActif=true;
}
public function __toString() {
return $this->getNom();
}
public function getId(): ?int {
return $this->id;
}
public function getNom(): ?string {
return $this->nom;
}
public function setNom(string $nom): self {
$this->nom=$nom;
return $this;
}
public function getFichier()/*Removed type hint*/ {
return $this->fichier;
}
public function setFichier(/*Removed type hint*/$fichier): self {
$this->fichier=$fichier;
return $this;
}
public function getIsActif(): ?bool {
return $this->isActif;
}
public function setIsActif(bool $isActif): self {
$this->isActif=$isActif;
return $this;
}
public function getSalle(): ?Salle {
return $this->salle;
}
public function setSalle(?Salle $salle): self {
$this->salle=$salle;
return $this;
}
public function getStand(): ?Stand {
return $this->stand;
}
public function setStand(?Stand $stand): self {
$this->stand=$stand;
return $this;
}
}
然而,表单验证器仍然需要 string
而不是 UploadedFile
对象。
控制器
/**
* @Route("/dashboard/documents/new", name="document_new", methods={"POST"})
* @Route("/dashboard/hall-{id}/documents/new", name="hall_document_new", methods={"POST"})
* @Route("/dashboard/stand-{id}/documents/new", name="stand_document_new", methods={"POST"})
* @param Router $router
* @param Request $request
* @param FileUploader $fileUploader
* @param SalleRepository $salleRepository
* @param Salle|null $salle
* @param Stand|null $stand
* @return JsonResponse
* @throws Exception
*/
public function new(Router $router, Request $request, FileUploader $fileUploader, SalleRepository $salleRepository, Salle $salle=null, Stand $stand=null) {
if($this->isGranted('ROLE_ORGANISATEUR')) {
$route=$router->match($request->getPathInfo())['_route'];
if(($route == 'hall_document_new' && !$salle) || ($route == 'stand_document_new' && !$stand)) {
//ToDo [SP] set message
return $this->json(array(
'messageInfo'=>array(
array(
'message'=>'',
'type'=>'error',
'length'=>'',
)
)
));
}
$document=new Document();
if($route == 'hall_document_new') {
$action=$this->generateUrl($route, array('id'=>$salle->getId()));
} elseif($route == 'stand_document_new') {
$action=$this->generateUrl($route, array('id'=>$stand->getId()));
} else {
$action=$this->generateUrl($route);
}
$form=$this->createForm(DocumentType::class, $document, array(
'action'=>$action,
'method'=>'POST',
'data_route'=>$route,
));
$form->handleRequest($request);
if($form->isSubmitted()) {
//Fail here, excepting a string value (shouldn't), got UploadedFile object
if($form->isValid()) {
if($route == 'hall_document_new') {
$document->setSalle($salle);
} elseif($route == 'stand_document_new') {
$document->setStand($stand);
} else {
$accueil=$salleRepository->findOneBy(array('isAccueil'=>true));
if($accueil) {
$document->setSalle($accueil);
} else {
//ToDo [SP] set message
return $this->json(array(
'messageInfo'=>array(
array(
'message'=>'',
'type'=>'',
'length'=>'',
)
)
));
}
}
/** @noinspection PhpParamsInspection */
$filename=$fileUploader->uploadDocument($document->getFichier());
if($filename) {
$document->setFichier($filename);
} else {
//ToDo [SP] set message
return $this->json(array(
'messageInfo'=>array(
array(
'message'=>'',
'type'=>'error',
'length'=>'',
)
)
));
}
$entityManager=$this->getDoctrine()->getManager();
$entityManager->persist($document);
$entityManager->flush();
return $this->json(array(
'modal'=>array(
'action'=>'unload',
'modal'=>'mdcDialog',
'content'=>null,
)
));
} else {
//ToDo [SP] Hide error message
return $this->json($form->getErrors(true, true));
// return $this->json(false);
}
}
return $this->json(array(
'modal'=>array(
'action'=>'load',
'modal'=>'mdcDialog',
'content'=>$this->renderView('salon/dashboard/document/new.html.twig', array(
'salle'=>$salle,
'stand'=>$stand,
'document'=>$document,
'form'=>$form->createView(),
)),
)
));
} else {
return $this->json(false);
}
}
services.yaml
parameters:
locale: 'en'
app_locales: en|fr
ul_document_path: '%kernel.root_dir%/../public/upload/document/'
services:
_defaults:
autowire: true
autoconfigure: true
bind:
$locales: '%app_locales%'
$defaultLocale: '%locale%'
$router: '@router'
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
App\Listener\kernelListener:
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse }
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
App\Service\FileUploader:
arguments:
$ulDocumentPath: '%ul_document_path%'
在 config/packages/validator.yaml
中注释掉这些行(如果它们存在):
framework:
validation:
# Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
#auto_mapping:
# App\Entity\: []
请参阅 Symfony 4.3 问题 [Validation] Activate auto-mapped validation via an annotation #32070。
在您的表单生成器中,您将 data_class
设置为 null
:
->add('fichier', FileType::class, array(
'data_class'=>null,
'required'=>true,
))
但 FileType
实际上需要一些数据 class 在内部定义。它有一些动态定义 class 的逻辑:它要么是 Symfony\Component\HttpFoundation\File\File
用于单个文件上传,要么是 null
用于多个文件。
因此,您实际上是在强制文件控制为多文件,但目标字段类型为 string
。 Symfony 会做一些 type-guessing 并相应地选择控件(例如,布尔实体字段将由复选框表示)——如果您没有指定明确的控件类型和选项。
所以,我认为您应该从选项中删除 data_class
,这将解决问题。
这里有一个 link 到特定的地方,使其表现得像我描述的那样:https://github.com/symfony/form/blob/master/Extension/Core/Type/FileType.php#L114
如您所见,它决定 data_class
值和一些其他值,然后 setDefaults()
,即这些正确的值就在那里——除非您覆盖它们。有点脆弱的架构,我想说,但这就是我们必须处理的问题。
[更新]:2019/06/24 - 23;28
使用表单上传文件,遇到如下错误:
This value should be of type string
表单生成器设置为 FileType
,因为它应该:
FormType
class DocumentType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
/** @var Document $salle */
$document=$options['data']; //Unused for now
$dataRoute=$options['data_route']; //Unused for now
$builder->add('nom')
->add('description')
->add('fichier', FileType::class, array(
//'data_class' is not the problem, tested without it.
//see comments if you don't know what it does.
'data_class'=>null,
'required'=>true,
))
->add('isActif', null, array('required'=>false));
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class'=>Document::class,
'data_route'=>null,
]);
}
}
我的 getter 和 setter 没有类型提示来确保 UploadedFile::__toString()
不会被调用:
实体
class Document {
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=100)
*/
private $nom;
/**
* @ORM\Column(type="string", length=40)
*/
private $fichier;
/**
* @ORM\Column(type="boolean")
*/
private $isActif;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Salle", inversedBy="documents")
* @ORM\JoinColumn(onDelete="CASCADE")
*/
private $salle;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Stand", inversedBy="documents")
* @ORM\JoinColumn(onDelete="CASCADE")
*/
private $stand;
public function __construct() {
$this->isActif=true;
}
public function __toString() {
return $this->getNom();
}
public function getId(): ?int {
return $this->id;
}
public function getNom(): ?string {
return $this->nom;
}
public function setNom(string $nom): self {
$this->nom=$nom;
return $this;
}
public function getFichier()/*Removed type hint*/ {
return $this->fichier;
}
public function setFichier(/*Removed type hint*/$fichier): self {
$this->fichier=$fichier;
return $this;
}
public function getIsActif(): ?bool {
return $this->isActif;
}
public function setIsActif(bool $isActif): self {
$this->isActif=$isActif;
return $this;
}
public function getSalle(): ?Salle {
return $this->salle;
}
public function setSalle(?Salle $salle): self {
$this->salle=$salle;
return $this;
}
public function getStand(): ?Stand {
return $this->stand;
}
public function setStand(?Stand $stand): self {
$this->stand=$stand;
return $this;
}
}
然而,表单验证器仍然需要 string
而不是 UploadedFile
对象。
控制器
/**
* @Route("/dashboard/documents/new", name="document_new", methods={"POST"})
* @Route("/dashboard/hall-{id}/documents/new", name="hall_document_new", methods={"POST"})
* @Route("/dashboard/stand-{id}/documents/new", name="stand_document_new", methods={"POST"})
* @param Router $router
* @param Request $request
* @param FileUploader $fileUploader
* @param SalleRepository $salleRepository
* @param Salle|null $salle
* @param Stand|null $stand
* @return JsonResponse
* @throws Exception
*/
public function new(Router $router, Request $request, FileUploader $fileUploader, SalleRepository $salleRepository, Salle $salle=null, Stand $stand=null) {
if($this->isGranted('ROLE_ORGANISATEUR')) {
$route=$router->match($request->getPathInfo())['_route'];
if(($route == 'hall_document_new' && !$salle) || ($route == 'stand_document_new' && !$stand)) {
//ToDo [SP] set message
return $this->json(array(
'messageInfo'=>array(
array(
'message'=>'',
'type'=>'error',
'length'=>'',
)
)
));
}
$document=new Document();
if($route == 'hall_document_new') {
$action=$this->generateUrl($route, array('id'=>$salle->getId()));
} elseif($route == 'stand_document_new') {
$action=$this->generateUrl($route, array('id'=>$stand->getId()));
} else {
$action=$this->generateUrl($route);
}
$form=$this->createForm(DocumentType::class, $document, array(
'action'=>$action,
'method'=>'POST',
'data_route'=>$route,
));
$form->handleRequest($request);
if($form->isSubmitted()) {
//Fail here, excepting a string value (shouldn't), got UploadedFile object
if($form->isValid()) {
if($route == 'hall_document_new') {
$document->setSalle($salle);
} elseif($route == 'stand_document_new') {
$document->setStand($stand);
} else {
$accueil=$salleRepository->findOneBy(array('isAccueil'=>true));
if($accueil) {
$document->setSalle($accueil);
} else {
//ToDo [SP] set message
return $this->json(array(
'messageInfo'=>array(
array(
'message'=>'',
'type'=>'',
'length'=>'',
)
)
));
}
}
/** @noinspection PhpParamsInspection */
$filename=$fileUploader->uploadDocument($document->getFichier());
if($filename) {
$document->setFichier($filename);
} else {
//ToDo [SP] set message
return $this->json(array(
'messageInfo'=>array(
array(
'message'=>'',
'type'=>'error',
'length'=>'',
)
)
));
}
$entityManager=$this->getDoctrine()->getManager();
$entityManager->persist($document);
$entityManager->flush();
return $this->json(array(
'modal'=>array(
'action'=>'unload',
'modal'=>'mdcDialog',
'content'=>null,
)
));
} else {
//ToDo [SP] Hide error message
return $this->json($form->getErrors(true, true));
// return $this->json(false);
}
}
return $this->json(array(
'modal'=>array(
'action'=>'load',
'modal'=>'mdcDialog',
'content'=>$this->renderView('salon/dashboard/document/new.html.twig', array(
'salle'=>$salle,
'stand'=>$stand,
'document'=>$document,
'form'=>$form->createView(),
)),
)
));
} else {
return $this->json(false);
}
}
services.yaml
parameters:
locale: 'en'
app_locales: en|fr
ul_document_path: '%kernel.root_dir%/../public/upload/document/'
services:
_defaults:
autowire: true
autoconfigure: true
bind:
$locales: '%app_locales%'
$defaultLocale: '%locale%'
$router: '@router'
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
App\Listener\kernelListener:
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse }
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
App\Service\FileUploader:
arguments:
$ulDocumentPath: '%ul_document_path%'
在 config/packages/validator.yaml
中注释掉这些行(如果它们存在):
framework:
validation:
# Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
#auto_mapping:
# App\Entity\: []
请参阅 Symfony 4.3 问题 [Validation] Activate auto-mapped validation via an annotation #32070。
在您的表单生成器中,您将 data_class
设置为 null
:
->add('fichier', FileType::class, array(
'data_class'=>null,
'required'=>true,
))
但 FileType
实际上需要一些数据 class 在内部定义。它有一些动态定义 class 的逻辑:它要么是 Symfony\Component\HttpFoundation\File\File
用于单个文件上传,要么是 null
用于多个文件。
因此,您实际上是在强制文件控制为多文件,但目标字段类型为 string
。 Symfony 会做一些 type-guessing 并相应地选择控件(例如,布尔实体字段将由复选框表示)——如果您没有指定明确的控件类型和选项。
所以,我认为您应该从选项中删除 data_class
,这将解决问题。
这里有一个 link 到特定的地方,使其表现得像我描述的那样:https://github.com/symfony/form/blob/master/Extension/Core/Type/FileType.php#L114
如您所见,它决定 data_class
值和一些其他值,然后 setDefaults()
,即这些正确的值就在那里——除非您覆盖它们。有点脆弱的架构,我想说,但这就是我们必须处理的问题。