Symfony 6: embedded form Collection with File upload in child form - symfony

I have an entity ClientFileAction which is parent to an entity Attachment in OneToMany relation. Attachment holds not only file path, but also information about files, like title, upload date, etc.
Attachment:
#[ORM\Column(type: 'string', length: 255)]
private $title;
#[ORM\Column(type: 'datetime')]
private $uploaded;
#[ORM\Column(type: 'string', length: 255)]
private $filePath;
When it comes to create a form type AttachmentType and upload files one to one, there is no problem:
AttachmentType:
$builder
->add('title', null, ['label' => 'Title', 'required' => true])
->add('attachmentFile', FileType::class, [
'label' => 'File',
'mapped' => false,
'required' => true,
'constraints' => [
new File([
'maxSize' => '1024k',
])
],
]);
In the controller I just get uploaded file with $attachmentFile = $form->get('attachmentFile')->getData(); and then proceed to the usual UploadedFile::move() stuff.
PROBLEM: EMBED AttachmentType IN PARENT FORM
But when I try to upload multiple attachments (not only files, but attachments with a title field), the uploaded file field seems to be unreachable.
ClientFileActionType:
$builder
->add('description', null, ['label' => 'Description', 'required' => true])
->add('attachments', CollectionType::class, ['label' => false,
'allow_add' => true,
'by_reference' => false,
'entry_type' => AttachmentType::class,
'entry_options' => ['label' => false],
]);
When I embed the AttachmentType as Collection inside ClientFileActionType, then, in the controller I don't find a way to get uploaded files:
$attachments = $form->get('attachments')->getData();
$attachments is an array of Attachment, and, as attachmentFile is not a mapped field, it dissapeared on the $form->handleRequest($request);.
I need a way to get unmapped attachmentFile fields of the child forms someway, something like:
$attachmentFiles = $form->get('attachments.attachmentFile')->getData();
That throws an error. Is there a correct way to do that?

I found the correct way to do it as I was typing the question.
The uploaded files are in the Request object, so a correct approach for this scenario of file upload management could be:
if ($form->isSubmitted() && $form->isValid())
{
$i=0;
$files = $request->files->all('client_file_action')['attachments'];
foreach ($files as $file)
{
$attachmentFile = $file['attachmentFile'];
$originalFilename = pathinfo($attachmentFile->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = $slugger->slug($originalFilename);
$newFilename = $safeFilename.'-' . uniqid() . '.' .$attachmentFile->guessExtension();
$attachmentFile->move('path/to/folder',$newFilename);
$attachment = $clientFileAction->findAttachment($i);
if ($attachment != null)
$attachment->setFilePath('path/to/folder/' . $newFilename);
$i++;
}
$clientFileActionRepository->add($clientFileAction, true);
}

Related

Symfony 6 - How can I change file upload to multiple file uploads

I'm working on a project where a user is able to upload a file. My code works when a single file is uploaded, but I need to change it so a user is able to upload multiple files.
I want to store the files in my database as String. Currently it is stored as example: "file1.png". When uploading multiple files I would like it to be stored as "file1.png;file2.png;file3.png".
However when I add the "multiple => true" in the form, I get an error when pressing submit by the validator that the input needs to be a String.
My best guess is that I need to use Data transformers, but after reading the docs I still don't know how to approach this. ?
Data Transform
This is the controller (currently it expects a single file, as for multiple I would use foreach):
\#\[Route('/new', name: 'app_blog_new', methods: \['GET', 'POST'\])\]
\#\[IsGranted('IS_AUTHENTICATED')\]
public function new(Request $request, BlogRepository $blogRepository, SluggerInterface $slugger, MailerInterface $mailer): Response
{
$blog = new Blog();
$form = $this-\>createForm(BlogType::class, $blog);
$form-\>handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$additionalImages = $form->get('additional_images')->getData();
if ($additionalImages) {
$originalFilename = pathinfo($additionalImages->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = $slugger->slug($originalFilename);
$newFilename = $safeFilename . '-' . uniqid() . '.' . $additionalImages->guessExtension();
try {
$additionalImages->move(
$this->getParameter('blogimage_directory'),
$newFilename
);
} catch (FileException $e) {
// ... handle exception if something happens during file upload
}
$blog->setAdditionalImages($newFilename);
}
}
If I add "multiple => true' to this form I get an "expected String" error on the front.
This is the form used to upload images to a blog:
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title')
->add('additional_images', FileType::class, [
'label' => 'Additional images',
'mapped' => false,
'multiple' => true,
'required' => false,
'constraints' => [`your text`
new File([
'maxSize' => '1024k',
'mimeTypes' => [
'image/*',
],
'mimeTypesMessage' => 'Please upload a valid image',
])
],
]);
$builder->get('additional_images')
->addModelTransformer(new CallbackTransformer(
function ($additionalAsArray) {
// transform the array to a string
return implode('; ', $additionalAsArray);
},
function ($additionalAsString) {
// transform the string back to an array
return explode('; ', $additionalAsString);
}
))
;
}
This is the blog entity class which contains the image(s)
#[ORM\Entity(repositoryClass: BlogRepository::class)]
class Blog
{
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $additional_images = null;
}
I tried adding 'multiple => true' to the form and it works, as the user is able to select multiple files. But after submitting I get "implode(): Argument #1 ($pieces) must be of type array, string given"
I found out that all I had to do was add "new All" to the form:
->add('additional_images', FileType::class, [
'label' => 'Additional images',
'mapped' => false,
'required' => false,
'multiple' => true,
'constraints' => [
new All([
new File([
'maxSize' => '1024k',
'mimeTypes' => [
'image/*',
],
'mimeTypesMessage' => 'Please upload a valid image',
])
])
],
]);
And made my controller work with an array:
$additionalImages = $form->get('additional_images')->getData();
if ($additionalImages) {
$result = array();
foreach ($additionalImages as $image)
{
$originalFilename = pathinfo($image->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = $slugger->slug($originalFilename);
$newFilename = $safeFilename . '-' . uniqid() . '.' . $image->guessExtension();
try {
$image->move(
$this->getParameter('blogimage_directory'),
$newFilename
);
} catch (FileException $e) {
// ... handle exception if something happens during file upload
}
$result[] = $newFilename;
}
$blog->setAdditionalImages(implode(";", $result));
}

Symfony - Error with the data parameter on a form

Context of the problem :
I created a symfony form.
Each tool has a collection of modules.
The user has a collection of modules of any tool.
What I want :
I want for each tool there are checkboxes corresponding to the tool's modules. The module checkboxes that the user owns are checked.
([] = checkbox)
Tool1 : []Module1 [x]Module2 [x]Module3
Tool2 : []Module4 [x]Module5
Tool3 : [x]Module6 []Module7
What I currently have:
For each tool, there are checkboxes corresponding to the tool's modules. But I have a problem to tick the checkboxes of user's modules. I get an error on the data parameter.
The form field :
$user = $options['user'];
$tools = $options['tools'];
foreach ($tools as $tool) {
$name = 'profile_'.str_replace(array('-', ' ', '.'), '', $tool->getLibelle());
$builder
->add($name, ChoiceType::class, [
'label' => $tool->getLibelle(),
'choices' => $tool->getModules(),
'choice_value' => 'id',
'choice_label' => function (?Module $module) {
return $module ? $module->getName() : '';
},
'data'=> $user->getModules(), // ERROR HERE
'expanded' => true,
'multiple' => true,
'mapped'=>false
])
;
}
[...]
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
'user'=> null,
'category'=> null,
'tools'=> null,
]);
}
The error :
My question :
Why do I have this error? How can I use the data parameter correctly to achieve the expected result?
You are on the good way, try to dump what is $user->getModules() returning, it has to be an array. May be is not returning an array, check te relation.
I did a little test and it works perfectly.
$name = 'name_field';
$builder->add($name,ChoiceType::class, array(
'choices' => array('Yes', 'No'),
'data' => array('Yes', false),
'mapped' => false,
'expanded' => true,
'multiple' => true
));
Here the solution :
Seems to me that $user->getModules() returns a collection. I managed to find another solution and that works (I changed the type of the field to EntityType)
foreach ($tools as $tool) {
$name = 'acces_'.str_replace(array('-', ' ', '.'), '', $tool->getLibelle());
$builder
->add($name, EntityType::class, [
'class'=> Module::class,
'label' => $tool->getLibelle(),
'data' => $user->getModules(),
'choices'=> $tool->getModules(),
'choice_value' => 'id',
'choice_label' => 'name',
'expanded' => true,
'multiple' => true,
'required' => true,
'mapped'=>false,
])
;
}
ChoiceType: data parameter need array
EntityType: data parameter need collection
Thanks for the help !

Use formBuilder in a foreach to generate multiple fields

I'm developing a project with Symfony 5, currently busy with an import of data from CSV to a database. It's requested to offer the client the possibility to choose, which data from the CSV is corresponding to which field of the database.
For example, I want to import users to the database.
The array of fields from the database:
$databaseFields = ['username', 'email', 'lastname', 'firstname'];
The array of headers, frome the CSV:
$headersCsvArray = ['email', 'username', 'lastName'];
Based on this 2 arrays, I need to build a form:
foreach ($databaseFields as $databaseField) {
$builder->add('extraFields', ChoiceType::class,
[
'label' => $databaseField,
'placeholder' => 'Choose a column from the excel file',
'choices' => $headersCsvArray,
'multiple' => false,
'expanded' => false,
'required' => false
]
);
}
To be clear, I need an input for each field of the User entity, with a dropdown with every header from the CSV.
This form is link to an ImportUserFormModel, with basically nothing for the moment:
class ImportUserFormModel
{
public $extraFields;
}
The result I need when I do $form->getData('extraData'):
$importDatas = [
'username' =>
[
'username' => true,
'emails' => false,
'lastName' => false
],
'email' =>
[
'username' => false,
'emails' => true,
'lastName' => false
],
'lastname' =>
[
'username' => false,
'emails' => false,
'lastName' => true
],
'firstname' =>
[
'username' => false,
'emails' => false,
'lastName' => true
],
];
Where the TRUE value is the one choose from the select dropdown.
Here is the result I actually have:
So I'm asking myself: Is it possible to use the foreach here?
Ok if you want it dynamic this could be a solution.
Create a Model for your extra field too, like
class ExtraField {
private $databseField;
private $csvField;
// ...
}
And in your base model use it as a collection
class ImportUserFormModel
{
private $extraFields;
public getFields(): ExtraField[] {
// ...
}
public addField(ExtraField $extraField) {
// ...
}
}
Create a FormType for your ExtraField model and another one for your base model where extraFields is a CollectionType.
Then before the form creation define the fields in your model
$importUserFormModel = new ImportUserFormModel();
foreach ($databaseFields as $databaseField) {
$extraField = new ExtraField();
$extraField->setDatabaseField($databaseField);
$importUserFormModel->addExtraField($extraField);
}
$form = $this->createForm(ImportUserFormModelType::class, $importUserFormModel);
// ....
The thing i'm not sure about is how to put the name of the database field in the label of the form field but you could template it.

Handle array of string in edit form in Sonata Admin Bundle

In one of my entities, I got an array attribute. I thought Sonata Admin Bundle could handle it but it seems that it requires some attention.
I'm pretty sure SONATA_TYPE_COLLECTION field type could handle that but I didn't find any clue on how to configure the field in configureFormFields()
Do anyone know how to configure it ?
Thanks
I give you an example that I used:
Entity:
/**
* #ORM\Column(type="array", nullable=true)
*/
private $tablaXY = [];
use Sonata\AdminBundle\Form\Type\CollectionType;
->add('tablaXY',CollectionType::class, [
'required' => false,
'by_reference' => false, // Use this because of reasons
'allow_add' => true, // True if you want allow adding new entries to the collection
'allow_delete' => true, // True if you want to allow deleting entries
'prototype' => true, // True if you want to use a custom form type
'entry_type' => TablaType::class, // Form type for the Entity that is being attached to the object
],
[
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'position',
]
)
form:
class TablaType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('ejeX', TextType::class,['label' => 'Eje X (texto)',
'required' => true,'attr' => array('class' => 'form-control'),])
->add('ejeY', NumberType::class,['label' => 'Eje Y (NĂºmero)',
'required' => true])
;
}
}
You can use the Sonata CollectionType class, which is capable of adding and removing elements from an array:
use Sonata\AdminBundle\Form\Type\CollectionType;
...
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('group')
->add('roles', CollectionType::class, array(
'allow_add' => true,
'allow_delete' => true,
))
->end()
;
}

How validate a selection of at least one element (or N elements) in Symfony 2?

My form as field users (entity type). How can i add validation in order to specificity that at least one user should be selected? Actually i'm adding an event listener but i don't know if this is a legit solution or not:
public function buildForm(\Symfony\Component\Form\FormBuilder $builder,
array $options)
{
$builder
->add('title', 'text', array(
'label' => 'Titolo'
))
->add('content', 'textarea', array(
'label' => 'Contenuto'
))
->add('sender_text', 'text', array(
'label' => 'Mittente testuale',
))
->add('users', 'entity', array(
'label' => 'Destinatari',
'class' => 'DL\FidelityBundle\Entity\User',
'property' => 'select_label',
'multiple' => true
));
;
// Valida il numero di utenti selezionati
$builder->addEventListener(\Symfony\Component\Form\FormEvents::POST_BIND,
function($event) {
$form = $event->getForm();
$data = $event->getData();
if(!$data->users->isEmpty()) return;
$msg = 'Occorre specificare almeno un utente destinatario';
$form->get('users')->addError(new FormError($msg));
});
}
As of Symfony 2.1, you can use the Count constraint. If you are on 2.0, you can simply copy the constraint to your project and adapt its namespaces and its API (which was slightly changed between 2.0 and 2.1).
/**
* #Assert\Count(min = 1, minMessage = "Occorre specificare almeno un utente destinatario")
*/
private $users = new ArrayCollection();
Have you tried using Count constraint validator?
I suppose your code will look like this:
->add('users', 'entity', array(
'label' => 'Destinatari',
'class' => 'DL\FidelityBundle\Entity\User',
'property' => 'select_label',
'multiple' => true,
'constraints' => new Count(
array('min' => 1, 'minMessage' => "Please select at least one user')
),
));
Have a look at the validation component: http://symfony.com/doc/current/book/validation.html
You can write a callback constraint within the object you want to validate:
use Symfony\Component\Validator\ExecutionContext;
public function isUsersValid(ExecutionContext $context)
{
if ($this->users->isEmpty()) {
$propertyPath = $context->getPropertyPath() . '.users';
$context->setPropertyPath($propertyPath);
$context->addViolation('Occorre specificare almeno un utente destinatario', array(), null);
}
}
See the callback constraint page how to add this constraint to your entity (this depens if you are using annotations or yaml/xml).

Resources